~cytrogen/vbhelper

01e82a6a44fe3837ac66ebc743403980ebb82252 — nacabaro 1 year, 3 months ago bc02689 + d9e492e
Merge pull request #5 from cfogrady/nfc-reader-from-maven

Pull Nfc Reader Library from dependency repo instead of as second module.
41 files changed, 48 insertions(+), 1982 deletions(-)

A .gitignore
A README.md
M app/build.gradle.kts
M gradle/libs.versions.toml
M settings.gradle.kts
D vb-nfc-reader/.gitignore
D vb-nfc-reader/.idea/.gitignore
D vb-nfc-reader/.idea/caches/deviceStreaming.xml
D vb-nfc-reader/.idea/gradle.xml
D vb-nfc-reader/.idea/misc.xml
D vb-nfc-reader/.idea/runConfigurations.xml
D vb-nfc-reader/build.gradle.kts
D vb-nfc-reader/consumer-rules.pro
D vb-nfc-reader/proguard-rules.pro
D vb-nfc-reader/src/androidTest/java/com/github/cfogrady/vbnfc/ExampleInstrumentedTest.kt
D vb-nfc-reader/src/main/AndroidManifest.xml
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ByteManipulation.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ChecksumCalculator.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/CryptographicTransformer.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslator.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslatorFactory.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/TagCommunicator.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/Utils.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcCharacter.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataFactory.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslator.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDevice.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/FirmwareVersion.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceSubType.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceType.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcCharacter.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcData.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcDevice.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcHeader.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcDataTranslator.kt
D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcHeader.kt
D vb-nfc-reader/src/main/res/values/arrays.xml
D vb-nfc-reader/src/main/res/values/strings.xml
D vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerHelper.kt
D vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerTest.kt
D vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslatorTest.kt
A .gitignore => .gitignore +8 -0
@@ 0,0 1,8 @@

.gradle/

.idea/

local.properties

app/src/main/res/values/keys.xml

A README.md => README.md +36 -0
@@ 0,0 1,36 @@
# VBHelper

## Developer Setup

