Use nfc library from maven local instead of copied module.

This commit is contained in:
Christopher O'Grady 2025-01-04 23:03:01 -05:00
parent 3019560a26
commit 5bd5fc3f74
39 changed files with 4 additions and 1982 deletions

View File

@ -42,7 +42,7 @@ android {
dependencies { dependencies {
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)
implementation(project(":vb-nfc-reader")) implementation(libs.vb.nfc.reader)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
annotationProcessor(libs.androidx.room.compiler) annotationProcessor(libs.androidx.room.compiler)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)

View File

@ -9,6 +9,7 @@ lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3" activityCompose = "1.9.3"
composeBom = "2024.04.01" composeBom = "2024.04.01"
roomRuntime = "2.6.1" roomRuntime = "2.6.1"
vbNfcReader = "0.1.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -27,6 +28,7 @@ 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-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
vb-nfc-reader = { module = "com.github.cfogrady:vb-nfc-reader", version.ref = "vbNfcReader" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }

View File

@ -14,6 +14,7 @@ pluginManagement {
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
mavenLocal()
google() google()
mavenCentral() mavenCentral()
} }
@ -21,4 +22,3 @@ dependencyResolutionManagement {
rootProject.name = "VBHelper" rootProject.name = "VBHelper"
include(":app") include(":app")
include(":vb-nfc-reader")

View File

@ -1 +0,0 @@
/build

3
vb-nfc-reader/.idea/.gitignore generated vendored
View File

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -1,340 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceStreaming">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
<option name="codename" value="OP573DL1" />
<option name="id" value="OP573DL1" />
<option name="manufacturer" value="OPPO" />
<option name="name" value="CPH2557" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-21" />
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -1,9 +0,0 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -1,44 +0,0 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.github.cfogrady.vbnfc"
compileSdk = 34
defaultConfig {
minSdk = 28
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
testImplementation("io.mockk:mockk-android:1.13.14")
testImplementation("io.mockk:mockk-agent:1.13.14")
}

View File

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -1,24 +0,0 @@
package com.github.cfogrady.vbnfc
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.github.cfogrady.vbnfc.test", appContext.packageName)
}
}

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -1,78 +0,0 @@
package com.github.cfogrady.vbnfc
import java.lang.IllegalArgumentException
import java.nio.ByteOrder
fun ByteArray.getUInt32(index: Int = 0, byteOrder: ByteOrder = ByteOrder.nativeOrder()): UInt {
if (this.size < index + 4) {
throw IllegalArgumentException("Must be 4 bytes from index to get a UInt")
}
var result: UInt = 0u
for (i in 0 until 4) {
result = if (byteOrder == ByteOrder.BIG_ENDIAN) {
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(3 - i))
} else {
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(i))
}
}
return result
}
fun UInt.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray {
val byteArray = byteArrayOf(0, 0, 0, 0)
for(i in 0 until 4) {
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
byteArray[i] = ((this shr 8*i) and 255u).toByte()
} else {
byteArray[3-i] = ((this shr 8*i) and 255u).toByte()
}
}
return byteArray
}
fun ByteArray.getUInt16(index: Int = 0, byteOrder: ByteOrder = ByteOrder.nativeOrder()): UShort {
if (this.size < index + 2) {
throw IllegalArgumentException("Must be 2 bytes from index to get a UInt")
}
var result: UInt = 0u
for (i in 0 until 2) {
result = if (byteOrder == ByteOrder.BIG_ENDIAN) {
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(1 - i))
} else {
result or ((this[index+i].toUInt() and 0xFFu) shl 8*(i))
}
}
return result.toUShort()
}
fun UShort.toByteArray(byteOrder: ByteOrder = ByteOrder.nativeOrder()): ByteArray {
val byteArray = byteArrayOf(0, 0)
val asUInt = this.toUInt()
for(i in 0 until 2) {
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
byteArray[i] = ((asUInt shr 8*i) and 255u).toByte()
} else {
byteArray[1-i] = ((asUInt shr 8*i) and 255u).toByte()
}
}
return byteArray
}
fun UShort.toByteArray(bytes: ByteArray, dstIndex: Int, byteOrder: ByteOrder = ByteOrder.nativeOrder()) {
val asUInt = this.toUInt()
for(i in 0 until 2) {
if(byteOrder == ByteOrder.LITTLE_ENDIAN) {
bytes[i+dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
} else {
bytes[(1-i) + dstIndex] = ((asUInt shr 8*i) and 255u).toByte()
}
}
}
fun ByteArray.copyIntoUShortArray(offset: Int, length: Int): Array<UShort> {
val result = Array<UShort>(length) { 0u }
for (i in 0..<length) {
result[i] = this.getUInt16(offset + i * 2, ByteOrder.BIG_ENDIAN)
}
return result
}

View File

@ -1,41 +0,0 @@
package com.github.cfogrady.vbnfc
import java.lang.IllegalStateException
class ChecksumCalculator {
companion object {
private val PagesWithChecksum = hashSetOf(8, 16, 24, 32, 40, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 104, 192, 200, 208, 216)
}
@OptIn(ExperimentalStdlibApi::class)
fun checkChecksums(data: ByteArray) {
operateOnChecksums(data) { checksumByte, checksumIdx ->
if (checksumByte != data[checksumIdx]) {
throw IllegalStateException("Checksum ${checksumByte.toHexString()} doesn't match expected ${data[checksumIdx].toHexString()}")
}
}
}
@OptIn(ExperimentalStdlibApi::class)
fun recalculateChecksums(data: ByteArray) {
operateOnChecksums(data) { checksumByte, checksumIdx ->
data[checksumIdx] = checksumByte
}
}
private fun operateOnChecksums(data: ByteArray, operator: (Byte, Int)->Unit) {
// loop through all data
for(i in data.indices step 16) {
val page = i/4 + 8 // first 8 pages are header data and not part of the character data
if (PagesWithChecksum.contains(page)) {
var sum = 0
val checksumIndex = i + 15
for(j in i..<checksumIndex) {
sum += data[j]
}
val checksumByte = (sum and 0xff).toByte()
operator(checksumByte, checksumIndex)
}
}
}
}

View File

@ -1,110 +0,0 @@
package com.github.cfogrady.vbnfc
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.xor
class CryptographicTransformer(private val readableHmacKey1: String, private val readableHmacKey2: String, private val aesKey: String, private val substitutionCipher: IntArray) {
companion object {
const val HMAC256 = "HmacSHA256"
}
// Creates a 4 byte password by hashing the current data using HMAC256 with the first key. Then
// applying a 4-bit substitution cypher on the result, and hashing again with the second key.
// The password are the 4 bytes starting at index 28 of that result.
fun createNfcPassword(inputData: ByteArray): ByteArray {
val hmacKey1 = decryptHmacKey(readableHmacKey1)
val hmacKey2 = decryptHmacKey(readableHmacKey2)
val hashedInput = generateHMacSHA256Hash(hmacKey1, inputData)
val substitutedBytes = apply4BitSubstitutionCipher(hashedInput)
val secondHash = generateHMacSHA256Hash(hmacKey2, substitutedBytes)
return secondHash.sliceArray(28..<32)
}
fun decryptData(data: ByteArray, tagId: ByteArray): ByteArray {
val hmacKey1 = decryptHmacKey(readableHmacKey1)
val hmacKey2 = decryptHmacKey(readableHmacKey2)
return cryptoTransformation(Cipher.DECRYPT_MODE, data, tagId, hmacKey1, hmacKey2)
}
fun encryptData(data: ByteArray, tagId: ByteArray): ByteArray {
val hmacKey1 = decryptHmacKey(readableHmacKey1)
val hmacKey2 = decryptHmacKey(readableHmacKey2)
return cryptoTransformation(Cipher.ENCRYPT_MODE, data, tagId, hmacKey1, hmacKey2)
}
private fun decryptHmacKey(str: String): String {
val decoded = Base64.getDecoder().decode(str)
val decrypt = decryptAesCbcPkcs5Padding(aesKey, decoded)
return String(decrypt, StandardCharsets.UTF_8)
}
private fun decryptAesCbcPkcs5Padding(key: String, data: ByteArray): ByteArray {
val keyBytes = key.toByteArray(StandardCharsets.UTF_8)
val rightSizedKey = keyBytes.copyOf(32)
val ivBytes = keyBytes.copyOfRange(key.length - 16, key.length)
val secretKeySpec = SecretKeySpec(rightSizedKey, "AES")
val ivParameterSpec = IvParameterSpec(ivBytes)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
return cipher.doFinal(data)
}
private fun generateHMacSHA256Hash(hmacKey: String, data: ByteArray): ByteArray {
val hmacKeyBytes = hmacKey.toByteArray(StandardCharsets.US_ASCII)
val secretKeySpec = SecretKeySpec(hmacKeyBytes, HMAC256)
val mac = Mac.getInstance(HMAC256)
mac.init(secretKeySpec)
return mac.doFinal(data)
}
// This is a 4 bit substitution cipher, where each 4 bits act as an index to another 4 bits
private fun apply4BitSubstitutionCipher(data: ByteArray): ByteArray {
val result = ByteArray(data.size)
for (idx in data.indices) {
val byte: Int = data[idx].toInt()
var newByte = 0
for (fourBitShifts in 0..<2) { // perform one OR without shift, and one OR shifted 4 bits
val shift = fourBitShifts * 4
val permutationIndex = (byte shr shift) and 0xF
newByte = newByte or (substitutionCipher[permutationIndex] shl shift)
}
result[idx] = newByte.toByte()
}
return result
}
// Hashes the tagId once and applies substitution cipher. Then hashes again.
// Splits hash and original tagId into key and initialization vector
private fun cryptoTransformation(cipherMode: Int, data: ByteArray, tagId: ByteArray, hmacKey1: String, hmacKey2: String): ByteArray {
var hashedTagId = apply4BitSubstitutionCipher(generateHMacSHA256Hash(hmacKey1, tagId))
hashedTagId = generateHMacSHA256Hash(hmacKey2, hashedTagId) // second hash
// generate actual key and initializing vector from tagIdGeneratedKey
val iv1 = ByteArray(15)
hashedTagId.copyInto(iv1, 0, 24, 32)
tagId.copyInto(iv1, 8, 0, 7)
val iv2 = hashedTagId.copyOf(15)
val ivParameterSpec = IvParameterSpec(xorBytes(iv1, iv2, 16))
val secretKeySpec = SecretKeySpec(hashedTagId.copyOf(16), "AES")
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
cipher.init(cipherMode, secretKeySpec, ivParameterSpec)
return cipher.doFinal(data)
}
private fun xorBytes(data1: ByteArray, data2: ByteArray, resultSize: Int): ByteArray {
val results = ByteArray(resultSize)
results.fill(0)
for (i in data1.indices) {
results[i] = data1[i] xor data2[i]
}
return results
}
}

View File

@ -1,30 +0,0 @@
package com.github.cfogrady.vbnfc
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.data.NfcHeader
interface NfcDataTranslator {
// setCharacterInByteArray takes the NfcCharacter and modifies the byte array with character
// data. At the time of writing this is used to write a parsed character into fresh unparsed
// device data when sending a character back to the device.
fun setCharacterInByteArray(character: NfcCharacter, bytes: ByteArray) {
}
// finalizeByteArrayFormat finalizes the byte array for NFC format by setting all the
// checksums, and duplicating the duplicate memory pages.
fun finalizeByteArrayFormat(bytes: ByteArray)
// getOperationCommandBytes gets an operation command corresponding to the existing header and
// the input operation
fun getOperationCommandBytes(header: NfcHeader, operation: Byte): ByteArray
// parseNfcCharacter parses the nfc data byte array into an instance of a NfcCharacter object
fun parseNfcCharacter(bytes: ByteArray): NfcCharacter
// parseHeader parses the nfc header byte array into an instance of NfcHeader
fun parseHeader(headerBytes: ByteArray): NfcHeader
val cryptographicTransformer: CryptographicTransformer
}

View File

@ -1,22 +0,0 @@
package com.github.cfogrady.vbnfc
import com.github.cfogrady.vbnfc.be.BENfcDataTranslator
import com.github.cfogrady.vbnfc.data.DeviceType
class NfcDataTranslatorFactory(
private val translators: MutableMap<UShort,NfcDataTranslator> = HashMap()
) {
fun getNfcDataTranslator(deviceTypeId: UShort): NfcDataTranslator {
val dataTranslator = translators[deviceTypeId]
if(dataTranslator != null) {
return dataTranslator
}
throw UnsupportedOperationException("Device type ${deviceTypeId} is not yet supported")
}
fun addNfcDataTranslator(nfcDataTranslator: NfcDataTranslator, deviceTypeId: UShort) {
translators[deviceTypeId] = nfcDataTranslator
}
}

View File

@ -1,193 +0,0 @@
package com.github.cfogrady.vbnfc
import android.nfc.tech.NfcA
import android.util.Log
import com.github.cfogrady.vbnfc.be.BENfcDataTranslator
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.data.NfcHeader
import java.nio.ByteOrder
class TagCommunicator(
private val nfcData: NfcA,
private val checksumCalculator: ChecksumCalculator,
private val nfcDataTranslatorFactory: NfcDataTranslatorFactory,
) {
companion object {
const val TAG = "VBNfcHandler"
const val HEADER_PAGE: Byte = 0x04
const val NFC_PASSWORD_COMMAND: Byte = 0x1b
const val NFC_READ_COMMAND: Byte = 0x30
const val NFC_WRITE_COMMAND: Byte = 0xA2.toByte()
const val STATUS_IDLE: Byte = 0
const val STATUS_READY: Byte = 1
const val OPERATION_IDLE: Byte = 0
const val OPERATION_READY: Byte = 1
const val OPERATION_TRANSFERRED_TO_APP: Byte = 2
const val OPERATION_CHECK_DIM: Byte = 3
const val OPERATION_TRANSFERED_TO_DEVICE: Byte = 4
const val START_DATA_PAGE = 8
const val LAST_DATA_PAGE = 220 // technically 223, but we read 4 pages at a time.
fun getInstance(nfcData: NfcA, deviceTypeIdSecrets: Map<UShort, CryptographicTransformer>): TagCommunicator {
val checksumCalculator = ChecksumCalculator()
val deviceToTranslator = HashMap<UShort, NfcDataTranslator>()
for (keyValue in deviceTypeIdSecrets) {
when(keyValue.key) {
DeviceType.VitalBraceletBEDeviceType -> {
deviceToTranslator[keyValue.key] = BENfcDataTranslator(keyValue.value, checksumCalculator)
}
else -> {
throw IllegalArgumentException("DeviceId ${keyValue.key} Provided Without Known Parser")
}
}
}
return TagCommunicator(nfcData, checksumCalculator, NfcDataTranslatorFactory(deviceToTranslator))
}
}
data class DeviceTranslatorAndHeader(val nfcHeader: NfcHeader, val translator: NfcDataTranslator)
@OptIn(ExperimentalStdlibApi::class)
fun receiveCharacter(): NfcCharacter {
val translatorAndHeader = fetchDeviceTranslatorAndHeader()
val header = translatorAndHeader.nfcHeader
val translator = translatorAndHeader.translator
Log.i(TAG, "Writing to make ready for operation")
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
Log.i(TAG, "Authenticating")
passwordAuth(translator.cryptographicTransformer)
Log.i(TAG, "Reading Character")
val encryptedCharacterData = readNfcData()
Log.i(TAG, "Raw NFC Data Received: ${encryptedCharacterData.toHexString()}")
val decryptedCharacterData = translator.cryptographicTransformer.decryptData(encryptedCharacterData, nfcData.tag.id)
checksumCalculator.checkChecksums(decryptedCharacterData)
val nfcCharacter = translator.parseNfcCharacter(decryptedCharacterData)
Log.i(TAG, "Known Character Stats: $nfcCharacter")
// Not ready to lose any of my mons in this...
//Log.i(TAG, "Signaling operation complete")
//nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_TRANSFERRED_TO_APP))
return nfcCharacter
}
@OptIn(ExperimentalStdlibApi::class)
private fun fetchDeviceTranslatorAndHeader(): DeviceTranslatorAndHeader {
val readData = nfcData.transceive(byteArrayOf(NFC_READ_COMMAND, HEADER_PAGE))
Log.i("TagCommunicator", "First 4 Pages: ${readData.toHexString()}")
val deviceTypeId = readData.getUInt16(4, ByteOrder.BIG_ENDIAN)
val translator = nfcDataTranslatorFactory.getNfcDataTranslator(deviceTypeId)
val header = translator.parseHeader(readData)
return DeviceTranslatorAndHeader(header, translator)
}
private fun readNfcData(): ByteArray {
val result = ByteArray(((LAST_DATA_PAGE +4)- START_DATA_PAGE) * 4)
for (page in START_DATA_PAGE..LAST_DATA_PAGE step 4) {
val pages = nfcData.transceive(byteArrayOf(NFC_READ_COMMAND, page.toByte()))
if (pages.size < 16) {
throw Exception("Failed to read page: $page")
}
System.arraycopy(pages, 0, result, (page - START_DATA_PAGE)*4, pages.size)
}
return result
}
fun prepareDIMForCharacter(dimId: UShort) {
val translatorAndHeader = fetchDeviceTranslatorAndHeader()
val header = translatorAndHeader.nfcHeader
val translator = translatorAndHeader.translator
// set app nonce to device ensure when we send back the character that we are preparing
// the same device we send to
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
// app authenticates, and reads everything, and checks the version
// The version check is only for the BE when transfering from DIM=0 (pulsemon).
// This was from the bug when the BE first came out.
// Check (page 103 [0:1] != 1, 0)
header.setDimId(dimId)
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_CHECK_DIM))
}
private fun defaultNfcDataGenerator(translator: NfcDataTranslator, character: NfcCharacter): ByteArray {
val currentNfcData = readNfcData()
val newNfcData = translator.cryptographicTransformer.decryptData(currentNfcData, nfcData.tag.id)
translator.setCharacterInByteArray(character, newNfcData)
checksumCalculator.recalculateChecksums(newNfcData)
translator.finalizeByteArrayFormat(newNfcData)
return newNfcData
}
// sendCharacter sends a character to the device using the nfcDataGenerator function. The
// default nfcDataGenerator reads the current data of the device and applies the new character
// data to the read data and prepares that to be sent back to the device. The nfcDataGenerator
// is a functor which takes in the NfcDataTranslator for the device and the NfcCharacter
// provided to the sendCharacter method and returns the decrypted byte array data to be sent
// back to the device.
@OptIn(ExperimentalStdlibApi::class)
fun sendCharacter(character: NfcCharacter, nfcDataGenerator: (NfcDataTranslator, NfcCharacter) -> ByteArray = this::defaultNfcDataGenerator) {
Log.i(TAG, "Sending Character: $character")
val deviceTranslatorAndHeader = fetchDeviceTranslatorAndHeader()
val translator = deviceTranslatorAndHeader.translator
val header = deviceTranslatorAndHeader.nfcHeader
// check the nonce
// if it's not expected, then the bracelet isn't ready
// Check the product and device ids that they match the target.
// This check relies on the app categories of BE, Vital Hero, and Vital Series.
// ensure the dim id matches the expected
if (character.dimId != header.getDimId()) {
throw IllegalArgumentException("Device is ready for DIM ${header.getDimId()}, but attempted to send ${character.dimId}")
}
// update the memory data
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
passwordAuth(translator.cryptographicTransformer)
var newNfcData = nfcDataGenerator(translator, character)
newNfcData = translator.cryptographicTransformer.encryptData(newNfcData, nfcData.tag.id)
Log.i(TAG, "Sending Character: ${newNfcData.toHexString()}")
// write nfc data
val pagedData = ConvertToPages(newNfcData)
for(pageToWriteIdx in 8..<pagedData.size) {
val pageToWrite = pagedData[pageToWriteIdx]
nfcData.transceive(byteArrayOf(NFC_WRITE_COMMAND, pageToWriteIdx.toByte(), pageToWrite[0], pageToWrite[1], pageToWrite[2], pageToWrite[3]))
}
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_TRANSFERED_TO_DEVICE))
}
@OptIn(ExperimentalStdlibApi::class)
fun passwordAuth(cryptographicTransformer: CryptographicTransformer) {
val tagId = nfcData.tag.id
Log.i(TAG, "TagId: ${tagId.toHexString()}")
val password = cryptographicTransformer.createNfcPassword(tagId)
try {
val result = nfcData.transceive(byteArrayOf(NFC_PASSWORD_COMMAND, password[0], password[1], password[2], password[3]))
Log.i(TAG, "PasswordAuth Result: ${result.toHexString()}")
if (result.size == 1) {
throw AuthenticationException("Authentication failed. Result: ${result.toHexString()}")
}
} catch (e: Exception) {
Log.e(TAG, "Exception: ${e.message}")
}
}
// addDataTranslator adds a new data translator to be used with the specified deviceTypeId.
// This can be used to keep the same general communication protocol, but allows for a different
// parsing of the data.
fun addDataTranslator(nfcDataTranslator: NfcDataTranslator, deviceTypeId: UShort) {
nfcDataTranslatorFactory.addNfcDataTranslator(nfcDataTranslator, deviceTypeId)
}
class AuthenticationException(message: String): Exception(message)
}

