mirror of
https://github.com/nacabaro/vbhelper.git
synced 2026-01-27 16:05:32 +00:00
Verify Secret correctness as part of loading.
Improve tests
This commit is contained in:
parent
f4974c8705
commit
9871f0420f
@ -3,13 +3,14 @@ package com.github.nacabaro.vbhelper.source
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
class ApkSecretsImporter(private val dexFileSecretsImporter: DexFileSecretsImporter = DexFileSecretsImporter()) {
|
class ApkSecretsImporter(private val dexFileSecretsImporter: SecretsImporter = DexFileSecretsImporter()): SecretsImporter {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DEX_FILE = "classes.dex"
|
const val DEX_FILE = "classes.dex"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importSecrets(inputStream: InputStream): Map<UShort, Secrets> {
|
// importSecrets imports the secrets from the apk input stream, and validates them.
|
||||||
|
override fun importSecrets(inputStream: InputStream): Map<UShort, Secrets> {
|
||||||
ZipInputStream(inputStream).use { zip ->
|
ZipInputStream(inputStream).use { zip ->
|
||||||
var zipEntry = zip.nextEntry
|
var zipEntry = zip.nextEntry
|
||||||
while(zipEntry != null) {
|
while(zipEntry != null) {
|
||||||
|
|||||||
@ -5,9 +5,10 @@ import java.io.InputStream
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
|
||||||
|
|
||||||
class DexFileSecretsImporter {
|
class DexFileSecretsImporter: SecretsImporter {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val VBDM_SUBSTITUTION_CIPHER_IDX = 1080145
|
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_1_IDX = 1580157
|
||||||
const val BE_HMAC_KEY_2_IDX = 1593759
|
const val BE_HMAC_KEY_2_IDX = 1593759
|
||||||
const val AES_KEY_IDX = 1277527
|
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<UShort, Secrets> {
|
override fun importSecrets(inputStream: InputStream): Map<UShort, Secrets> {
|
||||||
|
val deviceToSecrets = readSecrets(inputStream)
|
||||||
|
verifySecretCorrectness(deviceToSecrets)
|
||||||
|
return deviceToSecrets
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readSecrets(inputStream: InputStream): Map<UShort, Secrets> {
|
||||||
val dexFile = inputStream.readBytes()
|
val dexFile = inputStream.readBytes()
|
||||||
val byteOrder = ByteOrder.BIG_ENDIAN
|
val byteOrder = ByteOrder.BIG_ENDIAN
|
||||||
val vbdmSubstitutionCipher = dexFile.sliceArray(VBDM_SUBSTITUTION_CIPHER_IDX until VBDM_SUBSTITUTION_CIPHER_IDX+(16*4)).toIntArray(byteOrder)
|
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 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 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.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.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)),
|
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 {
|
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)
|
val hmacKey2 = dexFile.sliceArray(hmacKeyIdx2 until hmacKeyIdx2+24).toString(StandardCharsets.UTF_8)
|
||||||
return Secrets(hmacKey1, hmacKey2, aesKey, substitutionCipher)
|
return Secrets(hmacKey1, hmacKey2, aesKey, substitutionCipher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
private fun verifySecretCorrectness(deviceToSecrets: Map<UShort, Secrets>) {
|
||||||
|
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 {
|
fun ByteArray.toIntArray(byteOrder: ByteOrder): IntArray {
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.github.nacabaro.vbhelper.source
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
fun interface SecretsImporter {
|
||||||
|
fun importSecrets(inputStream: InputStream): Map<UShort, Secrets>
|
||||||
|
}
|
||||||
@ -3,14 +3,63 @@ package com.github.nacabaro.vbhelper.source
|
|||||||
import com.github.cfogrady.vbnfc.data.DeviceType
|
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
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 {
|
class ApkSecretsImporterTest {
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
@Test
|
@Test
|
||||||
fun importSecretsTest() {
|
fun testThatRealImportSecretsHasAllDeviceTypes() {
|
||||||
val apkSecretsImporter = ApkSecretsImporter()
|
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")
|
val url = javaClass.getResource("com.bandai.vitalbraceletarena.apk")
|
||||||
if(url == null) {
|
if(url == null) {
|
||||||
Assert.assertTrue("""
|
Assert.assertTrue("""
|
||||||
@ -19,16 +68,6 @@ class ApkSecretsImporterTest {
|
|||||||
should never be checked in and should be on the .gitignore.
|
should never be checked in and should be on the .gitignore.
|
||||||
""".trimIndent(), false)
|
""".trimIndent(), false)
|
||||||
}
|
}
|
||||||
val file = File(url.path)
|
return url!!
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,14 +3,44 @@ package com.github.nacabaro.vbhelper.source
|
|||||||
import com.github.cfogrady.vbnfc.data.DeviceType
|
import com.github.cfogrady.vbnfc.data.DeviceType
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
|
||||||
|
|
||||||
class DexFileSecretsImporterTest {
|
class DexFileSecretsImporterTest {
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
@Test
|
@Test
|
||||||
fun importSecretsTest() {
|
fun testThatImportSecretsHasAllDeviceTypes() {
|
||||||
val dexFileSecretsImporter = DexFileSecretsImporter()
|
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")
|
val url = javaClass.getResource("classes.dex")
|
||||||
if(url == null) {
|
if(url == null) {
|
||||||
Assert.assertTrue("""
|
Assert.assertTrue("""
|
||||||
@ -19,16 +49,26 @@ class DexFileSecretsImporterTest {
|
|||||||
checked in and should be on the .gitignore.
|
checked in and should be on the .gitignore.
|
||||||
""".trimIndent(), false)
|
""".trimIndent(), false)
|
||||||
}
|
}
|
||||||
val file = File(url.path)
|
return url!!
|
||||||
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)
|
fun IntArray.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray {
|
||||||
Assert.assertEquals("5651b1c8", password.toHexString())
|
val byteArray = ByteArray(this.size*4)
|
||||||
password = deviceIdToCryptographicTransformer[DeviceType.VitalSeriesDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId)
|
for(i in this.indices) {
|
||||||
Assert.assertEquals("dd2ceb84", password.toHexString())
|
val byteArrayIndex = i*4
|
||||||
password = deviceIdToCryptographicTransformer[DeviceType.VitalCharactersDeviceType]!!.toCryptographicTransformer().createNfcPassword(testTagId)
|
this[i].toByteArray(byteArray, byteArrayIndex, byteOrder)
|
||||||
Assert.assertEquals("515e0c12", password.toHexString())
|
}
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user