From f4974c8705bc6c05bcda567e2c954670d05c7df0 Mon Sep 17 00:00:00 2001 From: Christopher O'Grady Date: Wed, 8 Jan 2025 21:01:37 -0500 Subject: [PATCH 1/2] Import APK in tests --- .gitignore | 4 ++ .../vbhelper/source/ApkSecretsImporter.kt | 25 +++++++++ .../vbhelper/source/DexFileSecretsImporter.kt | 51 +++++++++++++++++++ .../nacabaro/vbhelper/source/Secrets.kt | 9 ++++ .../vbhelper/source/ApkSecretsImporterTest.kt | 34 +++++++++++++ .../source/DexFileSecretsImporterTest.kt | 34 +++++++++++++ 6 files changed, 157 insertions(+) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/Secrets.kt create mode 100644 app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt create mode 100644 app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt diff --git a/.gitignore b/.gitignore index 3017dac..d8d1c1d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ local.properties app/src/main/res/values/keys.xml + +app/src/test/resources/com/github/nacabaro/vbhelper/source/com.bandai.vitalbraceletarena.apk + +app/src/test/resources/com/github/nacabaro/vbhelper/source/classes.dex diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt new file mode 100644 index 0000000..f2bbb1e --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt @@ -0,0 +1,25 @@ +package com.github.nacabaro.vbhelper.source + +import java.io.InputStream +import java.util.zip.ZipInputStream + +class ApkSecretsImporter(private val dexFileSecretsImporter: DexFileSecretsImporter = DexFileSecretsImporter()) { + + companion object { + const val DEX_FILE = "classes.dex" + } + + fun importSecrets(inputStream: InputStream): Map { + ZipInputStream(inputStream).use { zip -> + var zipEntry = zip.nextEntry + while(zipEntry != null) { + println("Zip Entry: ${zipEntry.name}") + if(zipEntry.name == DEX_FILE) { + return dexFileSecretsImporter.importSecrets(zip) + } + zipEntry = zip.nextEntry + } + throw IllegalArgumentException("File `$DEX_FILE` is missing from apk!") + } + } +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt new file mode 100644 index 0000000..b37211d --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt @@ -0,0 +1,51 @@ +package com.github.nacabaro.vbhelper.source + +import com.github.cfogrady.vbnfc.data.DeviceType +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.charset.StandardCharsets + + +class DexFileSecretsImporter { + companion object { + + const val VBDM_SUBSTITUTION_CIPHER_IDX = 1080145 + const val BE_SUBSTITUTION_CIPHER_IDX = 1080217 + + const val VBDM_HMAC_KEY_2_IDX = 1249063 + const val VBDM_HMAC_KEY_1_IDX = 1494074 + const val VBC_HMAC_KEY_1_IDX = 1241640 + const val VBC_HMAC_KEY_2_IDX = 1466955 + const val BE_HMAC_KEY_1_IDX = 1580157 + const val BE_HMAC_KEY_2_IDX = 1593759 + const val AES_KEY_IDX = 1277527 + } + + fun importSecrets(inputStream: InputStream): Map { + 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 cryptographicTransformerByDevices = 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 cryptographicTransformerByDevices + } + + private fun buildSecrets(dexFile: ByteArray, hmacKeyIdx1: Int, hmacKeyIdx2: Int, aesKey: String, substitutionCipher: IntArray): Secrets { + 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) + } +} + +fun ByteArray.toIntArray(byteOrder: ByteOrder): IntArray { + require(this.size % 4 == 0) { "Number of bytes must be multiple of 4 to convert into 32-bit words" } + val values = IntArray(this.size / 4) + ByteBuffer.wrap(this).order(byteOrder).asIntBuffer()[values] + return values +} \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/Secrets.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/Secrets.kt new file mode 100644 index 0000000..485a670 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/Secrets.kt @@ -0,0 +1,9 @@ +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) + } +} diff --git a/app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt b/app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt new file mode 100644 index 0000000..d191753 --- /dev/null +++ b/app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt @@ -0,0 +1,34 @@ +package com.github.nacabaro.vbhelper.source + +import com.github.cfogrady.vbnfc.data.DeviceType +import org.junit.Assert +import org.junit.Test +import java.io.File + +class ApkSecretsImporterTest { + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun importSecretsTest() { + val apkSecretsImporter = ApkSecretsImporter() + val url = javaClass.getResource("com.bandai.vitalbraceletarena.apk") + if(url == null) { + Assert.assertTrue(""" + Create `resources\com\github\nacabaro\vbhelper\source` within the src/test directory. + Add com.bandai.vitalbraceletarena.apk (the official apk) in the above directory. It + should never be checked in and should be on the .gitignore. + """.trimIndent(), false) + } + val file = File(url.path) + val testTagId = byteArrayOf(0x04, 0x40, 0xaf.toByte(), 0xa2.toByte(), 0xee.toByte(), 0x0f, 0x90.toByte()) + file.inputStream().use { + val deviceIdToCryptographicTransformer = apkSecretsImporter.importSecrets(it) + var password = deviceIdToCryptographicTransformer[DeviceType.VitalBraceletBEDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) + Assert.assertEquals("5651b1c8", password.toHexString()) + password = deviceIdToCryptographicTransformer[DeviceType.VitalSeriesDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) + Assert.assertEquals("dd2ceb84", password.toHexString()) + password = deviceIdToCryptographicTransformer[DeviceType.VitalCharactersDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) + Assert.assertEquals("515e0c12", password.toHexString()) + } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt b/app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt new file mode 100644 index 0000000..5511f47 --- /dev/null +++ b/app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt @@ -0,0 +1,34 @@ +package com.github.nacabaro.vbhelper.source + +import com.github.cfogrady.vbnfc.data.DeviceType +import org.junit.Assert +import org.junit.Test +import java.io.File + + +class DexFileSecretsImporterTest { + @OptIn(ExperimentalStdlibApi::class) + @Test + fun importSecretsTest() { + val dexFileSecretsImporter = DexFileSecretsImporter() + val url = javaClass.getResource("classes.dex") + if(url == null) { + Assert.assertTrue(""" + Create `resources\com\github\nacabaro\vbhelper\source` within the src/test directory. + Add classes.dex from the official apk in the above directory. It should never be + checked in and should be on the .gitignore. + """.trimIndent(), false) + } + val file = File(url.path) + val testTagId = byteArrayOf(0x04, 0x40, 0xaf.toByte(), 0xa2.toByte(), 0xee.toByte(), 0x0f, 0x90.toByte()) + file.inputStream().use { + val deviceIdToCryptographicTransformer = dexFileSecretsImporter.importSecrets(it) + var password = deviceIdToCryptographicTransformer[DeviceType.VitalBraceletBEDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) + Assert.assertEquals("5651b1c8", password.toHexString()) + password = deviceIdToCryptographicTransformer[DeviceType.VitalSeriesDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) + Assert.assertEquals("dd2ceb84", password.toHexString()) + password = deviceIdToCryptographicTransformer[DeviceType.VitalCharactersDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) + Assert.assertEquals("515e0c12", password.toHexString()) + } + } +} \ No newline at end of file From 9871f0420fcf991209087a9e2810ad9fafc88edb Mon Sep 17 00:00:00 2001 From: Christopher O'Grady Date: Wed, 8 Jan 2025 22:12:03 -0500 Subject: [PATCH 2/2] Verify Secret correctness as part of loading. Improve tests --- .../vbhelper/source/ApkSecretsImporter.kt | 5 +- .../vbhelper/source/DexFileSecretsImporter.kt | 52 ++++++++++++-- .../vbhelper/source/SecretsImporter.kt | 7 ++ .../vbhelper/source/ApkSecretsImporterTest.kt | 67 +++++++++++++++---- .../source/DexFileSecretsImporterTest.kt | 64 ++++++++++++++---- 5 files changed, 163 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsImporter.kt diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt index f2bbb1e..df63d20 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporter.kt @@ -3,13 +3,14 @@ package com.github.nacabaro.vbhelper.source import java.io.InputStream import java.util.zip.ZipInputStream -class ApkSecretsImporter(private val dexFileSecretsImporter: DexFileSecretsImporter = DexFileSecretsImporter()) { +class ApkSecretsImporter(private val dexFileSecretsImporter: SecretsImporter = DexFileSecretsImporter()): SecretsImporter { companion object { const val DEX_FILE = "classes.dex" } - fun importSecrets(inputStream: InputStream): Map { + // importSecrets imports the secrets from the apk input stream, and validates them. + override fun importSecrets(inputStream: InputStream): Map { ZipInputStream(inputStream).use { zip -> var zipEntry = zip.nextEntry while(zipEntry != null) { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt index b37211d..ca6f4e7 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporter.kt @@ -5,9 +5,10 @@ import java.io.InputStream import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.charset.StandardCharsets +import java.security.InvalidKeyException -class DexFileSecretsImporter { +class DexFileSecretsImporter: SecretsImporter { companion object { const val VBDM_SUBSTITUTION_CIPHER_IDX = 1080145 @@ -20,20 +21,31 @@ class DexFileSecretsImporter { const val BE_HMAC_KEY_1_IDX = 1580157 const val BE_HMAC_KEY_2_IDX = 1593759 const val AES_KEY_IDX = 1277527 + + val TEST_TAG = byteArrayOf(0x34, 0x01, 0x10, 0xff.toByte(), 0xf5.toByte(), 0x00, 0xa2.toByte()) + const val BE_TEST_TAG_PASSWORD = "be29a87e" + const val VBDM_TEST_TAG_PASSWORD = "6ea33673" + const val VBC_TEST_TAG_PASSWORD = "a71dfb22" } - fun importSecrets(inputStream: InputStream): Map { + override fun importSecrets(inputStream: InputStream): Map { + val deviceToSecrets = readSecrets(inputStream) + verifySecretCorrectness(deviceToSecrets) + return deviceToSecrets + } + + private fun readSecrets(inputStream: InputStream): Map { 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 cryptographicTransformerByDevices = mapOf( + 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 cryptographicTransformerByDevices + return secretsByDevices } private fun buildSecrets(dexFile: ByteArray, hmacKeyIdx1: Int, hmacKeyIdx2: Int, aesKey: String, substitutionCipher: IntArray): Secrets { @@ -41,6 +53,38 @@ class DexFileSecretsImporter { val hmacKey2 = dexFile.sliceArray(hmacKeyIdx2 until hmacKeyIdx2+24).toString(StandardCharsets.UTF_8) return Secrets(hmacKey1, hmacKey2, aesKey, substitutionCipher) } + + @OptIn(ExperimentalStdlibApi::class) + private fun verifySecretCorrectness(deviceToSecrets: Map) { + for (keyValue in deviceToSecrets) { + 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()}") + } + } + } + } + } } fun ByteArray.toIntArray(byteOrder: ByteOrder): IntArray { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsImporter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsImporter.kt new file mode 100644 index 0000000..e1a8e77 --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/source/SecretsImporter.kt @@ -0,0 +1,7 @@ +package com.github.nacabaro.vbhelper.source + +import java.io.InputStream + +fun interface SecretsImporter { + fun importSecrets(inputStream: InputStream): Map +} \ No newline at end of file diff --git a/app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt b/app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt index d191753..19177f2 100644 --- a/app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt +++ b/app/src/test/java/com/github/nacabaro/vbhelper/source/ApkSecretsImporterTest.kt @@ -3,14 +3,63 @@ package com.github.nacabaro.vbhelper.source import com.github.cfogrady.vbnfc.data.DeviceType import org.junit.Assert import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.io.File +import java.net.URL +import java.nio.charset.StandardCharsets +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream class ApkSecretsImporterTest { - @OptIn(ExperimentalStdlibApi::class) @Test - fun importSecretsTest() { - val apkSecretsImporter = ApkSecretsImporter() + fun testThatRealImportSecretsHasAllDeviceTypes() { + 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]) + } + } + + @Test + fun testThatApkSecretsImporterCallsDexSecretImporterOnDexFile() { + val expectedDexContents = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1) + var foundFile = false + val apkFileSecretsImporter = ApkSecretsImporter { + val inputStreamContents = it.readAllBytes() + Assert.assertTrue("Unexpected file contents received by DexSecretsImporter", inputStreamContents.contentEquals(expectedDexContents)) + foundFile = true + emptyMap() + } + val apkBytes = constructTestApk(expectedDexContents) + ByteArrayInputStream(apkBytes).use { + apkFileSecretsImporter.importSecrets(it) + } + Assert.assertTrue("${ApkSecretsImporter.DEX_FILE} not found", foundFile) + } + + fun constructTestApk(dexContents: ByteArray): ByteArray { + val byteArrayOutputStream = ByteArrayOutputStream() + ZipOutputStream(byteArrayOutputStream).use { zipOutputStream -> + zipOutputStream.putNextEntry(ZipEntry("dummy.txt")) + zipOutputStream.write("This is a text file".toByteArray(StandardCharsets.UTF_8)) + zipOutputStream.putNextEntry(ZipEntry("AndroidManifest.xml")) + zipOutputStream.write("Malformed xml!".toByteArray(StandardCharsets.UTF_8)) + zipOutputStream.putNextEntry(ZipEntry("assets/")) + zipOutputStream.putNextEntry(ZipEntry("assets/bad.assets")) + zipOutputStream.write("Malformed asset!".toByteArray(StandardCharsets.UTF_8)) + zipOutputStream.putNextEntry(ZipEntry(ApkSecretsImporter.DEX_FILE)) + zipOutputStream.write(dexContents) + } + return byteArrayOutputStream.toByteArray() + } + + private fun getAndAssertApkFile(): URL { val url = javaClass.getResource("com.bandai.vitalbraceletarena.apk") if(url == null) { Assert.assertTrue(""" @@ -19,16 +68,6 @@ class ApkSecretsImporterTest { should never be checked in and should be on the .gitignore. """.trimIndent(), false) } - val file = File(url.path) - val testTagId = byteArrayOf(0x04, 0x40, 0xaf.toByte(), 0xa2.toByte(), 0xee.toByte(), 0x0f, 0x90.toByte()) - file.inputStream().use { - val deviceIdToCryptographicTransformer = apkSecretsImporter.importSecrets(it) - var password = deviceIdToCryptographicTransformer[DeviceType.VitalBraceletBEDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) - Assert.assertEquals("5651b1c8", password.toHexString()) - password = deviceIdToCryptographicTransformer[DeviceType.VitalSeriesDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) - Assert.assertEquals("dd2ceb84", password.toHexString()) - password = deviceIdToCryptographicTransformer[DeviceType.VitalCharactersDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) - Assert.assertEquals("515e0c12", password.toHexString()) - } + return url!! } } \ No newline at end of file diff --git a/app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt b/app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt index 5511f47..e58dfe1 100644 --- a/app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt +++ b/app/src/test/java/com/github/nacabaro/vbhelper/source/DexFileSecretsImporterTest.kt @@ -3,14 +3,44 @@ package com.github.nacabaro.vbhelper.source import com.github.cfogrady.vbnfc.data.DeviceType import org.junit.Assert import org.junit.Test +import java.io.ByteArrayInputStream import java.io.File +import java.net.URL +import java.nio.ByteOrder +import java.security.InvalidKeyException class DexFileSecretsImporterTest { - @OptIn(ExperimentalStdlibApi::class) + @Test - fun importSecretsTest() { + fun testThatImportSecretsHasAllDeviceTypes() { 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]) + } + } + + @Test + fun testThatImportWrongSecretsThrows() { + val dexFileSecretsImporter = DexFileSecretsImporter() + val url = getAndAssertClassesDexFile() + val file = File(url.path) + val content = file.readBytes() + val badCipher = intArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15).toByteArray(ByteOrder.BIG_ENDIAN) + badCipher.copyInto(content, DexFileSecretsImporter.BE_SUBSTITUTION_CIPHER_IDX) + ByteArrayInputStream(content).use { + Assert.assertThrows("Secrets are validated", InvalidKeyException::class.java) { + dexFileSecretsImporter.importSecrets(it) + } + } + } + + private fun getAndAssertClassesDexFile(): URL { val url = javaClass.getResource("classes.dex") if(url == null) { Assert.assertTrue(""" @@ -19,16 +49,26 @@ class DexFileSecretsImporterTest { checked in and should be on the .gitignore. """.trimIndent(), false) } - val file = File(url.path) - val testTagId = byteArrayOf(0x04, 0x40, 0xaf.toByte(), 0xa2.toByte(), 0xee.toByte(), 0x0f, 0x90.toByte()) - file.inputStream().use { - val deviceIdToCryptographicTransformer = dexFileSecretsImporter.importSecrets(it) - var password = deviceIdToCryptographicTransformer[DeviceType.VitalBraceletBEDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) - Assert.assertEquals("5651b1c8", password.toHexString()) - password = deviceIdToCryptographicTransformer[DeviceType.VitalSeriesDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) - Assert.assertEquals("dd2ceb84", password.toHexString()) - password = deviceIdToCryptographicTransformer[DeviceType.VitalCharactersDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId) - Assert.assertEquals("515e0c12", password.toHexString()) + return url!! + } +} + +fun IntArray.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray { + val byteArray = ByteArray(this.size*4) + for(i in this.indices) { + val byteArrayIndex = i*4 + this[i].toByteArray(byteArray, byteArrayIndex, byteOrder) + } + return byteArray +} + +fun Int.toByteArray(bytes: ByteArray, dstIndex: Int, byteOrder: ByteOrder = ByteOrder.nativeOrder()) { + val asUInt = this.toUInt() + for(i in 0 until 4) { + if(byteOrder == ByteOrder.LITTLE_ENDIAN) { + bytes[i+dstIndex] = ((asUInt shr 8*i) and 255u).toByte() + } else { + bytes[(3-i) + dstIndex] = ((asUInt shr 8*i) and 255u).toByte() } } } \ No newline at end of file