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:
parent
c19787a7fa
commit
e6905c0365
35 changed files with 276 additions and 114 deletions
|
@ -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)
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`() {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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>()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue