M app/build.gradle.kts => app/build.gradle.kts +26 -0
@@ 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")
}=
\ No newline at end of file
M app/src/main/java/com/github/nacabaro/vbhelper/di/AppContainer.kt => app/src/main/java/com/github/nacabaro/vbhelper/di/AppContainer.kt +2 -0
@@ 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
}=
\ No newline at end of file
M app/src/main/java/com/github/nacabaro/vbhelper/di/DefaultAppContainer.kt => app/src/main/java/com/github/nacabaro/vbhelper/di/DefaultAppContainer.kt +18 -1
@@ 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()
}
-}>
\ No newline at end of file
+
+ override val dataStoreSecretsRepository = DataStoreSecretsRepository(context.secretsStore)
+
+}
+
M app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt => app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt +2 -1
@@ 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) {
A app/src/main/java/com/github/nacabaro/vbhelper/source/DataStoreSecretsRepository.kt => app/src/main/java/com/github/nacabaro/vbhelper/source/DataStoreSecretsRepository.kt +46 -0
@@ 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)),
+ )
+}<
\ No newline at end of file
M app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt => app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt +34 -36
@@ 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 {
D app/src/main/java/com/github/nacabaro/vbhelper/source/Secrets.kt => app/src/main/java/com/github/nacabaro/vbhelper/source/Secrets.kt +0 -9
@@ 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)
- }
-}
M app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsImporter.kt => app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsImporter.kt +2 -1
@@ 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
}=
\ No newline at end of file
A app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsRepository.kt => app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsRepository.kt +11 -0
@@ 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)
+}<
\ No newline at end of file
A app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsSerializer.kt => app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsSerializer.kt +24 -0
@@ 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)
+ }
+}<
\ No newline at end of file
A app/src/main/proto/com/github/nacabaro/vbhelper/source/Secrets.proto => app/src/main/proto/com/github/nacabaro/vbhelper/source/Secrets.proto +19 -0
@@ 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;
+
+}<
\ No newline at end of file
M app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt => app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt +11 -8
@@ 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 {
@@ 70,4 73,4 @@ class ApkSecretsImporterTest {
}
return url!!
}
-}>
\ No newline at end of file
+}
M app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt => app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt +14 -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.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)
}
}
M build.gradle.kts => build.gradle.kts +6 -0
@@ 3,4 3,10 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
+}
+
+buildscript {
+ dependencies {
+ classpath(libs.protobuf.gradle.plugin)
+ }
}=
\ No newline at end of file
M gradle/libs.versions.toml => gradle/libs.versions.toml +6 -0
@@ 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" }