View File

@ -1,20 +0,0 @@
package com.github.cfogrady.vbnfc
// ConverToPages converts the byte array into the paged structure used in NFC communication
// If data for the header isn't included, the first 8 pages will be 0 filled.
fun ConvertToPages(data: ByteArray, header: ByteArray? = null) : List<ByteArray> {
val pages = ArrayList<ByteArray>()
// setup blank header pages
for (i in 0..7) {
if (header != null) {
val index = i*4
pages.add(header.sliceArray(index..<index+4))
} else {
pages.add(byteArrayOf(0, 0, 0, 0))
}
}
for(i in data.indices step 4) {
pages.add(data.sliceArray(i..<i+4))
}
return pages
}

View File

@ -1,183 +0,0 @@
package com.github.cfogrady.vbnfc.be
import com.github.cfogrady.vbnfc.data.NfcCharacter
import java.util.Arrays
import java.util.Objects
class BENfcCharacter(
dimId: UShort,
charIndex: UShort,
stage: Byte,
attribute: Attribute,
ageInDays: Byte,
nextAdventureMissionStage: Byte,
mood: Byte,
vitalPoints: UShort,
transformationCountdownInMinutes: UShort,
injuryStatus: InjuryStatus,
trainingPp: UShort,
currentPhaseBattlesWon: UShort,
currentPhaseBattlesLost: UShort,
totalBattlesWon: UShort,
totalBattlesLost: UShort,
activityLevel: Byte,
heartRateCurrent: UByte,
transformationHistory: Array<Transformation>,
var trainingHp: UShort,
var trainingAp: UShort,
var trainingBp: UShort,
var remainingTrainingTimeInMinutes: UShort,
var itemEffectMentalStateValue: Byte,
var itemEffectMentalStateMinutesRemaining: Byte,
var itemEffectActivityLevelValue: Byte,
var itemEffectActivityLevelMinutesRemaining: Byte,
var itemEffectVitalPointsChangeValue: Byte,
var itemEffectVitalPointsChangeMinutesRemaining: Byte,
var abilityRarity: AbilityRarity,
var abilityType: UShort,
var abilityBranch: UShort,
var abilityReset: Byte,
var rank: Byte,
var itemType: Byte,
var itemMultiplier: Byte,
var itemRemainingTime: Byte,
internal val otp0: ByteArray, // OTP matches the character to the dim
internal val otp1: ByteArray, // OTP matches the character to the dim
var characterCreationFirmwareVersion: FirmwareVersion,
var appReserved1: ByteArray, // this is a 12 byte array reserved for new app features, a custom app should be able to safely use this for custom features
var appReserved2: Array<UShort>, // this is a 3 element array reserved for new app features, a custom app should be able to safely use this for custom features
) :
NfcCharacter(
dimId = dimId,
charIndex = charIndex,
stage = stage,
attribute = attribute,
ageInDays = ageInDays,
nextAdventureMissionStage = nextAdventureMissionStage,
mood = mood,
vitalPoints = vitalPoints,
transformationCountdown = transformationCountdownInMinutes,
injuryStatus = injuryStatus,
trophies = trainingPp,
currentPhaseBattlesWon = currentPhaseBattlesWon,
currentPhaseBattlesLost = currentPhaseBattlesLost,
totalBattlesWon = totalBattlesWon,
totalBattlesLost = totalBattlesLost,
activityLevel = activityLevel,
heartRateCurrent = heartRateCurrent,
transformationHistory = transformationHistory,
)
{
fun getTrainingPp(): UShort {
return trophies
}
fun setTrainingPp(trainingPp: UShort) {
trophies = trainingPp
}
fun getWinPercentage(): Byte {
val totalBatles = currentPhaseBattlesWon + currentPhaseBattlesLost
if (totalBatles == 0u) {
return 0
}
return ((100u * currentPhaseBattlesWon) / totalBatles).toByte()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BENfcCharacter
if(!super.equals(other)) return false
if (trainingHp != other.trainingHp) return false
if (trainingAp != other.trainingAp) return false
if (trainingBp != other.trainingBp) return false
if (remainingTrainingTimeInMinutes != other.remainingTrainingTimeInMinutes) return false
if (itemEffectMentalStateValue != other.itemEffectMentalStateValue) return false
if (itemEffectMentalStateMinutesRemaining != other.itemEffectMentalStateMinutesRemaining) return false
if (itemEffectActivityLevelValue != other.itemEffectActivityLevelValue) return false
if (itemEffectActivityLevelMinutesRemaining != other.itemEffectActivityLevelMinutesRemaining) return false
if (itemEffectVitalPointsChangeValue != other.itemEffectVitalPointsChangeValue) return false
if (itemEffectVitalPointsChangeMinutesRemaining != other.itemEffectVitalPointsChangeMinutesRemaining) return false
if (abilityRarity != other.abilityRarity) return false
if (abilityType != other.abilityType) return false
if (abilityBranch != other.abilityBranch) return false
if (abilityReset != other.abilityReset) return false
if (rank != other.rank) return false
if (itemType != other.itemType) return false
if (itemMultiplier != other.itemMultiplier) return false
if (itemRemainingTime != other.itemRemainingTime) return false
if (!otp0.contentEquals(other.otp0)) return false
if (!otp1.contentEquals(other.otp1)) return false
if (characterCreationFirmwareVersion != other.characterCreationFirmwareVersion) return false
if (!appReserved1.contentEquals(other.appReserved1)) return false
if (!appReserved2.contentEquals(other.appReserved2)) return false
return true
}
override fun hashCode(): Int {
return Objects.hash(
super.hashCode(),
trainingHp,
trainingAp,
trainingBp,
remainingTrainingTimeInMinutes,
itemEffectMentalStateValue,
itemEffectMentalStateMinutesRemaining,
itemEffectActivityLevelValue,
itemEffectActivityLevelMinutesRemaining,
itemEffectVitalPointsChangeValue,
itemEffectVitalPointsChangeMinutesRemaining,
abilityRarity,
abilityType,
abilityBranch,
abilityReset,
rank,
itemType,
itemMultiplier,
itemRemainingTime,
otp0.contentHashCode(),
otp1.contentHashCode(),
characterCreationFirmwareVersion,
appReserved1.contentHashCode(),
appReserved2.contentHashCode())
}
@OptIn(ExperimentalStdlibApi::class)
override fun toString(): String {
return """
${super.toString()}
BENfcCharacter(
trainingHp=$trainingHp,
trainingAp=$trainingAp,
trainingBp=$trainingBp,
remainingTrainingTimeInMinutes=$remainingTrainingTimeInMinutes,
itemEffectMentalStateValue=$itemEffectMentalStateValue,
itemEffectMentalStateMinutesRemaining=$itemEffectMentalStateMinutesRemaining,
itemEffectActivityLevelValue=$itemEffectActivityLevelValue,
itemEffectActivityLevelMinutesRemaining=$itemEffectActivityLevelMinutesRemaining,
itemEffectVitalPointsChangeValue=$itemEffectVitalPointsChangeValue,
itemEffectVitalPointsChangeMinutesRemaining=$itemEffectVitalPointsChangeMinutesRemaining,
abilityRarity=$abilityRarity,
abilityType=$abilityType,
abilityBranch=$abilityBranch,
abilityReset=$abilityReset,
rank=$rank,
itemType=$itemType,
itemMultiplier=$itemMultiplier,
itemRemainingTime=$itemRemainingTime,
otp0=${otp0.toHexString()},
otp1=${otp1.toHexString()},
characterCreationFirmwareVersion=$characterCreationFirmwareVersion,
appReserved1=${appReserved1.contentToString()},
appReserved2=${appReserved2.contentToString()}
)"""
}
}