1. Clone vb-nfc-reader (https://github.com/cfogrady/lib-vb-nfc)
2. Run vb-nfc-reader/publishToMavenLocal gradle task.
3. Create res/values/keys.xml within the app module.
4. Populate with:
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="password1">beHmacKey1</string>
    <string name="password2">beHmacKey2</string>
    <string name="decryptionKey">aesKey</string>
    <integer-array name="substitutionArray">
        <item>0</item>
        <item>1</item>
        <item>2</item>
        <item>3</item>
        <item>4</item>
        <item>5</item>
        <item>6</item>
        <item>7</item>
        <item>8</item>
        <item>9</item>
        <item>10</item>
        <item>11</item>
        <item>12</item>
        <item>13</item>
        <item>14</item>
        <item>15</item>
    </integer-array>
</resources>
```
5. Replace the values in the keys.xml file with those extracted from the original app.
6. Run
\ No newline at end of file

M app/build.gradle.kts => app/build.gradle.kts +1 -1
@@ 42,7 42,7 @@ android {

dependencies {
    implementation(libs.androidx.room.runtime)
    implementation(project(":vb-nfc-reader"))
    implementation(libs.vb.nfc.reader)
    ksp(libs.androidx.room.compiler)
    annotationProcessor(libs.androidx.room.compiler)
    implementation(libs.androidx.core.ktx)

M gradle/libs.versions.toml => gradle/libs.versions.toml +2 -0
@@ 9,6 9,7 @@ lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
composeBom = "2024.04.01"
roomRuntime = "2.6.1"
vbNfcReader = "0.1.0"

[libraries]
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-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
vb-nfc-reader = { module = "com.github.cfogrady:vb-nfc-reader", version.ref = "vbNfcReader" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

M settings.gradle.kts => settings.gradle.kts +1 -1
@@ 14,6 14,7 @@ pluginManagement {
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenLocal()
        google()
        mavenCentral()
    }


@@ 21,4 22,3 @@ dependencyResolutionManagement {

rootProject.name = "VBHelper"
include(":app")
include(":vb-nfc-reader")

D vb-nfc-reader/.gitignore => vb-nfc-reader/.gitignore +0 -1
@@ 1,1 0,0 @@
/build
\ No newline at end of file

D vb-nfc-reader/.idea/.gitignore => vb-nfc-reader/.idea/.gitignore +0 -3
@@ 1,3 0,0 @@
# Default ignored files
/shelf/
/workspace.xml

D vb-nfc-reader/.idea/caches/deviceStreaming.xml => vb-nfc-reader/.idea/caches/deviceStreaming.xml +0 -340
@@ 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>
\ No newline at end of file

D vb-nfc-reader/.idea/gradle.xml => vb-nfc-reader/.idea/gradle.xml +0 -13
@@ 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>
\ No newline at end of file

D vb-nfc-reader/.idea/misc.xml => vb-nfc-reader/.idea/misc.xml +0 -9
@@ 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>
\ No newline at end of file

D vb-nfc-reader/.idea/runConfigurations.xml => vb-nfc-reader/.idea/runConfigurations.xml +0 -17
@@ 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>
\ No newline at end of file

D vb-nfc-reader/build.gradle.kts => vb-nfc-reader/build.gradle.kts +0 -44
@@ 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")
}
\ No newline at end of file

D vb-nfc-reader/consumer-rules.pro => vb-nfc-reader/consumer-rules.pro +0 -0
D vb-nfc-reader/proguard-rules.pro => vb-nfc-reader/proguard-rules.pro +0 -21
@@ 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
\ No newline at end of file

D vb-nfc-reader/src/androidTest/java/com/github/cfogrady/vbnfc/ExampleInstrumentedTest.kt => vb-nfc-reader/src/androidTest/java/com/github/cfogrady/vbnfc/ExampleInstrumentedTest.kt +0 -24
@@ 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)
    }
}
\ No newline at end of file

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

</manifest>
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ByteManipulation.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ByteManipulation.kt +0 -78
@@ 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
}

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ChecksumCalculator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/ChecksumCalculator.kt +0 -41
@@ 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)
            }
        }
    }
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/CryptographicTransformer.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/CryptographicTransformer.kt +0 -110
@@ 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
    }
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslator.kt +0 -30
@@ 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

}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslatorFactory.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/NfcDataTranslatorFactory.kt +0 -22
@@ 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
    }
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/TagCommunicator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/TagCommunicator.kt +0 -193
@@ 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)
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/Utils.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/Utils.kt +0 -20
@@ 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
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcCharacter.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcCharacter.kt +0 -183
@@ 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()}
)"""
    }


}

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataFactory.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataFactory.kt +0 -68
@@ 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)
//
    }

}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslator.kt +0 -271
@@ 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
    }
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDevice.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/BENfcDevice.kt +0 -20
@@ 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
    }
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/FirmwareVersion.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/be/FirmwareVersion.kt +0 -3
@@ 1,3 0,0 @@
package com.github.cfogrady.vbnfc.be

data class FirmwareVersion(val majorVersion: Byte, val minorVersion: Byte)
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceSubType.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceSubType.kt +0 -12
@@ 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
    }
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceType.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/DeviceType.kt +0 -9
@@ 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
    }
}

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcCharacter.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcCharacter.kt +0 -147
@@ 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()}
)"""
    }


}

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcData.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcData.kt +0 -3
@@ 1,3 0,0 @@
package com.github.cfogrady.vbnfc.data

class NfcData(val nfcCharacter: NfcCharacter, val nfcDevice: NfcDevice)

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcDevice.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcDevice.kt +0 -9
@@ 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()]
    }
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcHeader.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/data/NfcHeader.kt +0 -26
@@ 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)
    }
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcDataTranslator.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcDataTranslator.kt +0 -45
@@ 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
    }
}
\ No newline at end of file

D vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcHeader.kt => vb-nfc-reader/src/main/java/com/github/cfogrady/vbnfc/vb/VBNfcHeader.kt +0 -25
@@ 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,
)
\ No newline at end of file

D vb-nfc-reader/src/main/res/values/arrays.xml => vb-nfc-reader/src/main/res/values/arrays.xml +0 -4
@@ 1,4 0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="substitutionArray" />
</resources>
\ No newline at end of file

D vb-nfc-reader/src/main/res/values/strings.xml => vb-nfc-reader/src/main/res/values/strings.xml +0 -6
@@ 1,6 0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="password1" />
    <string name="password2" />
    <string name="decryptionKey" />
</resources>
\ No newline at end of file

D vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerHelper.kt => vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerHelper.kt +0 -50
@@ 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)
        }
    }
}
\ No newline at end of file

D vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerTest.kt => vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/CryptographicTransformerTest.kt +0 -56
@@ 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())
        }
    }
}
\ No newline at end of file

D vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslatorTest.kt => vb-nfc-reader/src/test/java/com/github/cfogrady/vbnfc/be/BENfcDataTranslatorTest.kt +0 -73
@@ 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)
    }
}
\ No newline at end of file