Initial commit

This commit is contained in:
Nacho 2025-01-04 01:12:57 +01:00
parent 4e3ebd43f4
commit 84a1c1af72
82 changed files with 3219 additions and 0 deletions

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

63
app/build.gradle.kts Normal file
View File

@ -0,0 +1,63 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp") version "2.0.21-1.0.27"
}
android {
namespace = "com.github.nacabaro.vbhelper"
compileSdk = 35
defaultConfig {
applicationId = "com.github.nacabaro.vbhelper"
minSdk = 28
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.room.runtime)
implementation(project(":vb-nfc-reader"))
ksp(libs.androidx.room.compiler)
annotationProcessor(libs.androidx.room.compiler)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

21
app/proguard-rules.pro vendored Normal file
View File

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

View File

@ -0,0 +1,24 @@
package com.github.nacabaro.vbhelper
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.nacabaro.vbhelper", appContext.packageName)
}
}

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VBHelper"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.VBHelper">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,121 @@
package com.github.nacabaro.vbhelper
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.NfcA
import android.os.Bundle
import android.provider.Settings
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.github.nacabaro.vbhelper.R
import com.github.cfogrady.vbnfc.CryptographicTransformer
import com.github.cfogrady.vbnfc.TagCommunicator
import com.github.cfogrady.vbnfc.data.DeviceType
import com.github.nacabaro.vbhelper.ui.theme.VBHelperTheme
class MainActivity : ComponentActivity() {
private lateinit var nfcAdapter: NfcAdapter
private lateinit var deviceToCryptographicTransformers: Map<UShort, CryptographicTransformer>
// EXTRACTED DIRECTLY FROM EXAMPLE APP
override fun onCreate(savedInstanceState: Bundle?) {
deviceToCryptographicTransformers = getMapOfCryptographicTransformers()
val maybeNfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (maybeNfcAdapter == null) {
Toast.makeText(this, "No NFC on device!", Toast.LENGTH_SHORT).show()
finish()
return
}
nfcAdapter = maybeNfcAdapter
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
VBHelperTheme {
}
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun getMapOfCryptographicTransformers(): Map<UShort, CryptographicTransformer> {
return mapOf(
Pair(DeviceType.VitalBraceletBEDeviceType,
CryptographicTransformer(readableHmacKey1 = resources.getString(com.github.cfogrady.vbnfc.R.string.password1),
readableHmacKey2 = resources.getString(com.github.cfogrady.vbnfc.R.string.password2),
aesKey = resources.getString(com.github.cfogrady.vbnfc.R.string.decryptionKey),
substitutionCipher = resources.getIntArray(com.github.cfogrady.vbnfc.R.array.substitutionArray))),
// Pair(DeviceType.VitalSeriesDeviceType,
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
// hmacKey2 = resources.getString(R.string.password2),
// decryptionKey = resources.getString(R.string.decryptionKey),
// substitutionCipher = resources.getIntArray(R.array.substitutionArray))),
// Pair(DeviceType.VitalCharactersDeviceType,
// CryptographicTransformer(hmacKey1 = resources.getString(R.string.password1),
// hmacKey2 = resources.getString(R.string.password2),
// decryptionKey = resources.getString(R.string.decryptionKey),
// substitutionCipher = resources.getIntArray(R.array.substitutionArray)))
)
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun showWirelessSettings() {
Toast.makeText(this, "NFC must be enabled", Toast.LENGTH_SHORT).show()
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun buildOnReadTag(handlerFunc: (TagCommunicator)->String): (Tag)->Unit {
return { tag->
val nfcData = NfcA.get(tag)
if (nfcData == null) {
runOnUiThread {
Toast.makeText(this, "Tag detected is not VB", Toast.LENGTH_SHORT).show()
}
}
nfcData.connect()
nfcData.use {
val tagCommunicator = TagCommunicator.getInstance(nfcData, deviceToCryptographicTransformers)
val successText = handlerFunc(tagCommunicator)
runOnUiThread {
Toast.makeText(this, successText, Toast.LENGTH_SHORT).show()
}
}
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
private fun handleTag(handlerFunc: (TagCommunicator)->String) {
if (!nfcAdapter.isEnabled) {
showWirelessSettings()
} else {
val options = Bundle()
// Work around for some broken Nfc firmware implementations that poll the card too fast
options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
nfcAdapter.enableReaderMode(this, buildOnReadTag(handlerFunc), NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
options
)
}
}
// EXTRACTED DIRECTLY FROM EXAMPLE APP
override fun onPause() {
super.onPause()
if (nfcAdapter.isEnabled) {
nfcAdapter.disableReaderMode(this)
}
}
}

View File

@ -0,0 +1,12 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Dim(
@PrimaryKey(autoGenerate = true)
val id: Int,
val name: String,
val stageCount: Int
)

View File

@ -0,0 +1,28 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Dim::class,
parentColumns = ["id"],
childColumns = ["dimId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class DimProgress(
@PrimaryKey val dimId: Int,
@PrimaryKey val userId: Int,
val currentStage: Int,
val unlocked: Boolean
)

View File

@ -0,0 +1,30 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = Mon::class,
parentColumns = ["id"],
childColumns = ["monId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Mon::class,
parentColumns = ["id"],
childColumns = ["nextMon"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Evolutions(
@PrimaryKey val monId: Int,
@PrimaryKey val nextMon: Int,
val trophies: Int,
val vitals: Int,
val totalBattles: Int,
val winRate: Int // Does not need to be a floating point
)

View File

@ -0,0 +1,28 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.ForeignKey
@Entity(
foreignKeys = [
ForeignKey(
entity = Dim::class,
parentColumns = ["id"],
childColumns = ["dimId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Mon (
@PrimaryKey val id: Int,
val dimId: Int,
val monIndex: Int,
val name: String,
val stage: Int, // These should be replaced with enums
val attribute: Int, // This one too
val baseHp: Int,
val baseBp: Int,
val baseAp: Int,
val evoTime: Int, // In minutes
)

View File

@ -0,0 +1,11 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class User (
@PrimaryKey(autoGenerate = true) val id: Int,
val username: String,
val gender: Boolean
)

View File

@ -0,0 +1,21 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class UserHealthData(
@PrimaryKey val userId: Int,
val tamerRank: Int, // Old VB thingy, will probably go unused at first
val totalSteps: Int
)

View File

@ -0,0 +1,45 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Mon::class,
parentColumns = ["id"],
childColumns = ["monId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = UserMonsters::class,
parentColumns = ["id"],
childColumns = ["previousStage"],
onDelete = ForeignKey.CASCADE
)
]
)
data class UserMonsters (
@PrimaryKey(autoGenerate = true) val id: Int,
val userId: Int,
val monId: Int,
val previousStage: Int?,
val vitals: Int,
val trophies: Int,
val trainingAp: Int,
val trainingBp: Int,
val trainingHp: Int,
val rank: Int, // Maybe use another enum (?)
val ability: Int, // Another enum (???)
val evoTimerLeft: Int, // Minutes!!
val limitTimerLeft: Int, // Minutes!!!!!
val totalBattles: Int,
val totalWins: Int
)

View File

@ -0,0 +1,33 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = UserMonsters::class,
parentColumns = ["id"],
childColumns = ["monId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class UserMonstersSpecialMissions(
@PrimaryKey val monId: Int,
val slot1: Int,
val timeLeft1: Int,
val progression1: Int,
val slot2: Int,
val timeLeft2: Int,
val progression2: Int,
val slot3: Int,
val timeLeft3: Int,
val progression3: Int,
val slot4: Int,
val timeLeft4: Int,
val progression4: Int
)
// Not really proud of this one boss

View File

@ -0,0 +1,21 @@
package com.github.nacabaro.vbhelper.domain
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class UserStepsData(
@PrimaryKey val userId: Int,
val day: Int, // Unix?
val stepCount: Int
)

View File

@ -0,0 +1,11 @@
package com.github.nacabaro.vbhelper.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -0,0 +1,58 @@
package com.github.nacabaro.vbhelper.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun VBHelperTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -0,0 +1,34 @@
package com.github.nacabaro.vbhelper.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">VBHelper</string>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.VBHelper" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,17 @@
package com.github.nacabaro.vbhelper
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

6
build.gradle.kts Normal file
View File

@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

23
gradle.properties Normal file
View File

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

35
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,35 @@
[versions]
agp = "8.7.3"
kotlin = "2.0.0"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
composeBom = "2024.04.01"
roomRuntime = "2.6.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
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" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Tue Dec 24 10:55:57 UTC 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View File

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

24
settings.gradle.kts Normal file
View File

@ -0,0 +1,24 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "VBHelper"
include(":app")
include(":vb-nfc-reader")

1
vb-nfc-reader/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

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

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

View File

@ -0,0 +1,340 @@
<?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>

13
vb-nfc-reader/.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?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>

9
vb-nfc-reader/.idea/misc.xml generated Normal file
View File

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

View File

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

View File

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

View File

21
vb-nfc-reader/proguard-rules.pro vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,192 @@
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")
Log.i(TAG, "Signaling operation complete")
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_TRANSFERRED_TO_APP))
return nfcCharacter
}
@OptIn(ExperimentalStdlibApi::class)
private fun fetchDeviceTranslatorAndHeader(): DeviceTranslatorAndHeader {
val readData = nfcData.transceive(byteArrayOf(NFC_READ_COMMAND, HEADER_PAGE))
Log.i("TagCommunicator", "First 4 Pages: ${readData.toHexString()}")
val deviceTypeId = readData.getUInt16(4, ByteOrder.BIG_ENDIAN)
val translator = nfcDataTranslatorFactory.getNfcDataTranslator(deviceTypeId)
val header = translator.parseHeader(readData)
return DeviceTranslatorAndHeader(header, translator)
}
private fun readNfcData(): ByteArray {
val result = ByteArray(((LAST_DATA_PAGE +4)- START_DATA_PAGE) * 4)
for (page in START_DATA_PAGE..LAST_DATA_PAGE step 4) {
val pages = nfcData.transceive(byteArrayOf(NFC_READ_COMMAND, page.toByte()))
if (pages.size < 16) {
throw Exception("Failed to read page: $page")
}
System.arraycopy(pages, 0, result, (page - START_DATA_PAGE)*4, pages.size)
}
return result
}
fun prepareDIMForCharacter(dimId: UShort) {
val translatorAndHeader = fetchDeviceTranslatorAndHeader()
val header = translatorAndHeader.nfcHeader
val translator = translatorAndHeader.translator
// set app nonce to device ensure when we send back the character that we are preparing
// the same device we send to
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
// app authenticates, and reads everything, and checks the version
// The version check is only for the BE when transfering from DIM=0 (pulsemon).
// This was from the bug when the BE first came out.
// Check (page 103 [0:1] != 1, 0)
header.setDimId(dimId)
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_CHECK_DIM))
}
private fun defaultNfcDataGenerator(translator: NfcDataTranslator, character: NfcCharacter): ByteArray {
val currentNfcData = readNfcData()
val newNfcData = translator.cryptographicTransformer.decryptData(currentNfcData, nfcData.tag.id)
translator.setCharacterInByteArray(character, newNfcData)
checksumCalculator.recalculateChecksums(newNfcData)
translator.finalizeByteArrayFormat(newNfcData)
return newNfcData
}
// sendCharacter sends a character to the device using the nfcDataGenerator function. The
// default nfcDataGenerator reads the current data of the device and applies the new character
// data to the read data and prepares that to be sent back to the device. The nfcDataGenerator
// is a functor which takes in the NfcDataTranslator for the device and the NfcCharacter
// provided to the sendCharacter method and returns the decrypted byte array data to be sent
// back to the device.
@OptIn(ExperimentalStdlibApi::class)
fun sendCharacter(character: NfcCharacter, nfcDataGenerator: (NfcDataTranslator, NfcCharacter) -> ByteArray = this::defaultNfcDataGenerator) {
Log.i(TAG, "Sending Character: $character")
val deviceTranslatorAndHeader = fetchDeviceTranslatorAndHeader()
val translator = deviceTranslatorAndHeader.translator
val header = deviceTranslatorAndHeader.nfcHeader
// check the nonce
// if it's not expected, then the bracelet isn't ready
// Check the product and device ids that they match the target.
// This check relies on the app categories of BE, Vital Hero, and Vital Series.
// ensure the dim id matches the expected
if (character.dimId != header.getDimId()) {
throw IllegalArgumentException("Device is ready for DIM ${header.getDimId()}, but attempted to send ${character.dimId}")
}
// update the memory data
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_READY))
passwordAuth(translator.cryptographicTransformer)
var newNfcData = nfcDataGenerator(translator, character)
newNfcData = translator.cryptographicTransformer.encryptData(newNfcData, nfcData.tag.id)
Log.i(TAG, "Sending Character: ${newNfcData.toHexString()}")
// write nfc data
val pagedData = ConvertToPages(newNfcData)
for(pageToWriteIdx in 8..<pagedData.size) {
val pageToWrite = pagedData[pageToWriteIdx]
nfcData.transceive(byteArrayOf(NFC_WRITE_COMMAND, pageToWriteIdx.toByte(), pageToWrite[0], pageToWrite[1], pageToWrite[2], pageToWrite[3]))
}
nfcData.transceive(translator.getOperationCommandBytes(header, OPERATION_TRANSFERED_TO_DEVICE))
}
@OptIn(ExperimentalStdlibApi::class)
fun passwordAuth(cryptographicTransformer: CryptographicTransformer) {
val tagId = nfcData.tag.id
Log.i(TAG, "TagId: ${tagId.toHexString()}")
val password = cryptographicTransformer.createNfcPassword(tagId)
try {
val result = nfcData.transceive(byteArrayOf(NFC_PASSWORD_COMMAND, password[0], password[1], password[2], password[3]))
Log.i(TAG, "PasswordAuth Result: ${result.toHexString()}")
if (result.size == 1) {
throw AuthenticationException("Authentication failed. Result: ${result.toHexString()}")
}
} catch (e: Exception) {
Log.e(TAG, "Exception: ${e.message}")
}
}
// addDataTranslator adds a new data translator to be used with the specified deviceTypeId.
// This can be used to keep the same general communication protocol, but allows for a different
// parsing of the data.
fun addDataTranslator(nfcDataTranslator: NfcDataTranslator, deviceTypeId: UShort) {
nfcDataTranslatorFactory.addNfcDataTranslator(nfcDataTranslator, deviceTypeId)
}
class AuthenticationException(message: String): Exception(message)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,73 @@
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)
}
}