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 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.google.crypto.tink.subtle.AesGcmHkdfStreaming
import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.header.HeaderReader import com.stevesoltys.seedvault.header.HeaderReader
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH
import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE
import com.stevesoltys.seedvault.header.SegmentHeader import com.stevesoltys.seedvault.header.SegmentHeader
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.VersionHeader import com.stevesoltys.seedvault.header.VersionHeader
import org.calyxos.backup.storage.crypto.StreamCrypto import org.calyxos.seedvault.core.crypto.CoreCrypto
import org.calyxos.backup.storage.crypto.StreamCrypto.deriveStreamKey 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.EOFException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.ByteBuffer
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
/** /**
@ -47,13 +57,18 @@ internal interface Crypto {
*/ */
fun getRandomBytes(size: Int): ByteArray 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. * A secret key of size [KEY_SIZE_BYTES]
* @param suffix empty string for normal APKs and the name of the split in case of an APK split * 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 * Returns a [AesGcmHkdfStreaming] encrypting stream
@ -75,6 +90,40 @@ internal interface Crypto {
associatedData: ByteArray, associatedData: ByteArray,
): InputStream ): 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] * Reads and decrypts a [VersionHeader] from the given [InputStream]
* and ensures that the expected version, package name and key match * 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 const val TYPE_ICONS: Byte = 0x03
internal class CryptoImpl( internal class CryptoImpl(
private val context: Context,
private val keyManager: KeyManager, private val keyManager: KeyManager,
private val cipherFactory: CipherFactory, private val cipherFactory: CipherFactory,
private val headerReader: HeaderReader, private val headerReader: HeaderReader,
) : Crypto { ) : Crypto {
private val key: ByteArray by lazy { private val keyV1: ByteArray by lazy {
deriveStreamKey(keyManager.getMainKey(), "app data key".toByteArray()) 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 { override fun getRandomBytes(size: Int) = ByteArray(size).apply {
secureRandom.nextBytes(this) 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 { override fun getNameForPackage(salt: String, packageName: String): String {
return sha256("$salt$packageName".toByteArray()).encodeBase64() return sha256("$salt$packageName".toByteArray()).encodeBase64()
} }
@Deprecated("only for v1")
override fun getNameForApk(salt: String, packageName: String, suffix: String): String { override fun getNameForApk(salt: String, packageName: String, suffix: String): String {
return sha256("${salt}APK$packageName$suffix".toByteArray()).encodeBase64() return sha256("${salt}APK$packageName$suffix".toByteArray()).encodeBase64()
} }
private fun sha256(bytes: ByteArray): ByteArray { override fun sha256(bytes: ByteArray): ByteArray {
val messageDigest: MessageDigest = try { val messageDigest: MessageDigest = try {
MessageDigest.getInstance("SHA-256") MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
@ -155,21 +239,19 @@ internal class CryptoImpl(
return messageDigest.digest() return messageDigest.digest()
} }
@Deprecated("only for v1")
@Throws(IOException::class, GeneralSecurityException::class) @Throws(IOException::class, GeneralSecurityException::class)
override fun newEncryptingStream( override fun newEncryptingStreamV1(
outputStream: OutputStream, outputStream: OutputStream,
associatedData: ByteArray, associatedData: ByteArray,
): OutputStream { ): OutputStream = CoreCrypto.newEncryptingStream(keyV1, outputStream, associatedData)
return StreamCrypto.newEncryptingStream(key, outputStream, associatedData)
}
@Deprecated("only for v1")
@Throws(IOException::class, GeneralSecurityException::class) @Throws(IOException::class, GeneralSecurityException::class)
override fun newDecryptingStream( override fun newDecryptingStreamV1(
inputStream: InputStream, inputStream: InputStream,
associatedData: ByteArray, associatedData: ByteArray,
): InputStream { ): InputStream = CoreCrypto.newDecryptingStream(keyV1, inputStream, associatedData)
return StreamCrypto.newDecryptingStream(key, inputStream, associatedData)
}
@Suppress("Deprecation") @Suppress("Deprecation")
@Throws(IOException::class, SecurityException::class) @Throws(IOException::class, SecurityException::class)

View file

@ -5,6 +5,7 @@
package com.stevesoltys.seedvault.crypto package com.stevesoltys.seedvault.crypto
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
import java.security.KeyStore import java.security.KeyStore
@ -20,5 +21,5 @@ val cryptoModule = module {
} }
KeyManagerImpl(keyStore) 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) if (version == 0.toByte()) return readMetadataV0(inputStream, expectedToken)
val metadataBytes = try { val metadataBytes = try {
crypto.newDecryptingStream(inputStream, getAD(version, expectedToken)).readBytes() crypto.newDecryptingStreamV1(inputStream, getAD(version, expectedToken)).readBytes()
} catch (e: GeneralSecurityException) { } catch (e: GeneralSecurityException) {
throw DecryptionFailedException(e) throw DecryptionFailedException(e)
} }

View file

@ -25,7 +25,7 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
@Throws(IOException::class) @Throws(IOException::class)
override fun write(metadata: BackupMetadata, outputStream: OutputStream) { override fun write(metadata: BackupMetadata, outputStream: OutputStream) {
outputStream.write(ByteArray(1).apply { this[0] = metadata.version }) 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)) it.write(encode(metadata))
} }
} }

View file

@ -139,7 +139,7 @@ internal class FullBackup(
// store version header // store version header
val state = this.state ?: throw AssertionError() val state = this.state ?: throw AssertionError()
outputStream.write(ByteArray(1) { VERSION }) 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 } // this lambda is only called before we actually write backup data the first time
return TRANSPORT_OK return TRANSPORT_OK
} }

View file

@ -259,7 +259,7 @@ internal class KVBackup(
backend.save(handle).use { outputStream -> backend.save(handle).use { outputStream ->
outputStream.write(ByteArray(1) { VERSION }) outputStream.write(ByteArray(1) { VERSION })
val ad = getADForKV(VERSION, packageName) val ad = getADForKV(VERSION, packageName)
crypto.newEncryptingStream(outputStream, ad).use { encryptedStream -> crypto.newEncryptingStreamV1(outputStream, ad).use { encryptedStream ->
GZIPOutputStream(encryptedStream).use { gZipStream -> GZIPOutputStream(encryptedStream).use { gZipStream ->
dbManager.getDbInputStream(packageName).use { inputStream -> dbManager.getDbInputStream(packageName).use { inputStream ->
inputStream.copyTo(gZipStream) inputStream.copyTo(gZipStream)

View file

@ -119,7 +119,7 @@ internal class FullRestore(
val inputStream = backend.load(handle) val inputStream = backend.load(handle)
val version = headerReader.readVersion(inputStream, state.version) val version = headerReader.readVersion(inputStream, state.version)
val ad = getADForFull(version, packageName) val ad = getADForFull(version, packageName)
state.inputStream = crypto.newDecryptingStream(inputStream, ad) state.inputStream = crypto.newDecryptingStreamV1(inputStream, ad)
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.w(TAG, "Error getting input stream for $packageName", e) Log.w(TAG, "Error getting input stream for $packageName", e)

View file

@ -161,7 +161,7 @@ internal class KVRestore(
backend.load(handle).use { inputStream -> backend.load(handle).use { inputStream ->
headerReader.readVersion(inputStream, state.version) headerReader.readVersion(inputStream, state.version)
val ad = getADForKV(VERSION, packageName) val ad = getADForKV(VERSION, packageName)
crypto.newDecryptingStream(inputStream, ad).use { decryptedStream -> crypto.newDecryptingStreamV1(inputStream, ad).use { decryptedStream ->
GZIPInputStream(decryptedStream).use { gzipStream -> GZIPInputStream(decryptedStream).use { gzipStream ->
dbManager.getDbOutputStream(packageName).use { outputStream -> dbManager.getDbOutputStream(packageName).use { outputStream ->
gzipStream.copyTo(outputStream) gzipStream.copyTo(outputStream)

View file

@ -49,7 +49,7 @@ internal class IconManager(
fun uploadIcons(token: Long, outputStream: OutputStream) { fun uploadIcons(token: Long, outputStream: OutputStream) {
Log.d(TAG, "Start uploading icons") Log.d(TAG, "Start uploading icons")
val packageManager = context.packageManager val packageManager = context.packageManager
crypto.newEncryptingStream(outputStream, getAD(VERSION, token)).use { cryptoStream -> crypto.newEncryptingStreamV1(outputStream, getAD(VERSION, token)).use { cryptoStream ->
ZipOutputStream(cryptoStream).use { zip -> ZipOutputStream(cryptoStream).use { zip ->
zip.setLevel(BEST_SPEED) zip.setLevel(BEST_SPEED)
val entries = mutableSetOf<String>() val entries = mutableSetOf<String>()
@ -89,7 +89,7 @@ internal class IconManager(
if (!folder.isDirectory && !folder.mkdirs()) if (!folder.isDirectory && !folder.mkdirs())
throw IOException("Can't create cache folder for icons") throw IOException("Can't create cache folder for icons")
val set = mutableSetOf<String>() val set = mutableSetOf<String>()
crypto.newDecryptingStream(inputStream, getAD(version, token)).use { cryptoStream -> crypto.newDecryptingStreamV1(inputStream, getAD(version, token)).use { cryptoStream ->
ZipInputStream(cryptoStream).use { zip -> ZipInputStream(cryptoStream).use { zip ->
var entry = zip.nextEntry var entry = zip.nextEntry
while (entry != null) { while (entry != null) {

View file

@ -5,6 +5,7 @@
package com.stevesoltys.seedvault package com.stevesoltys.seedvault
import com.stevesoltys.seedvault.backend.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.crypto.CipherFactory import com.stevesoltys.seedvault.crypto.CipherFactory
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.Crypto 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.crypto.KeyManagerTestImpl
import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.metadataModule import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.backend.saf.storagePluginModuleSaf
import com.stevesoltys.seedvault.restore.install.installModule import com.stevesoltys.seedvault.restore.install.installModule
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
@ -33,7 +33,7 @@ class TestApp : App() {
private val testCryptoModule = module { private val testCryptoModule = module {
factory<CipherFactory> { CipherFactoryImpl(get()) } factory<CipherFactory> { CipherFactoryImpl(get()) }
single<KeyManager> { KeyManagerTestImpl() } single<KeyManager> { KeyManagerTestImpl() }
single<Crypto> { CryptoImpl(get(), get(), get()) } single<Crypto> { CryptoImpl(this@TestApp, get(), get(), get()) }
} }
private val packageService: PackageService = mockk() private val packageService: PackageService = mockk()
private val appModule = module { private val appModule = module {

View file

@ -5,6 +5,7 @@
package com.stevesoltys.seedvault.crypto package com.stevesoltys.seedvault.crypto
import android.content.Context
import com.stevesoltys.seedvault.getRandomBase64 import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.header.HeaderReaderImpl 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.ByteArrayInputStream
import java.io.IOException import java.io.IOException
@Suppress("DEPRECATION")
@TestInstance(PER_METHOD) @TestInstance(PER_METHOD)
class CryptoImplTest { class CryptoImplTest {
private val context = mockk<Context>()
private val keyManager = mockk<KeyManager>() private val keyManager = mockk<KeyManager>()
private val cipherFactory = mockk<CipherFactory>() private val cipherFactory = mockk<CipherFactory>()
private val headerReader = HeaderReaderImpl() private val headerReader = HeaderReaderImpl()
private val crypto = CryptoImpl(keyManager, cipherFactory, headerReader) private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader)
@Test @Test
fun `decrypting multiple segments on empty stream throws`() { fun `decrypting multiple segments on empty stream throws`() {

View file

@ -5,8 +5,10 @@
package com.stevesoltys.seedvault.crypto package com.stevesoltys.seedvault.crypto
import android.content.Context
import com.stevesoltys.seedvault.assertReadEquals import com.stevesoltys.seedvault.assertReadEquals
import com.stevesoltys.seedvault.header.HeaderReaderImpl import com.stevesoltys.seedvault.header.HeaderReaderImpl
import io.mockk.mockk
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.not import org.hamcrest.Matchers.not
@ -19,13 +21,15 @@ import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import kotlin.random.Random import kotlin.random.Random
@Suppress("DEPRECATION")
@TestInstance(PER_METHOD) @TestInstance(PER_METHOD)
class CryptoIntegrationTest { class CryptoIntegrationTest {
private val context = mockk<Context>()
private val keyManager = KeyManagerTestImpl() private val keyManager = KeyManagerTestImpl()
private val cipherFactory = CipherFactoryImpl(keyManager) private val cipherFactory = CipherFactoryImpl(keyManager)
private val headerReader = HeaderReaderImpl() 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)) private val cleartext = Random.nextBytes(Random.nextInt(1, 422300))
@ -38,7 +42,18 @@ class CryptoIntegrationTest {
} }
@Test @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 ad = Random.nextBytes(42)
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
crypto.newEncryptingStream(outputStream, ad).use { it.write(cleartext) } crypto.newEncryptingStream(outputStream, ad).use { it.write(cleartext) }
@ -49,7 +64,19 @@ class CryptoIntegrationTest {
} }
@Test @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() val outputStream = ByteArrayOutputStream()
crypto.newEncryptingStream(outputStream, Random.nextBytes(42)).use { it.write(cleartext) } crypto.newEncryptingStream(outputStream, Random.nextBytes(42)).use { it.write(cleartext) }
val inputStream = ByteArrayInputStream(outputStream.toByteArray()) val inputStream = ByteArrayInputStream(outputStream.toByteArray())

View file

@ -5,6 +5,7 @@
package com.stevesoltys.seedvault.crypto package com.stevesoltys.seedvault.crypto
import android.content.Context
import com.stevesoltys.seedvault.assertContains import com.stevesoltys.seedvault.assertContains
import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
@ -36,11 +37,12 @@ import kotlin.random.Random
@TestInstance(PER_METHOD) @TestInstance(PER_METHOD)
class CryptoTest { class CryptoTest {
private val context = mockk<Context>()
private val keyManager = mockk<KeyManager>() private val keyManager = mockk<KeyManager>()
private val cipherFactory = mockk<CipherFactory>() private val cipherFactory = mockk<CipherFactory>()
private val headerReader = mockk<HeaderReader>() 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>() private val cipher = mockk<Cipher>()

View file

@ -5,6 +5,7 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.metadata
import android.content.Context
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.CryptoImpl
import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES 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.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
@ -30,10 +32,11 @@ internal class MetadataReadWriteTest {
private val secretKey = SecretKeySpec( private val secretKey = SecretKeySpec(
"This is a legacy backup key 1234".toByteArray(), 0, KEY_SIZE_BYTES, "AES" "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 keyManager = KeyManagerTestImpl(secretKey)
private val cipherFactory = CipherFactoryImpl(keyManager) private val cipherFactory = CipherFactoryImpl(keyManager)
private val headerReader = HeaderReaderImpl() 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 writer = MetadataWriterImpl(cryptoImpl)
private val reader = MetadataReaderImpl(cryptoImpl) private val reader = MetadataReaderImpl(cryptoImpl)

View file

@ -5,6 +5,7 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.metadata
import android.content.Context
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.CryptoImpl
import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES 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.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.toByteArrayFromHex import com.stevesoltys.seedvault.toByteArrayFromHex
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
@ -29,10 +31,11 @@ internal class MetadataV0ReadTest {
private val secretKey = SecretKeySpec( private val secretKey = SecretKeySpec(
"This is a legacy backup key 1234".toByteArray(), 0, KEY_SIZE_BYTES, "AES" "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 keyManager = KeyManagerTestImpl(secretKey)
private val cipherFactory = CipherFactoryImpl(keyManager) private val cipherFactory = CipherFactoryImpl(keyManager)
private val headerReader = HeaderReaderImpl() private val headerReader = HeaderReaderImpl()
private val cryptoImpl = CryptoImpl(keyManager, cipherFactory, headerReader) private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader)
private val reader = MetadataReaderImpl(cryptoImpl) 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
import android.app.backup.RestoreDescription.TYPE_FULL_STREAM import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
import android.os.ParcelFileDescriptor 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.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.CryptoImpl
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl 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.BackupType
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.metadata.PackageMetadata 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.BackupCoordinator
import com.stevesoltys.seedvault.transport.backup.FullBackup import com.stevesoltys.seedvault.transport.backup.FullBackup
import com.stevesoltys.seedvault.transport.backup.InputFactory import com.stevesoltys.seedvault.transport.backup.InputFactory
@ -59,7 +59,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val keyManager = KeyManagerTestImpl() private val keyManager = KeyManagerTestImpl()
private val cipherFactory = CipherFactoryImpl(keyManager) private val cipherFactory = CipherFactoryImpl(keyManager)
private val headerReader = HeaderReaderImpl() 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 metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val dbManager = TestKvDbManager() private val dbManager = TestKvDbManager()

View file

@ -150,7 +150,7 @@ internal class FullBackupTest : BackupTest() {
every { inputFactory.getInputStream(data) } returns inputStream every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream() expectInitializeOutputStream()
every { settingsManager.isQuotaUnlimited() } returns false 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() every { inputStream.read(any(), any(), bytes.size) } throws IOException()
expectClearState() expectClearState()
@ -204,7 +204,7 @@ internal class FullBackupTest : BackupTest() {
every { inputFactory.getInputStream(data) } returns inputStream every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream() expectInitializeOutputStream()
every { settingsManager.isQuotaUnlimited() } returns false 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 { inputStream.read(any(), any(), bytes.size) } returns bytes.size
every { encryptedOutputStream.write(any<ByteArray>()) } throws IOException() every { encryptedOutputStream.write(any<ByteArray>()) } throws IOException()
expectClearState() expectClearState()
@ -345,7 +345,7 @@ internal class FullBackupTest : BackupTest() {
private fun expectSendData(numBytes: Int, readBytes: Int = numBytes) { private fun expectSendData(numBytes: Int, readBytes: Int = numBytes) {
every { inputStream.read(any(), any(), numBytes) } returns readBytes 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 every { encryptedOutputStream.write(any<ByteArray>()) } just Runs
} }

View file

@ -233,7 +233,7 @@ internal class KVBackupTest : BackupTest() {
coEvery { backend.save(handle) } returns outputStream coEvery { backend.save(handle) } returns outputStream
every { outputStream.write(ByteArray(1) { VERSION }) } just Runs every { outputStream.write(ByteArray(1) { VERSION }) } just Runs
val ad = getADForKV(VERSION, packageInfo.packageName) 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() every { encryptedOutputStream.write(any<ByteArray>()) } throws IOException()
assertEquals(TRANSPORT_ERROR, backup.finishBackup()) assertEquals(TRANSPORT_ERROR, backup.finishBackup())
@ -304,7 +304,7 @@ internal class KVBackupTest : BackupTest() {
coEvery { backend.save(handle) } returns outputStream coEvery { backend.save(handle) } returns outputStream
every { outputStream.write(ByteArray(1) { VERSION }) } just Runs every { outputStream.write(ByteArray(1) { VERSION }) } just Runs
val ad = getADForKV(VERSION, packageInfo.packageName) 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<ByteArray>()) } just Runs // gzip header
every { encryptedOutputStream.write(any(), any(), any()) } just Runs // stream copy every { encryptedOutputStream.write(any(), any(), any()) } just Runs // stream copy
every { dbManager.getDbInputStream(packageName) } returns inputStream 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_ERROR
import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED 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.coAssertThrows
import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH 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.VERSION
import com.stevesoltys.seedvault.header.VersionHeader import com.stevesoltys.seedvault.header.VersionHeader
import com.stevesoltys.seedvault.header.getADForFull 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.CapturingSlot
import io.mockk.Runs import io.mockk.Runs
import io.mockk.coEvery import io.mockk.coEvery
@ -135,7 +135,7 @@ internal class FullRestoreTest : RestoreTest() {
coEvery { backend.load(handle) } returns inputStream coEvery { backend.load(handle) } returns inputStream
every { headerReader.readVersion(inputStream, VERSION) } returns VERSION 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 every { fileDescriptor.close() } just Runs
assertEquals( assertEquals(
@ -151,7 +151,9 @@ internal class FullRestoreTest : RestoreTest() {
coEvery { backend.load(handle) } returns inputStream coEvery { backend.load(handle) } returns inputStream
every { headerReader.readVersion(inputStream, VERSION) } returns VERSION 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 every { fileDescriptor.close() } just Runs
assertEquals(TRANSPORT_ERROR, restore.getNextFullRestoreDataChunk(fileDescriptor)) assertEquals(TRANSPORT_ERROR, restore.getNextFullRestoreDataChunk(fileDescriptor))
@ -217,7 +219,7 @@ internal class FullRestoreTest : RestoreTest() {
coEvery { backend.load(handle) } returns inputStream coEvery { backend.load(handle) } returns inputStream
every { headerReader.readVersion(inputStream, VERSION) } returns VERSION 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 { outputFactory.getOutputStream(fileDescriptor) } returns outputStream
every { fileDescriptor.close() } just Runs every { fileDescriptor.close() } just Runs
every { inputStream.close() } just Runs every { inputStream.close() } just Runs
@ -250,7 +252,7 @@ internal class FullRestoreTest : RestoreTest() {
private fun initInputStream() { private fun initInputStream() {
coEvery { backend.load(handle) } returns inputStream coEvery { backend.load(handle) } returns inputStream
every { headerReader.readVersion(inputStream, VERSION) } returns VERSION 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) { private fun readAndEncryptInputStream(encryptedBytes: ByteArray) {

View file

@ -105,7 +105,7 @@ internal class KVRestoreTest : RestoreTest() {
coEvery { backend.load(handle) } returns inputStream coEvery { backend.load(handle) } returns inputStream
every { headerReader.readVersion(inputStream, VERSION) } returns VERSION 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 every { dbManager.deleteDb(packageInfo.packageName, true) } returns true
streamsGetClosed() streamsGetClosed()
@ -123,7 +123,7 @@ internal class KVRestoreTest : RestoreTest() {
coEvery { backend.load(handle) } returns inputStream coEvery { backend.load(handle) } returns inputStream
every { headerReader.readVersion(inputStream, VERSION) } returns VERSION every { headerReader.readVersion(inputStream, VERSION) } returns VERSION
every { crypto.newDecryptingStream(inputStream, ad) } returns decryptInputStream every { crypto.newDecryptingStreamV1(inputStream, ad) } returns decryptInputStream
every { every {
dbManager.getDbOutputStream(packageInfo.packageName) dbManager.getDbOutputStream(packageInfo.packageName)
} returns ByteArrayOutputStream() } returns ByteArrayOutputStream()
@ -148,7 +148,7 @@ internal class KVRestoreTest : RestoreTest() {
coEvery { backend.load(handle) } returns inputStream coEvery { backend.load(handle) } returns inputStream
every { headerReader.readVersion(inputStream, VERSION) } returns VERSION every { headerReader.readVersion(inputStream, VERSION) } returns VERSION
every { crypto.newDecryptingStream(inputStream, ad) } returns decryptInputStream every { crypto.newDecryptingStreamV1(inputStream, ad) } returns decryptInputStream
every { every {
dbManager.getDbOutputStream(packageInfo.packageName) dbManager.getDbOutputStream(packageInfo.packageName)
} returns ByteArrayOutputStream() } 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
import android.app.backup.RestoreDescription.TYPE_FULL_STREAM import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
import android.os.ParcelFileDescriptor 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.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.CryptoImpl
import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES 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.encodeBase64
import com.stevesoltys.seedvault.header.HeaderReaderImpl import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl 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.toByteArrayFromHex
import com.stevesoltys.seedvault.transport.TransportTest import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.backup.KvDbManager import com.stevesoltys.seedvault.transport.backup.KvDbManager
@ -50,7 +50,7 @@ internal class RestoreV0IntegrationTest : TransportTest() {
private val keyManager = KeyManagerTestImpl(secretKey) private val keyManager = KeyManagerTestImpl(secretKey)
private val cipherFactory = CipherFactoryImpl(keyManager) private val cipherFactory = CipherFactoryImpl(keyManager)
private val headerReader = HeaderReaderImpl() 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 dbManager = mockk<KvDbManager>()
private val metadataReader = MetadataReaderImpl(cryptoImpl) private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()

View file

@ -14,6 +14,7 @@ android_library {
"src/main/java/org/calyxos/seedvault/core/backends/BackendTest.kt", "src/main/java/org/calyxos/seedvault/core/backends/BackendTest.kt",
], ],
static_libs: [ static_libs: [
"seedvault-lib-tink-android",
"androidx.core_core-ktx", "androidx.core_core-ktx",
"androidx.documentfile_documentfile", "androidx.documentfile_documentfile",
"kotlinx-coroutines-android", "kotlinx-coroutines-android",

View file

@ -41,6 +41,7 @@ dependencies {
implementation(libs.bundles.coroutines) implementation(libs.bundles.coroutines)
implementation(libs.androidx.documentfile) implementation(libs.androidx.documentfile)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.google.tink.android)
implementation(fileTree("${rootProject.rootDir}/libs/dav4jvm").include("*.jar")) implementation(fileTree("${rootProject.rootDir}/libs/dav4jvm").include("*.jar"))
implementation(libs.squareup.okio) implementation(libs.squareup.okio)
implementation(libs.kotlin.logging) 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 * 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.nio.ByteBuffer
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
import javax.crypto.Mac import javax.crypto.Mac
@ -14,19 +15,17 @@ import kotlin.math.min
internal object Hkdf { 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. * Step 2 of RFC 5869.
* *
* Based on the Apache2 licensed HKDF library by Patrick Favre-Bulle. * Based on the Apache2 licensed HKDF library by Patrick Favre-Bulle.
* Link: https://github.com/patrickfav/hkdf * 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 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) * @return new byte array of output keying material (OKM)
*/ */
@Throws(GeneralSecurityException::class) @Throws(GeneralSecurityException::class)

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0 * 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.Assert.assertArrayEquals
import org.junit.Test import org.junit.Test

View file

@ -20,7 +20,6 @@ android_library {
}, },
static_libs: [ static_libs: [
"seedvault-lib-core", "seedvault-lib-core",
"seedvault-lib-tink-android",
"libprotobuf-java-lite", "libprotobuf-java-lite",
"androidx.core_core-ktx", "androidx.core_core-ktx",
"androidx.fragment_fragment-ktx", "androidx.fragment_fragment-ktx",

View file

@ -93,7 +93,6 @@ dependencies {
implementation(libs.google.material) implementation(libs.google.material)
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
implementation(libs.google.protobuf.javalite) implementation(libs.google.protobuf.javalite)
implementation(libs.google.tink.android)
ksp(group = "androidx.room", name = "room-compiler", version = libs.versions.room.get()) ksp(group = "androidx.room", name = "room-compiler", version = libs.versions.room.get())
lintChecks(libs.thirdegg.lint.rules) lintChecks(libs.thirdegg.lint.rules)

View file

@ -6,8 +6,9 @@
package org.calyxos.backup.storage.crypto package org.calyxos.backup.storage.crypto
import org.calyxos.backup.storage.backup.Chunker import org.calyxos.backup.storage.backup.Chunker
import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC import org.calyxos.seedvault.core.crypto.CoreCrypto
import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES 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.GeneralSecurityException
import java.security.KeyStore import java.security.KeyStore
import javax.crypto.Mac import javax.crypto.Mac
@ -25,12 +26,11 @@ internal object ChunkCrypto {
*/ */
@Throws(GeneralSecurityException::class) @Throws(GeneralSecurityException::class)
fun deriveChunkIdKey( fun deriveChunkIdKey(
masterKey: SecretKey, mainKey: SecretKey,
info: ByteArray = INFO_CHUNK_ID.toByteArray(), info: ByteArray = INFO_CHUNK_ID.toByteArray(),
): ByteArray = Hkdf.expand( ): ByteArray = CoreCrypto.deriveKey(
secretKey = masterKey, mainKey = mainKey,
info = info, info = info,
outLengthBytes = KEY_SIZE_BYTES
) )
/** /**

View file

@ -5,10 +5,9 @@
package org.calyxos.backup.storage.crypto 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.backup.Backup.Companion.VERSION
import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC import org.calyxos.seedvault.core.crypto.CoreCrypto
import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES import org.calyxos.seedvault.core.crypto.CoreCrypto.KEY_SIZE_BYTES
import org.calyxos.seedvault.core.toByteArrayFromHex import org.calyxos.seedvault.core.toByteArrayFromHex
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -20,18 +19,16 @@ import javax.crypto.SecretKey
public object StreamCrypto { public object StreamCrypto {
private const val INFO_STREAM_KEY = "stream key" 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_CHUNK: Byte = 0x00
private const val TYPE_SNAPSHOT: Byte = 0x01 private const val TYPE_SNAPSHOT: Byte = 0x01
@Throws(GeneralSecurityException::class) @Throws(GeneralSecurityException::class)
public fun deriveStreamKey( public fun deriveStreamKey(
masterKey: SecretKey, mainKey: SecretKey,
info: ByteArray = INFO_STREAM_KEY.toByteArray(), info: ByteArray = INFO_STREAM_KEY.toByteArray(),
): ByteArray = Hkdf.expand( ): ByteArray = CoreCrypto.deriveKey(
secretKey = masterKey, mainKey = mainKey,
info = info, info = info,
outLengthBytes = KEY_SIZE_BYTES
) )
internal fun getAssociatedDataForChunk(chunkId: String, version: Byte = VERSION): ByteArray = internal fun getAssociatedDataForChunk(chunkId: String, version: Byte = VERSION): ByteArray =
@ -48,39 +45,19 @@ public object StreamCrypto {
.put(timestamp.toByteArray()) .put(timestamp.toByteArray())
.array() .array()
/**
* Returns a [AesGcmHkdfStreaming] encrypting stream
* that gets encrypted with the given secret.
*/
@Throws(IOException::class, GeneralSecurityException::class) @Throws(IOException::class, GeneralSecurityException::class)
public fun newEncryptingStream( public fun newEncryptingStream(
secret: ByteArray, secret: ByteArray,
outputStream: OutputStream, outputStream: OutputStream,
associatedData: ByteArray = ByteArray(0), associatedData: ByteArray = ByteArray(0),
): OutputStream { ): OutputStream = CoreCrypto.newEncryptingStream(secret, outputStream, associatedData)
return AesGcmHkdfStreaming(
secret,
ALGORITHM_HMAC,
KEY_SIZE_BYTES,
SIZE_SEGMENT,
0
).newEncryptingStream(outputStream, associatedData)
}
@Throws(IOException::class, GeneralSecurityException::class) @Throws(IOException::class, GeneralSecurityException::class)
public fun newDecryptingStream( public fun newDecryptingStream(
secret: ByteArray, secret: ByteArray,
inputStream: InputStream, inputStream: InputStream,
associatedData: ByteArray = ByteArray(0), associatedData: ByteArray = ByteArray(0),
): InputStream { ): InputStream = CoreCrypto.newDecryptingStream(secret, inputStream, associatedData)
return AesGcmHkdfStreaming(
secret,
ALGORITHM_HMAC,
KEY_SIZE_BYTES,
SIZE_SEGMENT,
0
).newDecryptingStream(inputStream, associatedData)
}
public fun Long.toByteArray(): ByteArray = ByteArray(8).apply { public fun Long.toByteArray(): ByteArray = ByteArray(8).apply {
var l = this@toByteArray 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.ContentFile
import org.calyxos.backup.storage.content.DocFile import org.calyxos.backup.storage.content.DocFile
import org.calyxos.backup.storage.content.MediaFile 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.CachedChunk
import org.calyxos.backup.storage.db.CachedFile import org.calyxos.backup.storage.db.CachedFile
import org.calyxos.backup.storage.db.ChunksCache 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.Backend
import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob
import org.calyxos.seedvault.core.backends.FileBackupFileType.Snapshot 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.calyxos.seedvault.core.crypto.KeyManager
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals

View file

@ -13,13 +13,13 @@ import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.calyxos.backup.storage.backup.Backup.Companion.VERSION 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.crypto.StreamCrypto
import org.calyxos.backup.storage.db.ChunksCache import org.calyxos.backup.storage.db.ChunksCache
import org.calyxos.backup.storage.getRandomString import org.calyxos.backup.storage.getRandomString
import org.calyxos.backup.storage.mockLog import org.calyxos.backup.storage.mockLog
import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob 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.calyxos.seedvault.core.toHexString
import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals

View file

@ -6,7 +6,7 @@
package org.calyxos.backup.storage.backup package org.calyxos.backup.storage.backup
import org.calyxos.backup.storage.crypto.ChunkCrypto 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.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream

View file

@ -14,7 +14,6 @@ import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.calyxos.backup.storage.api.BackupObserver 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.crypto.StreamCrypto
import org.calyxos.backup.storage.db.CachedChunk import org.calyxos.backup.storage.db.CachedChunk
import org.calyxos.backup.storage.db.ChunksCache 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.getRandomString
import org.calyxos.backup.storage.mockLog import org.calyxos.backup.storage.mockLog
import org.calyxos.seedvault.core.backends.Backend 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.calyxos.seedvault.core.toHexString
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test

View file

@ -13,22 +13,22 @@ import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.slot import io.mockk.slot
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.calyxos.backup.storage.SnapshotRetriever
import org.calyxos.backup.storage.api.StoredSnapshot import org.calyxos.backup.storage.api.StoredSnapshot
import org.calyxos.backup.storage.backup.BackupDocumentFile import org.calyxos.backup.storage.backup.BackupDocumentFile
import org.calyxos.backup.storage.backup.BackupMediaFile import org.calyxos.backup.storage.backup.BackupMediaFile
import org.calyxos.backup.storage.backup.BackupSnapshot 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.crypto.StreamCrypto
import org.calyxos.backup.storage.db.CachedChunk import org.calyxos.backup.storage.db.CachedChunk
import org.calyxos.backup.storage.db.ChunksCache import org.calyxos.backup.storage.db.ChunksCache
import org.calyxos.backup.storage.db.Db import org.calyxos.backup.storage.db.Db
import org.calyxos.backup.storage.getCurrentBackupSnapshots
import org.calyxos.backup.storage.getRandomString import org.calyxos.backup.storage.getRandomString
import org.calyxos.backup.storage.mockLog import org.calyxos.backup.storage.mockLog
import org.calyxos.backup.storage.SnapshotRetriever
import org.calyxos.backup.storage.getCurrentBackupSnapshots
import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob 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.calyxos.seedvault.core.crypto.KeyManager
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -48,15 +48,15 @@ internal class PrunerTest {
private val retentionManager: RetentionManager = mockk() private val retentionManager: RetentionManager = mockk()
private val streamCrypto: StreamCrypto = mockk() private val streamCrypto: StreamCrypto = mockk()
private val streamKey = "This is a backup key for testing".toByteArray() 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 { init {
mockLog(false) mockLog(false)
mockkStatic("org.calyxos.backup.storage.SnapshotRetrieverKt") mockkStatic("org.calyxos.backup.storage.SnapshotRetrieverKt")
every { backendGetter() } returns backend every { backendGetter() } returns backend
every { db.getChunksCache() } returns chunksCache every { db.getChunksCache() } returns chunksCache
every { keyManager.getMainKey() } returns masterKey every { keyManager.getMainKey() } returns mainKey
every { streamCrypto.deriveStreamKey(masterKey) } returns streamKey every { streamCrypto.deriveStreamKey(mainKey) } returns streamKey
} }
private val pruner = Pruner( private val pruner = Pruner(