mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-01-27 16:05:32 +00:00
Implement Secrets Repository
Add proto plugin and dependencies Create Secrets Proto Create Secrets Proto DataStore Replace old secrets with proto secrets Fix importers and tests to use new proto secrets.
This commit is contained in:
parent
9871f0420f
commit
c456d455ef
@ -3,6 +3,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
id("com.google.devtools.ksp") version "2.0.21-1.0.27"
|
||||
id("com.google.protobuf")
|
||||
}
|
||||
|
||||
android {
|
||||
@ -38,6 +39,29 @@ android {
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
lint {
|
||||
baseline = file("lint-baseline.xml")
|
||||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = "com.google.protobuf:protoc:4.27.0"
|
||||
}
|
||||
|
||||
// Generates the java Protobuf-lite code for the Protobufs in this project. See
|
||||
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
|
||||
// for more information.
|
||||
generateProtoTasks {
|
||||
all().forEach { task ->
|
||||
task.builtins {
|
||||
create("java") {
|
||||
option("lite")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -51,6 +75,7 @@ dependencies {
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
@ -64,5 +89,6 @@ dependencies {
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
implementation("androidx.navigation:navigation-compose:2.7.0")
|
||||
implementation("com.google.android.material:material:1.2.0")
|
||||
implementation(libs.protobuf.javalite)
|
||||
implementation("androidx.compose.material:material")
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
package com.github.nacabaro.vbhelper.di
|
||||
|
||||
import com.github.nacabaro.vbhelper.database.AppDatabase
|
||||
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
|
||||
|
||||
interface AppContainer {
|
||||
val db: AppDatabase
|
||||
val dataStoreSecretsRepository: DataStoreSecretsRepository
|
||||
}
|
||||
@ -1,9 +1,22 @@
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.dataStore
|
||||
import androidx.room.Room
|
||||
import com.github.nacabaro.vbhelper.database.AppDatabase
|
||||
import com.github.nacabaro.vbhelper.di.AppContainer
|
||||
import com.github.nacabaro.vbhelper.source.DataStoreSecretsRepository
|
||||
import com.github.nacabaro.vbhelper.source.SecretsSerializer
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
|
||||
private const val SECRETS_DATA_STORE_NAME = "secrets.pb"
|
||||
|
||||
val Context.secretsStore: DataStore<Secrets> by dataStore(
|
||||
fileName = SECRETS_DATA_STORE_NAME,
|
||||
serializer = SecretsSerializer
|
||||
)
|
||||
|
||||
class DefaultAppContainer(private val context: Context) : AppContainer {
|
||||
|
||||
override val db: AppDatabase by lazy {
|
||||
Room.databaseBuilder(
|
||||
context = context,
|
||||
@ -11,4 +24,8 @@ class DefaultAppContainer(private val context: Context) : AppContainer {
|
||||
"internalDb"
|
||||
).build()
|
||||
}
|
||||
|
||||
override val dataStoreSecretsRepository = DataStoreSecretsRepository(context.secretsStore)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
@ -10,7 +11,7 @@ class ApkSecretsImporter(private val dexFileSecretsImporter: SecretsImporter = D
|
||||
}
|
||||
|
||||
// importSecrets imports the secrets from the apk input stream, and validates them.
|
||||
override fun importSecrets(inputStream: InputStream): Map<UShort, Secrets> {
|
||||
override fun importSecrets(inputStream: InputStream): Secrets {
|
||||
ZipInputStream(inputStream).use { zip ->
|
||||
var zipEntry = zip.nextEntry
|
||||
while(zipEntry != null) {
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.github.cfogrady.vbnfc.CryptographicTransformer
|
||||
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets.HmacKeys
|
||||
import kotlinx.coroutines.flow.single
|
||||
|
||||
class DataStoreSecretsRepository(
|
||||
private val secretsDataStore: DataStore<Secrets>,
|
||||
): SecretsRepository {
|
||||
override val secretsFlow = secretsDataStore.data
|
||||
|
||||
override suspend fun updateSecrets(secrets: Secrets) {
|
||||
secretsDataStore.updateData {
|
||||
secrets
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSecrets(): Secrets {
|
||||
return secretsFlow.single()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Secrets.getHmacKeys(deviceTypeId: UShort): HmacKeys {
|
||||
return when(deviceTypeId) {
|
||||
DeviceType.VitalBraceletBEDeviceType -> this.beHmacKeys
|
||||
DeviceType.VitalCharactersDeviceType -> this.vbcHmacKeys
|
||||
DeviceType.VitalSeriesDeviceType -> this.vbdmHmacKeys
|
||||
else -> throw IllegalArgumentException("Unknown DeviceTypeId")
|
||||
}
|
||||
}
|
||||
|
||||
fun Secrets.getCryptographicTransformerMap(): Map<UShort, CryptographicTransformer> {
|
||||
val cipher = this.vbCipherList.toIntArray()
|
||||
val beCipher = this.beCipherList.toIntArray()
|
||||
val vbdmHmacKeys = this.getHmacKeys(DeviceType.VitalSeriesDeviceType)
|
||||
val vbcHmacKeys = this.getHmacKeys(DeviceType.VitalCharactersDeviceType)
|
||||
val beHmacKeys = this.getHmacKeys(DeviceType.VitalBraceletBEDeviceType)
|
||||
return mapOf(
|
||||
Pair(DeviceType.VitalSeriesDeviceType, CryptographicTransformer(vbdmHmacKeys.hmacKey1, vbdmHmacKeys.hmacKey2, this.aesKey, cipher)),
|
||||
Pair(DeviceType.VitalCharactersDeviceType, CryptographicTransformer(vbcHmacKeys.hmacKey1, vbcHmacKeys.hmacKey2, this.aesKey, cipher)),
|
||||
Pair(DeviceType.VitalBraceletBEDeviceType, CryptographicTransformer(beHmacKeys.hmacKey1, beHmacKeys.hmacKey2, this.aesKey, beCipher)),
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import com.github.cfogrady.vbnfc.CryptographicTransformer
|
||||
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets.HmacKeys
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
@ -28,63 +31,58 @@ class DexFileSecretsImporter: SecretsImporter {
|
||||
const val VBC_TEST_TAG_PASSWORD = "a71dfb22"
|
||||
}
|
||||
|
||||
override fun importSecrets(inputStream: InputStream): Map<UShort, Secrets> {
|
||||
override fun importSecrets(inputStream: InputStream): Secrets {
|
||||
val deviceToSecrets = readSecrets(inputStream)
|
||||
verifySecretCorrectness(deviceToSecrets)
|
||||
return deviceToSecrets
|
||||
}
|
||||
|
||||
private fun readSecrets(inputStream: InputStream): Map<UShort, Secrets> {
|
||||
private fun readSecrets(inputStream: InputStream): Secrets {
|
||||
val dexFile = inputStream.readBytes()
|
||||
val byteOrder = ByteOrder.BIG_ENDIAN
|
||||
val vbdmSubstitutionCipher = dexFile.sliceArray(VBDM_SUBSTITUTION_CIPHER_IDX until VBDM_SUBSTITUTION_CIPHER_IDX+(16*4)).toIntArray(byteOrder)
|
||||
val beSubstitutionCipher = dexFile.sliceArray(BE_SUBSTITUTION_CIPHER_IDX until BE_SUBSTITUTION_CIPHER_IDX+(16*4)).toIntArray(byteOrder)
|
||||
val aesKey = dexFile.sliceArray(AES_KEY_IDX until AES_KEY_IDX+24).toString(StandardCharsets.UTF_8)
|
||||
val secretsByDevices = mapOf(
|
||||
Pair(DeviceType.VitalSeriesDeviceType, buildSecrets(dexFile, VBDM_HMAC_KEY_1_IDX, VBDM_HMAC_KEY_2_IDX, aesKey, vbdmSubstitutionCipher)),
|
||||
Pair(DeviceType.VitalBraceletBEDeviceType, buildSecrets(dexFile, BE_HMAC_KEY_1_IDX, BE_HMAC_KEY_2_IDX, aesKey, beSubstitutionCipher)),
|
||||
Pair(DeviceType.VitalCharactersDeviceType, buildSecrets(dexFile, VBC_HMAC_KEY_1_IDX, VBC_HMAC_KEY_2_IDX, aesKey, vbdmSubstitutionCipher)),
|
||||
)
|
||||
return secretsByDevices
|
||||
return Secrets.newBuilder()
|
||||
.setAesKey(aesKey)
|
||||
.addAllVbCipher(vbdmSubstitutionCipher.toList())
|
||||
.addAllBeCipher(beSubstitutionCipher.toList())
|
||||
.setVbdmHmacKeys(getHmac(dexFile, VBDM_HMAC_KEY_1_IDX, VBDM_HMAC_KEY_2_IDX))
|
||||
.setVbcHmacKeys(getHmac(dexFile, VBC_HMAC_KEY_1_IDX, VBC_HMAC_KEY_2_IDX))
|
||||
.setBeHmacKeys(getHmac(dexFile, BE_HMAC_KEY_1_IDX, BE_HMAC_KEY_2_IDX))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildSecrets(dexFile: ByteArray, hmacKeyIdx1: Int, hmacKeyIdx2: Int, aesKey: String, substitutionCipher: IntArray): Secrets {
|
||||
private fun getHmac(dexFile: ByteArray, hmacKeyIdx1: Int, hmacKeyIdx2: Int): HmacKeys {
|
||||
val hmacKey1 = dexFile.sliceArray(hmacKeyIdx1 until hmacKeyIdx1+24).toString(StandardCharsets.UTF_8)
|
||||
val hmacKey2 = dexFile.sliceArray(hmacKeyIdx2 until hmacKeyIdx2+24).toString(StandardCharsets.UTF_8)
|
||||
return Secrets(hmacKey1, hmacKey2, aesKey, substitutionCipher)
|
||||
return HmacKeys.newBuilder()
|
||||
.setHmacKey1(hmacKey1)
|
||||
.setHmacKey2(hmacKey2)
|
||||
.build()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun verifySecretCorrectness(deviceToSecrets: Map<UShort, Secrets>) {
|
||||
for (keyValue in deviceToSecrets) {
|
||||
private fun verifySecretCorrectness(secrets: Secrets) {
|
||||
val deviceToCryptographicTransformers = secrets.getCryptographicTransformerMap()
|
||||
for (keyValue in deviceToCryptographicTransformers) {
|
||||
when(keyValue.key) {
|
||||
DeviceType.VitalBraceletBEDeviceType -> {
|
||||
val result = keyValue.value.toCryptographicTransformer().createNfcPassword(
|
||||
TEST_TAG
|
||||
)
|
||||
if( result.toHexString() != BE_TEST_TAG_PASSWORD) {
|
||||
throw InvalidKeyException("Secrets were loaded, but were unsuccessful at generating the test password: ${result.toHexString()}")
|
||||
}
|
||||
}
|
||||
DeviceType.VitalCharactersDeviceType -> {
|
||||
val result = keyValue.value.toCryptographicTransformer().createNfcPassword(
|
||||
TEST_TAG
|
||||
)
|
||||
if( result.toHexString() != VBC_TEST_TAG_PASSWORD) {
|
||||
throw InvalidKeyException("Secrets were loaded, but were unsuccessful at generating the test password: ${result.toHexString()}")
|
||||
}
|
||||
}
|
||||
DeviceType.VitalSeriesDeviceType -> {
|
||||
val result = keyValue.value.toCryptographicTransformer().createNfcPassword(
|
||||
TEST_TAG
|
||||
)
|
||||
if( result.toHexString() != VBDM_TEST_TAG_PASSWORD) {
|
||||
throw InvalidKeyException("Secrets were loaded, but were unsuccessful at generating the test password: ${result.toHexString()}")
|
||||
}
|
||||
}
|
||||
DeviceType.VitalBraceletBEDeviceType -> assertBuildsCorrectPassword(keyValue.value, BE_TEST_TAG_PASSWORD)
|
||||
DeviceType.VitalCharactersDeviceType -> assertBuildsCorrectPassword(keyValue.value, VBC_TEST_TAG_PASSWORD)
|
||||
DeviceType.VitalSeriesDeviceType -> assertBuildsCorrectPassword(keyValue.value, VBDM_TEST_TAG_PASSWORD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun assertBuildsCorrectPassword(cryptographicTransformer: CryptographicTransformer, expectedPassword: String) {
|
||||
val result = cryptographicTransformer.createNfcPassword(
|
||||
TEST_TAG
|
||||
)
|
||||
if( result.toHexString() != expectedPassword) {
|
||||
throw InvalidKeyException("Secrets were loaded, but were unsuccessful at generating the test password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toIntArray(byteOrder: ByteOrder): IntArray {
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import com.github.cfogrady.vbnfc.CryptographicTransformer
|
||||
|
||||
data class Secrets(val hmacKey1: String, val hmacKey2: String, val aesKey: String, val substitutionCipher: IntArray) {
|
||||
fun toCryptographicTransformer(): CryptographicTransformer {
|
||||
return CryptographicTransformer(hmacKey1, hmacKey2, aesKey, substitutionCipher)
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import java.io.InputStream
|
||||
|
||||
fun interface SecretsImporter {
|
||||
fun importSecrets(inputStream: InputStream): Map<UShort, Secrets>
|
||||
fun importSecrets(inputStream: InputStream): Secrets
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SecretsRepository {
|
||||
val secretsFlow: Flow<Secrets>
|
||||
|
||||
suspend fun getSecrets(): Secrets
|
||||
suspend fun updateSecrets(secrets: Secrets)
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
object SecretsSerializer: Serializer<Secrets> {
|
||||
override val defaultValue = Secrets.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): Secrets {
|
||||
try {
|
||||
return Secrets.parseFrom(input)
|
||||
} catch (exception: InvalidProtocolBufferException) {
|
||||
throw CorruptionException("Cannot read proto.", exception)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: Secrets, output: OutputStream) {
|
||||
t.writeTo(output)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "com.github.nacabaro.vbhelper.source.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Secrets {
|
||||
string aes_key = 1;
|
||||
repeated int32 vb_cipher = 2;
|
||||
repeated int32 be_cipher = 3;
|
||||
|
||||
message HmacKeys {
|
||||
string hmac_key_1 = 1;
|
||||
string hmac_key_2 = 2;
|
||||
}
|
||||
HmacKeys vbdm_hmac_keys = 4;
|
||||
HmacKeys vbc_hmac_keys = 5;
|
||||
HmacKeys be_hmac_keys = 6;
|
||||
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
@ -14,15 +14,18 @@ import java.util.zip.ZipOutputStream
|
||||
class ApkSecretsImporterTest {
|
||||
|
||||
@Test
|
||||
fun testThatRealImportSecretsHasAllDeviceTypes() {
|
||||
fun testThatRealImportSecretsPasses() {
|
||||
val apkFileSecretsImporter = ApkSecretsImporter()
|
||||
val url = getAndAssertApkFile()
|
||||
val file = File(url.path)
|
||||
file.inputStream().use {
|
||||
val deviceIdToSecrets = apkFileSecretsImporter.importSecrets(it)
|
||||
Assert.assertNotNull("BE Device Type", deviceIdToSecrets[DeviceType.VitalBraceletBEDeviceType])
|
||||
Assert.assertNotNull("VBDM Device Type", deviceIdToSecrets[DeviceType.VitalSeriesDeviceType])
|
||||
Assert.assertNotNull("VBC Device Type", deviceIdToSecrets[DeviceType.VitalCharactersDeviceType])
|
||||
val secrets = apkFileSecretsImporter.importSecrets(it)
|
||||
Assert.assertEquals("Cipher size isn't correct", 16, secrets.vbCipherCount)
|
||||
Assert.assertEquals("BE Cipher size isn't correct", 16, secrets.beCipherCount)
|
||||
Assert.assertFalse("AES Key is empty", secrets.aesKey.isEmpty())
|
||||
assertHmacKeysArePopulated("VBDM", secrets.vbdmHmacKeys)
|
||||
assertHmacKeysArePopulated("VBC", secrets.vbcHmacKeys)
|
||||
assertHmacKeysArePopulated("BE", secrets.beHmacKeys)
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +37,7 @@ class ApkSecretsImporterTest {
|
||||
val inputStreamContents = it.readAllBytes()
|
||||
Assert.assertTrue("Unexpected file contents received by DexSecretsImporter", inputStreamContents.contentEquals(expectedDexContents))
|
||||
foundFile = true
|
||||
emptyMap()
|
||||
Secrets.newBuilder().build()
|
||||
}
|
||||
val apkBytes = constructTestApk(expectedDexContents)
|
||||
ByteArrayInputStream(apkBytes).use {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.github.nacabaro.vbhelper.source
|
||||
|
||||
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||
import com.github.nacabaro.vbhelper.source.proto.Secrets.HmacKeys
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
@ -10,18 +10,26 @@ import java.nio.ByteOrder
|
||||
import java.security.InvalidKeyException
|
||||
|
||||
|
||||
fun assertHmacKeysArePopulated(msg: String, hmacKeys: HmacKeys) {
|
||||
Assert.assertFalse("$msg hmacKey1 is empty", hmacKeys.hmacKey1.isEmpty())
|
||||
Assert.assertFalse("$msg hmacKey2 is empty", hmacKeys.hmacKey2.isEmpty())
|
||||
}
|
||||
|
||||
class DexFileSecretsImporterTest {
|
||||
|
||||
@Test
|
||||
fun testThatImportSecretsHasAllDeviceTypes() {
|
||||
fun testThatImportSecretsIsPopulated() {
|
||||
val dexFileSecretsImporter = DexFileSecretsImporter()
|
||||
val url = getAndAssertClassesDexFile()
|
||||
val file = File(url.path)
|
||||
file.inputStream().use {
|
||||
val deviceIdToSecrets = dexFileSecretsImporter.importSecrets(it)
|
||||
Assert.assertNotNull("BE Device Type", deviceIdToSecrets[DeviceType.VitalBraceletBEDeviceType])
|
||||
Assert.assertNotNull("VBDM Device Type", deviceIdToSecrets[DeviceType.VitalSeriesDeviceType])
|
||||
Assert.assertNotNull("VBC Device Type", deviceIdToSecrets[DeviceType.VitalCharactersDeviceType])
|
||||
val secrets = dexFileSecretsImporter.importSecrets(it)
|
||||
Assert.assertEquals("Cipher size isn't correct", 16, secrets.vbCipherCount)
|
||||
Assert.assertEquals("BE Cipher size isn't correct", 16, secrets.beCipherCount)
|
||||
Assert.assertFalse("AES Key is empty", secrets.aesKey.isEmpty())
|
||||
assertHmacKeysArePopulated("VBDM", secrets.vbdmHmacKeys)
|
||||
assertHmacKeysArePopulated("VBC", secrets.vbcHmacKeys)
|
||||
assertHmacKeysArePopulated("BE", secrets.beHmacKeys)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,3 +4,9 @@ plugins {
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(libs.protobuf.gradle.plugin)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
[versions]
|
||||
agp = "8.7.3"
|
||||
datastore = "1.1.1"
|
||||
kotlin = "2.0.0"
|
||||
coreKtx = "1.15.0"
|
||||
junit = "4.13.2"
|
||||
@ -8,12 +9,15 @@ espressoCore = "3.6.1"
|
||||
lifecycleRuntimeKtx = "2.8.7"
|
||||
activityCompose = "1.9.3"
|
||||
composeBom = "2024.04.01"
|
||||
protobufGradlePlugin = "0.9.4"
|
||||
protobufJavalite = "4.27.0"
|
||||
roomRuntime = "2.6.1"
|
||||
vbNfcReader = "0.1.0"
|
||||
dimReader = "2.1.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
|
||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
@ -29,6 +33,8 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
protobuf-gradle-plugin = { module = "com.google.protobuf:protobuf-gradle-plugin", version.ref = "protobufGradlePlugin" }
|
||||
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobufJavalite" }
|
||||
vb-nfc-reader = { module = "com.github.cfogrady:vb-nfc-reader", version.ref = "vbNfcReader" }
|
||||
dim-reader = { module = "com.github.cfogrady:vb-dim-reader", version.ref = "dimReader" }
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user