Skip to content

Commit

Permalink
Move tink library into core module and expose via CoreCrypto
Browse files Browse the repository at this point in the history
This also moves key derivation via HKDF into the core.
  • Loading branch information
grote committed Sep 4, 2024
1 parent 77a5b46 commit e49859b
Show file tree
Hide file tree
Showing 35 changed files with 271 additions and 110 deletions.
115 changes: 99 additions & 16 deletions app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -47,20 +57,59 @@ internal interface Crypto {
*/
fun getRandomBytes(size: Int): ByteArray


/**
* Returns the ID of the backup repository as a 64 char hex string.
*/
val repoId: String

/**
* A secret key of size [KEY_SIZE_BYTES]
* only used to create a gear table specific to each main key.
*/
val gearTableKey: ByteArray

fun sha256(bytes: ByteArray): ByteArray

/**
* Returns a [AesGcmHkdfStreaming] encrypting stream
* that gets encrypted and authenticated the given associated data.
*/
@Throws(IOException::class, GeneralSecurityException::class)
fun newEncryptingStream(
outputStream: OutputStream,
associatedData: ByteArray,
): OutputStream

/**
* Returns a [AesGcmHkdfStreaming] decrypting stream
* that gets decrypted and authenticated the given associated data.
*/
@Throws(IOException::class, GeneralSecurityException::class)
fun newDecryptingStream(
inputStream: InputStream,
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 newEncryptingStream(
fun newEncryptingStreamV1(
outputStream: OutputStream,
associatedData: ByteArray,
): OutputStream
Expand All @@ -69,8 +118,9 @@ internal interface Crypto {
* Returns a [AesGcmHkdfStreaming] decrypting stream
* that gets decrypted and authenticated the given associated data.
*/
@Deprecated("only for v1")
@Throws(IOException::class, GeneralSecurityException::class)
fun newDecryptingStream(
fun newDecryptingStreamV1(
inputStream: InputStream,
associatedData: ByteArray,
): InputStream
Expand Down Expand Up @@ -123,29 +173,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) {
Expand All @@ -155,21 +240,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -20,5 +21,5 @@ val cryptoModule = module {
}
KeyManagerImpl(keyStore)
}
single<Crypto> { CryptoImpl(get(), get(), get()) }
single<Crypto> { CryptoImpl(androidContext(), get(), get(), get()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>()
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions app/src/test/java/com/stevesoltys/seedvault/TestApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`() {
Expand Down
Loading

0 comments on commit e49859b

Please sign in to comment.