View File

@ -1,68 +0,0 @@
package com.github.cfogrady.vbnfc.be
import com.github.cfogrady.vbnfc.ChecksumCalculator
// Obsolete... being held onto for reference to device side data.
class BENfcDataFactory(checksumCalculator: ChecksumCalculator = ChecksumCalculator()) {
fun buildBENfcDevice(bytes: ByteArray): BENfcDevice {
return BENfcDevice(
gender = BENfcDevice.Gender.entries[bytes[12].toInt()],
registedDims = bytes.sliceArray(32..<32+15),
currDays = bytes[78]
)
// reserved1 = readByte(107)
// saveFirmwareVersion = readUShort(108)
// advMissionStage = readByte(128)
//
//
//
//
// reserved2 = readByte(140)
//
// reserved3 = readUShort(162)
// year = readByte(164)
// month = readByte(165)
// day = readByte(166)
// vitalPointsHistory0 = readUShort(176)
// vitalPointsHistory1 = readUShort(178)
// vitalPointsHistory2 = readUShort(180)
// vitalPointsHistory3 = readUShort(182)
// vitalPointsHistory4 = readUShort(184)
// vitalPointsHistory5 = readUShort(186)
// year0PreviousVitalPoints = readByte(188)
// month0PreviousVitalPoints = readByte(189)
// day0PreviousVitalPoints = readByte(190)
// year1PreviousVitalPoints = readByte(192)
// month1PreviousVitalPoints = readByte(193)
// day1PreviousVitalPoints = readByte(194)
// year2PreviousVitalPoints = readByte(195)
// month2PreviousVitalPoints = readByte(196)
// day2PreviousVitalPoints = readByte(197)
// year3PreviousVitalPoints = readByte(198)
// month3PreviousVitalPoints = readByte(199)
// day3PreviousVitalPoints = readByte(200)
// year4PreviousVitalPoints = readByte(201)
// month4PreviousVitalPoints = readByte(202)
// day4PreviousVitalPoints = readByte(203)
// year5PreviousVitalPoints = readByte(204)
// month5PreviousVitalPoints = readByte(205)
// day5PreviousVitalPoints = readByte(206)
//
// reserved4 = readUShort(262)
// reserved5 = readByte(264)
// questionMark = readByte(265)
// reserved6 = readByte(289)
// reserved7 = readByte(291)
//
// reserved8 = readUShort(297)
// reserved9 = readUShortArray(376, 2)
//
}
}

View File

@ -1,271 +0,0 @@
package com.github.cfogrady.vbnfc.be
import android.util.Log
import com.github.cfogrady.vbnfc.ChecksumCalculator
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.NfcDataTranslator
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.copyIntoUShortArray
import com.github.cfogrady.vbnfc.data.DeviceSubType
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.data.NfcHeader
import com.github.cfogrady.vbnfc.getUInt16
import com.github.cfogrady.vbnfc.toByteArray
import java.nio.ByteOrder
class BENfcDataTranslator(
override val cryptographicTransformer: CryptographicTransformer,
private val checksumCalculator: ChecksumCalculator = ChecksumCalculator()
): NfcDataTranslator {
companion object {
const val OPERATION_PAGE: Byte = 0x6
// CHARACTER
const val APP_RESERVED_START = 0
const val APP_RESERVED_SIZE = 12
const val INJURY_STATUS_IDX = 64
const val APP_RESERVED_2_START = 66
const val APP_RESERVED_2_SIZE = 3 //3 ushorts
const val CHARACTER_INDEX_IDX = 72
const val DIM_ID_IDX = 74
const val PHASE_IDX = 76
const val ATTRIBUTE_IDX = 77
const val AGE_IN_DAYS_IDX = 78 // always 0 on BE :(
const val TRAINING_PP_IDX = 96
const val CURRENT_BATTLES_WON_IDX = 98
const val CURRENT_BATTLES_LOST_IDX = 100
const val TOTAL_BATTLES_WON_IDX = 102
const val TOTAL_BATTLES_LOST_IDX = 104
const val WIN_PCT_IDX = 106 // unused
const val CHARACTER_CREATION_FIRMWARE_VERSION_IDX = 108
const val NEXT_ADVENTURE_MISSION_STAGE_IDX = 128
const val MOOD_IDX = 129
const val ACTIVITY_LEVEL_IDX = 130
const val HEART_RATE_CURRENT_IDX = 131
const val VITAL_POINTS_IDX = 132
const val ITEM_EFFECT_MENTAL_STATE_VALUE_IDX = 134
const val ITEM_EFFECT_MENTAL_STATE_MINUTES_REMAINING_IDX = 135
const val ITEM_EFFECT_ACTIVITY_LEVEL_VALUE_IDX = 136
const val ITEM_EFFECT_ACTIVITY_LEVEL_MINUTES_REMAINING_IDX = 137
const val ITEM_EFFECT_VITAL_POINTS_CHANGE_VALUE_IDX = 138
const val ITEM_EFFECT_VITAL_POINTS_CHANGE_MINUTES_REMAINING_IDX = 139
// 140 reserved
const val TRANSFORMATION_COUNT_DOWN_IDX = 141
const val TRANSFORMATION_HISTORY_START = 208
const val TRAINING_HP_IDX = 256
const val TRAINING_AP_IDX = 258
const val TRAINING_BP_IDX = 260
const val TRAINING_TIME_IDX = 266
const val RANK_IDX = 288
const val ABILITY_RARITY_IDX = 290
const val ABILITY_TYPE_IDX = 292
const val ABILITY_BRANCH_IDX = 294
const val ABILITY_RESET_IDX = 296
const val ITEM_TYPE_IDX = 299
const val ITEM_MULTIPLIER_IDX = 300
const val ITEM_REMAINING_TIME_IDX = 301
const val OTP_START_IDX = 352
const val OTP_END_IDX = 359
const val OTP2_START_IDX = 368
const val OTP2_END_IDX = 375
// DEVICE
const val VITAL_POINTS_CURRENT_IDX = 160
const val FIMRWARE_VERSION_IDX = 380
}
// setCharacterInByteArray takes the BENfcCharacter and modifies the byte array with character
// data. At the time of writing this is used to write a parsed character into fresh unparsed
// device data when sending a character back to the device.
override fun setCharacterInByteArray(
character: NfcCharacter,
bytes: ByteArray
) {
val beCharacter = character as BENfcCharacter
beCharacter.appReserved1.copyInto(bytes,
APP_RESERVED_START, 0,
APP_RESERVED_SIZE
)
beCharacter.injuryStatus.ordinal.toUShort().toByteArray(bytes,
INJURY_STATUS_IDX, ByteOrder.BIG_ENDIAN)
for(i in 0..<APP_RESERVED_2_SIZE) {
val index = APP_RESERVED_2_START + 2*i
beCharacter.appReserved2[i].toByteArray(bytes, index, ByteOrder.BIG_ENDIAN)
}
beCharacter.charIndex.toByteArray(bytes, CHARACTER_INDEX_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.dimId.toByteArray(bytes, DIM_ID_IDX, ByteOrder.BIG_ENDIAN)
bytes[PHASE_IDX] = beCharacter.stage
bytes[ATTRIBUTE_IDX] = beCharacter.attribute.ordinal.toByte()
bytes[AGE_IN_DAYS_IDX] = beCharacter.ageInDays
beCharacter.getTrainingPp().toByteArray(bytes, TRAINING_PP_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.currentPhaseBattlesWon.toByteArray(bytes,
CURRENT_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.currentPhaseBattlesLost.toByteArray(bytes,
CURRENT_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.totalBattlesWon.toByteArray(bytes,
TOTAL_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.totalBattlesLost.toByteArray(bytes,
TOTAL_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN)
bytes[WIN_PCT_IDX] = beCharacter.getWinPercentage()
bytes[NEXT_ADVENTURE_MISSION_STAGE_IDX] = beCharacter.nextAdventureMissionStage
bytes[MOOD_IDX] = beCharacter.mood
bytes[ACTIVITY_LEVEL_IDX] = beCharacter.activityLevel
bytes[HEART_RATE_CURRENT_IDX] = beCharacter.heartRateCurrent.toByte()
beCharacter.vitalPoints.toByteArray(bytes, VITAL_POINTS_IDX, ByteOrder.BIG_ENDIAN)
bytes[ITEM_EFFECT_MENTAL_STATE_VALUE_IDX] = beCharacter.itemEffectMentalStateValue
bytes[ITEM_EFFECT_MENTAL_STATE_MINUTES_REMAINING_IDX] = beCharacter.itemEffectMentalStateMinutesRemaining
bytes[ITEM_EFFECT_ACTIVITY_LEVEL_VALUE_IDX] = beCharacter.itemEffectActivityLevelValue
bytes[ITEM_EFFECT_ACTIVITY_LEVEL_MINUTES_REMAINING_IDX] = beCharacter.itemEffectActivityLevelMinutesRemaining
bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_VALUE_IDX] = beCharacter.itemEffectVitalPointsChangeValue
bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_MINUTES_REMAINING_IDX] = beCharacter.itemEffectVitalPointsChangeMinutesRemaining
beCharacter.transformationCountdown.toByteArray(bytes,
TRANSFORMATION_COUNT_DOWN_IDX, ByteOrder.BIG_ENDIAN)
transformationHistoryToByteArray(beCharacter.transformationHistory, bytes)
beCharacter.trainingHp.toByteArray(bytes, TRAINING_HP_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.trainingAp.toByteArray(bytes, TRAINING_AP_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.trainingBp.toByteArray(bytes, TRAINING_BP_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.remainingTrainingTimeInMinutes.toByteArray(bytes,
TRAINING_TIME_IDX, ByteOrder.BIG_ENDIAN)
bytes[ABILITY_RARITY_IDX] = beCharacter.abilityRarity.ordinal.toByte()
beCharacter.abilityType.toByteArray(bytes, ABILITY_TYPE_IDX, ByteOrder.BIG_ENDIAN)
beCharacter.abilityBranch.toByteArray(bytes, ABILITY_BRANCH_IDX, ByteOrder.BIG_ENDIAN)
bytes[ABILITY_RESET_IDX] = beCharacter.abilityReset
bytes[RANK_IDX] = beCharacter.rank
bytes[ITEM_TYPE_IDX] = beCharacter.itemType
bytes[ITEM_MULTIPLIER_IDX] = beCharacter.itemMultiplier
bytes[ITEM_REMAINING_TIME_IDX] = beCharacter.itemRemainingTime
beCharacter.otp0.copyInto(bytes, OTP_START_IDX, 0, beCharacter.otp0.size)
beCharacter.otp1.copyInto(bytes, OTP2_START_IDX, 0, beCharacter.otp1.size)
bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX] = beCharacter.characterCreationFirmwareVersion.majorVersion
bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX+1] = beCharacter.characterCreationFirmwareVersion.minorVersion
}
// finalizeByteArrayFormat finalizes the byte array for BE NFC format by setting all the
// checksums, and duplicating the duplicate memory pages.
override fun finalizeByteArrayFormat(bytes: ByteArray) {
checksumCalculator.recalculateChecksums(bytes)
performPageBlockDuplications(bytes)
}
override fun getOperationCommandBytes(header: NfcHeader, operation: Byte): ByteArray {
return byteArrayOf(TagCommunicator.NFC_WRITE_COMMAND, OPERATION_PAGE, header.status, operation, header.dimIdBytes[0], header.dimIdBytes[1])
}
// parses a BENfcCharacter from a ByteArray produced by the TagCommunicator
override fun parseNfcCharacter(bytes: ByteArray): BENfcCharacter {
return BENfcCharacter(
appReserved1 = bytes.sliceArray(APP_RESERVED_START..<(APP_RESERVED_START + APP_RESERVED_SIZE)),
injuryStatus = NfcCharacter.InjuryStatus.entries[bytes.getUInt16(INJURY_STATUS_IDX, ByteOrder.BIG_ENDIAN).toInt()],
appReserved2 = bytes.copyIntoUShortArray(APP_RESERVED_2_START, APP_RESERVED_2_SIZE),
charIndex = bytes.getUInt16(CHARACTER_INDEX_IDX, ByteOrder.BIG_ENDIAN),
dimId = bytes.getUInt16(DIM_ID_IDX, ByteOrder.BIG_ENDIAN),
stage = bytes[PHASE_IDX],
attribute = NfcCharacter.Attribute.entries[bytes[ATTRIBUTE_IDX].toInt()],
ageInDays = bytes[AGE_IN_DAYS_IDX],
trainingPp = bytes.getUInt16(TRAINING_PP_IDX, ByteOrder.BIG_ENDIAN),
currentPhaseBattlesWon = bytes.getUInt16(CURRENT_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN),
currentPhaseBattlesLost = bytes.getUInt16(CURRENT_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN),
totalBattlesWon = bytes.getUInt16(TOTAL_BATTLES_WON_IDX, ByteOrder.BIG_ENDIAN),
totalBattlesLost = bytes.getUInt16(TOTAL_BATTLES_LOST_IDX, ByteOrder.BIG_ENDIAN),
nextAdventureMissionStage = bytes[NEXT_ADVENTURE_MISSION_STAGE_IDX],
mood = bytes[MOOD_IDX],
activityLevel = bytes[ACTIVITY_LEVEL_IDX],
heartRateCurrent = bytes[HEART_RATE_CURRENT_IDX].toUByte(),
vitalPoints = bytes.getUInt16(VITAL_POINTS_IDX, ByteOrder.BIG_ENDIAN),
itemEffectMentalStateValue = bytes[ITEM_EFFECT_MENTAL_STATE_VALUE_IDX],
itemEffectMentalStateMinutesRemaining = bytes[ITEM_EFFECT_MENTAL_STATE_MINUTES_REMAINING_IDX],
itemEffectActivityLevelValue = bytes[ITEM_EFFECT_ACTIVITY_LEVEL_VALUE_IDX],
itemEffectActivityLevelMinutesRemaining = bytes[ITEM_EFFECT_ACTIVITY_LEVEL_MINUTES_REMAINING_IDX],
itemEffectVitalPointsChangeValue = bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_VALUE_IDX],
itemEffectVitalPointsChangeMinutesRemaining = bytes[ITEM_EFFECT_VITAL_POINTS_CHANGE_MINUTES_REMAINING_IDX],
transformationCountdownInMinutes = bytes.getUInt16(TRANSFORMATION_COUNT_DOWN_IDX, ByteOrder.BIG_ENDIAN),
transformationHistory = buildTransformationHistory(bytes),
trainingHp = bytes.getUInt16(TRAINING_HP_IDX, ByteOrder.BIG_ENDIAN),
trainingAp = bytes.getUInt16(TRAINING_AP_IDX, ByteOrder.BIG_ENDIAN),
trainingBp = bytes.getUInt16(TRAINING_BP_IDX, ByteOrder.BIG_ENDIAN),
remainingTrainingTimeInMinutes = bytes.getUInt16(TRAINING_TIME_IDX, ByteOrder.BIG_ENDIAN),
abilityRarity = NfcCharacter.AbilityRarity.entries[bytes[ABILITY_RARITY_IDX].toInt()],
abilityType = bytes.getUInt16(ABILITY_TYPE_IDX, ByteOrder.BIG_ENDIAN),
abilityBranch = bytes.getUInt16(ABILITY_BRANCH_IDX, ByteOrder.BIG_ENDIAN),
abilityReset = bytes[ABILITY_RESET_IDX],
rank = bytes[RANK_IDX],
itemType = bytes[ITEM_TYPE_IDX],
itemMultiplier = bytes[ITEM_MULTIPLIER_IDX],
itemRemainingTime = bytes[ITEM_REMAINING_TIME_IDX],
otp0 = bytes.sliceArray(OTP_START_IDX..OTP_END_IDX),
otp1 = bytes.sliceArray(OTP2_START_IDX..OTP2_END_IDX),
characterCreationFirmwareVersion = FirmwareVersion(
majorVersion = bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX],
minorVersion = bytes[CHARACTER_CREATION_FIRMWARE_VERSION_IDX+1]),
)
}
override fun parseHeader(headerBytes: ByteArray): NfcHeader {
Log.i(TagCommunicator.TAG, "Bytes in header: ${headerBytes.size}")
val header = NfcHeader(
deviceId = DeviceType.VitalBraceletBEDeviceType,
deviceSubType = DeviceSubType.Original,
vbCompatibleTagIdentifier = headerBytes.sliceArray(0..3), // this is a magic number used to verify that the tag is a VB.
status = headerBytes[8],
operation = headerBytes[9],
dimIdBytes = headerBytes.sliceArray(10..11),
appFlag = headerBytes[12],
nonce = headerBytes.sliceArray(13..15)
)
Log.i(TagCommunicator.TAG, "Header: $header")
return header
}
// a block being 4 pages
private val firstIndicesOfBlocksToCopy = intArrayOf(32, 64, 96, 128, 256, 416)
private fun performPageBlockDuplications(data: ByteArray) {
for (firstIndex in firstIndicesOfBlocksToCopy) {
for (i in firstIndex..firstIndex + 15) {
data[i+16] = data[i]
}
}
}
private fun transformationHistoryToByteArray(transformationHistory: Array<NfcCharacter.Transformation>, bytes: ByteArray) {
if (transformationHistory.size != 8) {
throw IllegalArgumentException("Transformation History must be exactly size 8")
}
for (phase in 0..<transformationHistory.size) {
var rootIdx = phase*4 + TRANSFORMATION_HISTORY_START
if (phase > 2) {
rootIdx += 4 // we skip 220-223 for some reason
}
if (phase > 5) {
rootIdx += 4 // we skip 236-239 for some reason
}
bytes[rootIdx] = transformationHistory[phase].toCharIndex
bytes[rootIdx+1] = transformationHistory[phase].yearsSince1988
bytes[rootIdx+2] = transformationHistory[phase].month
bytes[rootIdx+3] = transformationHistory[phase].day
}
}
private fun buildTransformationHistory(data: ByteArray): Array<NfcCharacter.Transformation> {
val transformationHistory = Array<NfcCharacter.Transformation>(8) { phase ->
var rootIdx = phase*4 + TRANSFORMATION_HISTORY_START
if (phase > 2) {
rootIdx += 4 // we skip 220-223 for some reason
}
if (phase > 5) {
rootIdx += 4 // we skip 236-239 for some reason
}
NfcCharacter.Transformation(
toCharIndex = data[rootIdx],
yearsSince1988 = data[rootIdx+1],
month = data[rootIdx+2],
day = data[rootIdx+3]
)
}
return transformationHistory
}
}

View File

@ -1,20 +0,0 @@
package com.github.cfogrady.vbnfc.be
import com.github.cfogrady.vbnfc.data.NfcDevice
import java.util.BitSet
class BENfcDevice(
val gender: Gender,
val registedDims: ByteArray,
val currDays: Byte,
):
NfcDevice(
BitSet(),
) {
enum class Gender {
Male,
Female
}
}

View File

@ -1,3 +0,0 @@
package com.github.cfogrady.vbnfc.be
data class FirmwareVersion(val majorVersion: Byte, val minorVersion: Byte)

View File

@ -1,12 +0,0 @@
package com.github.cfogrady.vbnfc.data
class DeviceSubType {
companion object {
const val Original: UShort = 1u
const val FirstRevision: UShort = 2u
const val SecondRevision: UShort = 3u
const val DigiviceV: UShort = 4u
const val DigiviceVSecondRevision: UShort = 5u
const val VitalHero: UShort = 6u
}
}

View File

@ -1,9 +0,0 @@
package com.github.cfogrady.vbnfc.data
class DeviceType {
companion object {
const val VitalSeriesDeviceType: UShort = 2u
const val VitalCharactersDeviceType: UShort = 3u
const val VitalBraceletBEDeviceType: UShort = 4u
}
}

View File

@ -1,147 +0,0 @@
package com.github.cfogrady.vbnfc.data
import java.util.Objects
open class NfcCharacter(
val dimId: UShort,
var charIndex: UShort,
var stage: Byte,
var attribute: Attribute,
var ageInDays: Byte,
var nextAdventureMissionStage: Byte, // next adventure mission stage on the character's dim
var mood: Byte,
var vitalPoints: UShort,
var transformationCountdown: UShort,
var injuryStatus: InjuryStatus,
var trophies: UShort,
var currentPhaseBattlesWon: UShort,
var currentPhaseBattlesLost: UShort,
var totalBattlesWon: UShort,
var totalBattlesLost: UShort,
var activityLevel: Byte,
var heartRateCurrent: UByte,
var transformationHistory: Array<Transformation>
) {
data class Transformation(
val toCharIndex: Byte,
val yearsSince1988: Byte,
val month: Byte,
val day: Byte)
enum class AbilityRarity {
None,
Common,
Rare,
SuperRare,
SuperSuperRare,
UltraRare,
}
enum class Attribute {
None,
Virus,
Data,
Vaccine,
Free
}
enum class InjuryStatus {
None,
Injury,
InjuryHealed,
InjuryTwo,
InjuryTwoHealed,
InjuryThree,
InjuryThreeHealed,
InjuryFour,
}
fun getTransformationHistoryString(separator: String = System.lineSeparator()): String {
val builder = StringBuilder()
for(i in transformationHistory.indices) {
builder.append(transformationHistory[i])
if(i != transformationHistory.size-1) {
builder.append(separator)
}
}
return builder.toString()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as NfcCharacter
if (dimId != other.dimId) return false
if (charIndex != other.charIndex) return false
if (stage != other.stage) return false
if (attribute != other.attribute) return false
if (ageInDays != other.ageInDays) return false
if (nextAdventureMissionStage != other.nextAdventureMissionStage) return false
if (mood != other.mood) return false
if (vitalPoints != other.vitalPoints) return false
if (transformationCountdown != other.transformationCountdown) return false
if (injuryStatus != other.injuryStatus) return false
if (trophies != other.trophies) return false
if (currentPhaseBattlesWon != other.currentPhaseBattlesWon) return false
if (currentPhaseBattlesLost != other.currentPhaseBattlesLost) return false
if (totalBattlesWon != other.totalBattlesWon) return false
if (totalBattlesLost != other.totalBattlesLost) return false
if (activityLevel != other.activityLevel) return false
if (heartRateCurrent != other.heartRateCurrent) return false
if (!transformationHistory.contentEquals(other.transformationHistory)) return false
return true
}
override fun hashCode(): Int {
return Objects.hash(
dimId,
charIndex,
stage,
attribute,
ageInDays,
nextAdventureMissionStage,
mood,
vitalPoints,
transformationCountdown,
injuryStatus,
trophies,
currentPhaseBattlesWon,
currentPhaseBattlesLost,
totalBattlesWon,
totalBattlesLost,
activityLevel,
heartRateCurrent,
transformationHistory.contentHashCode()
)
}
override fun toString(): String {
return """NfcCharacter(
dimId=$dimId,
charIndex=$charIndex,
stage=$stage,
attribute=$attribute,
ageInDays=$ageInDays,
nextAdventureMissionStage=$nextAdventureMissionStage,
mood=$mood,
vitalPoints=$vitalPoints,
transformationCountdown=$transformationCountdown,
injuryStatus=$injuryStatus,
trophies=$trophies,
currentPhaseBattlesWon=$currentPhaseBattlesWon,
currentPhaseBattlesLost=$currentPhaseBattlesLost,
totalBattlesWon=$totalBattlesWon,
totalBattlesLost=$totalBattlesLost,
activityLevel=$activityLevel,
heartRateCurrent=$heartRateCurrent,
transformationHistory=${transformationHistory.contentToString()}
)"""
}
}

View File

@ -1,3 +0,0 @@
package com.github.cfogrady.vbnfc.data
class NfcData(val nfcCharacter: NfcCharacter, val nfcDevice: NfcDevice)

View File

@ -1,9 +0,0 @@
package com.github.cfogrady.vbnfc.data
import java.util.BitSet
open class NfcDevice(private val registeredDims: BitSet) {
fun isDimRegistered(dimId: UShort): Boolean {
return registeredDims[dimId.toInt()]
}
}

View File

@ -1,26 +0,0 @@
package com.github.cfogrady.vbnfc.data
import com.github.cfogrady.vbnfc.NfcDataTranslator
import com.github.cfogrady.vbnfc.getUInt16
import com.github.cfogrady.vbnfc.toByteArray
import java.nio.ByteOrder
open class NfcHeader (
val deviceId: UShort,
val deviceSubType: UShort,
val vbCompatibleTagIdentifier: ByteArray, // this is a magic number used to verify that the tag is a VB.
val status: Byte,
val operation: Byte,
var dimIdBytes: ByteArray,
val appFlag: Byte,
val nonce: ByteArray,
) {
fun getDimId(): UShort {
return dimIdBytes.getUInt16(0, ByteOrder.BIG_ENDIAN)
}
fun setDimId(dimId: UShort) {
dimIdBytes = dimId.toByteArray(ByteOrder.BIG_ENDIAN)
}
}

View File

@ -1,45 +0,0 @@
package com.github.cfogrady.vbnfc.vb
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.NfcDataTranslator
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.data.DeviceSubType
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcCharacter
import com.github.cfogrady.vbnfc.data.NfcHeader
import com.github.cfogrady.vbnfc.getUInt16
class VBNfcDataTranslator(override val cryptographicTransformer: CryptographicTransformer) : NfcDataTranslator {
companion object {
const val OPERATION_PAGE: Byte = 0x6
}
override fun finalizeByteArrayFormat(bytes: ByteArray) {
TODO("Not yet implemented")
}
override fun getOperationCommandBytes(header: NfcHeader, operation: Byte): ByteArray {
val vbHeader = header as VBNfcHeader
return byteArrayOf(TagCommunicator.NFC_WRITE_COMMAND, OPERATION_PAGE, header.status, header.dimIdBytes[1], operation, vbHeader.reserved)
}
override fun parseNfcCharacter(bytes: ByteArray): NfcCharacter {
TODO("Not yet implemented")
}
override fun parseHeader(headerBytes: ByteArray): NfcHeader {
val header = VBNfcHeader(
deviceType = DeviceType.VitalSeriesDeviceType,
deviceSubType = headerBytes.getUInt16(6),
vbCompatibleTagIdentifier = headerBytes.sliceArray(0..3), // this is a magic number used to verify that the tag is a VB.
status = headerBytes[8],
dimId = headerBytes[9],
operation = headerBytes[10],
reserved = headerBytes[11],
appFlag = headerBytes[12],
nonce = headerBytes.sliceArray(13..15)
)
return header
}
}

View File

@ -1,25 +0,0 @@
package com.github.cfogrady.vbnfc.vb
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.cfogrady.vbnfc.data.NfcHeader
class VBNfcHeader(
deviceType: UShort,
deviceSubType: UShort,
vbCompatibleTagIdentifier: ByteArray,
status: Byte,
operation: Byte,
dimId: Byte,
val reserved: Byte,
appFlag: Byte,
nonce: ByteArray
) : NfcHeader(
deviceId = deviceType,
deviceSubType = deviceSubType,
vbCompatibleTagIdentifier = vbCompatibleTagIdentifier,
status = status,
operation = operation,
dimIdBytes = byteArrayOf(0, dimId),
appFlag = appFlag,
nonce = nonce,
)

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="substitutionArray" />
</resources>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="password1" />
<string name="password2" />
<string name="decryptionKey" />
</resources>

View File

@ -1,50 +0,0 @@
package com.github.cfogrady.vbnfc
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
// This class allows for creating new test keys
class CryptographicTransformerHelper {
companion object {
fun generateAesKey(): String {
val combinedKey = ByteArray(24)
for (i in combinedKey.indices) {
combinedKey[i] = Random.nextInt(48, 58 + 26).toByte()
if(combinedKey[i] > 57) {
combinedKey[i] = (combinedKey[i] + 7).toByte()
}
}
return combinedKey.toString(StandardCharsets.UTF_8)
}
fun generateHMacKey(aesKey: String, hmacKey: String = generateRandomPlainTextHmacKey()): String {
val hmacKeyData = hmacKey.toByteArray(StandardCharsets.UTF_8)
val encryptedHmacKey = encryptAesCbcPkcs5Padding(aesKey, hmacKeyData)
return Base64.getEncoder().encodeToString(encryptedHmacKey)
}
private fun generateRandomPlainTextHmacKey(): String {
val key = ByteArray(4)
for (i in key.indices) {
key[i] = Random.nextInt(33, 126).toByte()
}
return key.toString(StandardCharsets.UTF_8)
}
private fun encryptAesCbcPkcs5Padding(key: String, data: ByteArray): ByteArray {
val keyBytes = key.toByteArray(StandardCharsets.UTF_8)
val rightSizedKey = keyBytes.copyOf(32)
val ivBytes = keyBytes.copyOfRange(key.length - 16, key.length)
val secretKeySpec = SecretKeySpec(rightSizedKey, "AES")
val ivParameterSpec = IvParameterSpec(ivBytes)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
return cipher.doFinal(data)
}
}
}

View File

@ -1,56 +0,0 @@
package com.github.cfogrady.vbnfc
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Assert
import org.junit.Test
class CryptographicTransformerTest {
val testTagId = byteArrayOf(0x04, 0x40, 0xaf.toByte(), 0xa2.toByte(), 0xee.toByte(), 0x0f, 0x90.toByte())
val testAesKey = "8A4PEGIXJS454EFRTX9F5PCT"
val testHmacKey1 = "40nz2LdPI99D+x748XmQmw=="
val testHmacKey2 = "5Jz9lWtNg28qxqIBoR5kLw=="
val testSubstitutionCipher = intArrayOf(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
@OptIn(ExperimentalStdlibApi::class)
@Test
fun createPasswordCreatesExpectedPassword() {
mockkStatic(android.util.Log::class)
every { android.util.Log.i(any<String>(), any<String>()) } answers {
val message = it.invocation.args[1] as String
println(message)
1
}
val cryptographicTransformer = CryptographicTransformer(testHmacKey1, testHmacKey2, testAesKey, testSubstitutionCipher)
val result = cryptographicTransformer.createNfcPassword(testTagId)
val expected = "a3c83dd7"
Assert.assertEquals(expected, result.toHexString())
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun dataEncryptionAndDecryptionWorks() {
val cryptographicTransformer = CryptographicTransformer(testHmacKey1, testHmacKey2, testAesKey, testSubstitutionCipher)
val characters = listOf(
"000000000000000000000000000000000000000000000000000000000000000010400010040010001000000000001094104000100400100010000000000010940000000000000000000400840203008d0000000000000000000400840203008d00000006000300060003000001010014000000060003000600030000010100140156025309850000000000000000d6100156025309850000000000000000d610000000000000280000000000000000280000000000000000030c08302402279424022624022524022424022324020151002402290124022904240229000000f2ffffffffffffffffffffffff000000f4ffffffffffffffff00000000000000f80000000000000000000014860000009a0000000000000000000014860000009a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010101000000000000008702020202020202020000000001010033000000000000000000000000000000000000000000000000000000000000000014c5400000000000000000000000001914c540000000000000000000000000190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"000000000000000000000000000000000000000000000000000000000000000010400010040010001000000000001094104000100400100010000000000010940000000000000000000500820302008c0000000000000000000500820302008c000b00030002000b000b000001010028000b00030002000b000b0000010100280464028b04b4000000000000000308b80464028b04b4000000000000000308b80474000000001500000000000000008d0002000000000000000000002401062d240105240104240103240102240101c80024010601240106042401060000008605240116ffffffffffffffff00000038ffffffffffffffff00000000000000f80005000f000a00000000046b0000008d0005000f000a00000000046b0000008d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030303030303030300000000000000210404040404040404000000000101003a000000000000000000000000000000000000000000000000000000000000000014c5400000000000000000000000001914c540000000000000000000000000190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
)
val expectedEncryptions = listOf(
"74b46e2d78292ce251a84e644f5451dd4aa2c6d782a9aeef2c3229103c7a26e5d9c2c53d2f63077a1cf9ae18efc382edca9e50d7d861e2927cd9df2494a0772ff1b8791bae7883a862d218559277db6a9665f18da0f1caa92ccc903dbc80e66367ee39b2f243cc0b5bfcc2131dfeb9d361179d4033517c4ebbceea15ab9bfb27a9a05cb45e5dfe37287a010db3bcfd9a6f1cc54a31ea7da193dbd341e83997ad80713d5fd19c7d76a6dbedfb25049ab5472e4094d0949f19c853b2ac6c6827412904fd1683fae61ea1a64508e9243e04bdfdee63ff98321204fd4fd2a17112c0bd5aa7c11986ea7bc00110dedea2d0644ea5ea3f6e928f8c6c98107eeedbfd1b774d07df237dc294f3b8fccaa8ab2e1c59de2c695f367214f55accc2e43ca0b2c021161d3c8b9ea2405717d6884d089d848d82b37a9ed711a8e9809336d05bab0091face97671e9260d4ae741e94ef1f8d950ff390dd05bfa28adb6b72a8214192be530af26db6abda69dce0707d3d92f44beb1f01f24d7c1123ad25d37349bf7d5777723f1acb56d2f96ba435ffa505c3ae3d270c85bcb64aabac0d15d8128f7fe34cc303a74f8d42871a25d452a1ef033b824b17e51b808b5363f2f1da5a57c3fcc881b57028e76155f6d49e35e43bd142747c3a4a14217fa8600a6e0d3cdbc6b5598510023ad0897d38ca04e78a78e198ee76e784d6337ad8fa65e4b617ea11433fb3d9d7d3269a7b5e1ff9a4cd723f3fc4f81768816619fadf21b5276f31d704bba1a3a9afb0363cca3ddb272bdced2d252e21824d2828b7c36cdd37202cd5a6b5224e505f87188d3c63af33c8916aa8ae0116dd1028c370236591a4413559fe40d14dcdf72959a122def9b4c6cf5d928142a66b0dcf0ffa5d447b53a8661e3578a49d632e0285d180814116a3e78fc00fc01106a248af8afa329a69054b827e41a62abdc65074554fa2f07b773d56bae73252efa374f47397c1d36e056eb9d32b6dcf18f40669a5b23723c475c50c5ba4154b550c67dd90d4a6919686c69fdcfd7d4d988a0df5e420b6240068ae8aa07c88c38bac3b6ce7a87c8df0f540cc3c5be2180b70127c523102ed9be2cfb25a032089d47d05db6dbde2758239ad54b94756906922ed7e7c4ecf6b256c658417fa8003100c72de1dee627c1a0c670dbc91596e4dc0d4393eb24884b41979e8ff3d5b0583416750d1d856a2a689cd",
"74b46e2d78292ce251a84e644f5451dd4aa2c6d782a9aeef2c3229103c7a26e5d9c2c53d2f63077a1cf9ae18efc382edca9e50d7d861e2927cd9df2494a0772ff1b8791bae7883a862d318539376db6b9665f18da0f1caa92ccd903bbd81e66267e539b7f242cc065bf4c2131dfeb9ef611c9d4533507c43bbc6ea15ab9bfb1bac925c6c536cfe37287a010db3bf23326a2ec5923cdb7da193dbd341e83a490584053d5fd19c4076a6dbedfb25049a10472c4094d0949f19cb5fba9c6c6b06f82907de1680dbe61d86a64629e9273e9dbdfded4cff98313d04fd4cfda17112b4478159281986ea7bc00110dedea2d0a84ea5ea3f6e928f8c6c98107eeedbfd1b774807d02377c294f3b8ec27a8ab2e0b59db2c665f3c7214f55adc2fe43ca0a5c021161d3c8b9ea2405717d6884d089d848d82b37a9ed711a8e9809336d05bab0091face97671e9260d4ae741e94ef1f8d950ff390dd05bfa28adb6b72a8214190bc5108f06fb4a9da69dce0707d3d34f24ded1907f44b7a1123ad25d37349b67d5777723f1acb56d2f96ba435ffa505c3ae3d270c85bcb64aabac0d15d8128f7fe34cc303a74f8d42871a25d452a1ef033b824b17e51b808b5363f2f1da5a57c3fcc881b57028e76155f6d49e35e43bd142747c3a4a14217fa8600a6e0d3cdbc6b5598510023ad0897d38ca04e78a78e198ee76e784d6337ad8fa65e4b617ea11433fb3d9d7d3269a7b5e1ff9a4cd723f3fc4f81768816619fadf21b5276f31d704bba1a3a9afb0363cca3ddb272bdced2d252e21824d2828b7c36cdd37202cd5a6b5224e505f87188d3c63af33c8916aa8ae0116dd1028c370236591a4413559fe40d14dcdf72959a122def9b4c6cf5d928142a66b0dcf0ffa5d447b53a8661e3578a49d632e0285d180814116a3e78fc00fc01106a248af8afa329a69054b827e41a62abdc65074554fa2f07b773d56bae73252efa374f47397c1d36e056eb9d32b6dcf18f40669a5b23723c475c50c5ba4154b550c67dd90d4a6919686c69fdcfd7d4d988a0df5e420b6240068ae8aa07c88c38bac3b6ce7a87c8df0f540cc3c5be2180b70127c523102ed9be2cfb25a032089d47d05db6dbde2758239ad54b94756906922ed7e7c4ecf6b256c658417fa8003100c72de1dee627c1a0c670dbc91596e4dc0d4393eb24884b41979e8ff3d5b0583416750d1d856a2a689cd",
)
for (i in characters.indices) {
val encrypted = cryptographicTransformer.encryptData(characters[i].hexToByteArray(), testTagId)
Assert.assertEquals(expectedEncryptions[i], encrypted.toHexString())
val decrypted = cryptographicTransformer.decryptData(encrypted, testTagId)
Assert.assertEquals(characters[i], decrypted.toHexString())
}
}
}

View File

@ -1,73 +0,0 @@
package com.github.cfogrady.vbnfc.be
import com.github.cfogrady.vbnfc.ChecksumCalculator
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.data.NfcCharacter
import io.mockk.mockkClass
import org.junit.Assert
import org.junit.Test
class BENfcDataTranslatorTest {
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testNfcCharacterParsing() {
val nfcBytes = "000000000000000000000000000000000000000000000000000000000000000010400010040010001000000000001094104000100400100010000000000010940000000000000000000500820302008c0000000000000000000500820302008c000b00030002000b000b000001010028000b00030002000b000b0000010100280464028b04b4000000000000000308b80464028b04b4000000000000000308b80474000000001500000000000000008d0002000000000000000000002401062d240105240104240103240102240101c80024010601240106042401060000008605240116ffffffffffffffff00000038ffffffffffffffff00000000000000f80005000f000a00000000046b0000008d0005000f000a00000000046b0000008d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010100000000000000210202020202020202000000000101003a000000000000000000000000000000000000000000000000000000000000000014c5400000000000000000000000001914c540000000000000000000000000190000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".hexToByteArray()
val mockCryptographicTransformer = mockkClass(CryptographicTransformer::class)
val checksumCalculator = ChecksumCalculator()
val beNfcDataTranslator = BENfcDataTranslator(mockCryptographicTransformer, checksumCalculator)
val character = beNfcDataTranslator.parseNfcCharacter(nfcBytes)
val expectedCharacter = BENfcCharacter(
dimId = 130u,
charIndex = 5u,
stage = 3,
attribute = NfcCharacter.Attribute.Data,
ageInDays = 0,
mood = 100,
characterCreationFirmwareVersion = FirmwareVersion(1, 1),
nextAdventureMissionStage = 4,
vitalPoints = 1204u,
transformationCountdownInMinutes = 776u,
injuryStatus = NfcCharacter.InjuryStatus.None,
trainingPp = 11u,
currentPhaseBattlesWon = 3u,
currentPhaseBattlesLost = 2u,
totalBattlesWon = 11u,
totalBattlesLost = 11u,
activityLevel = 2,
heartRateCurrent = 139u,
transformationHistory = arrayOf(NfcCharacter.Transformation(0, 36, 1, 6),
NfcCharacter.Transformation(1, 36, 1, 6),
NfcCharacter.Transformation(4, 36, 1, 6),
NfcCharacter.Transformation(5, 36, 1, 22),
NfcCharacter.Transformation(-1, -1, -1, -1),
NfcCharacter.Transformation(-1, -1, -1, -1),
NfcCharacter.Transformation(-1, -1, -1, -1),
NfcCharacter.Transformation(-1, -1, -1, -1),
),
trainingHp = 5u,
trainingAp = 15u,
trainingBp = 10u,
remainingTrainingTimeInMinutes = 1131u,
itemEffectMentalStateValue = 0,
itemEffectMentalStateMinutesRemaining = 0,
itemEffectActivityLevelValue = 0,
itemEffectActivityLevelMinutesRemaining = 0,
itemEffectVitalPointsChangeValue = 0,
itemEffectVitalPointsChangeMinutesRemaining = 0,
abilityRarity = NfcCharacter.AbilityRarity.None,
abilityType = 0u,
abilityBranch = 0u,
abilityReset = 0,
rank = 0,
itemType = 0,
itemMultiplier = 0,
itemRemainingTime = 0,
appReserved1 = byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
appReserved2 = arrayOf(0u, 0u, 0u),
otp0 = "0101010101010101".hexToByteArray(),
otp1 = "0202020202020202".hexToByteArray()
)
Assert.assertEquals(expectedCharacter, character)
}
}