59 changed files with 2376 additions and 698 deletions
@ -0,0 +1,9 @@
|
||||
kind: pipeline |
||||
name: default |
||||
|
||||
steps: |
||||
- name: assembleRelease |
||||
image: gradle:jdk8 |
||||
commands: |
||||
- gradle assembleRelease |
||||
|
@ -0,0 +1,227 @@
|
||||
/** |
||||
* Utils.kt |
||||
* |
||||
* Copyright (C) 2011 Eric Butler |
||||
* Copyright (C) 2019 <seil0@mosad.xyz> |
||||
* |
||||
* Authors: |
||||
* Eric Butler <eric@codebutler.com> |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
*/ |
||||
|
||||
package com.codebutler.farebot |
||||
|
||||
import android.app.Activity |
||||
import android.app.AlertDialog |
||||
import android.util.Log |
||||
import android.view.WindowManager |
||||
import com.codebutler.farebot.card.desfire.DesfireException |
||||
import com.codebutler.farebot.card.desfire.DesfireFileSettings |
||||
import com.codebutler.farebot.card.desfire.DesfireProtocol |
||||
import org.w3c.dom.Node |
||||
import java.io.StringWriter |
||||
import javax.xml.transform.OutputKeys |
||||
import javax.xml.transform.TransformerFactory |
||||
import javax.xml.transform.dom.DOMSource |
||||
import javax.xml.transform.stream.StreamResult |
||||
import kotlin.experimental.and |
||||
|
||||
class Utils { |
||||
|
||||
companion object { |
||||
private val TAG = Utils::class.java.name |
||||
|
||||
@Suppress("unused") |
||||
fun showError(activity: Activity, ex: Exception) { |
||||
Log.e(activity.javaClass.name, ex.message, ex) |
||||
AlertDialog.Builder(activity) |
||||
.setMessage(getErrorMessage(ex)) |
||||
.show() |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
fun showErrorAndFinish(activity: Activity, ex: Exception) { |
||||
try { |
||||
Log.e(activity.javaClass.name, getErrorMessage(ex)) |
||||
ex.printStackTrace() |
||||
|
||||
AlertDialog.Builder(activity) |
||||
.setMessage(getErrorMessage(ex)) |
||||
.setCancelable(false) |
||||
.setPositiveButton(android.R.string.ok) { _, _ -> activity.finish() } |
||||
.show() |
||||
} catch (unused: WindowManager.BadTokenException) { |
||||
/* Ignore... happens if the activity was destroyed */ |
||||
} |
||||
|
||||
} |
||||
|
||||
@Throws(Exception::class) |
||||
fun getHexString(b: ByteArray): String { |
||||
var result = "" |
||||
for (i in b.indices) { |
||||
result += ((b[i] and 0xff.toByte()) + 0x100).toString(16).substring(1) |
||||
} |
||||
return result |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
fun getHexString(b: ByteArray, defaultResult: String): String { |
||||
return try { |
||||
getHexString(b) |
||||
} catch (ex: Exception) { |
||||
defaultResult |
||||
} |
||||
|
||||
} |
||||
|
||||
@Suppress("unused") |
||||
fun hexStringToByteArray(s: String): ByteArray { |
||||
if (s.length % 2 != 0) { |
||||
throw IllegalArgumentException("Bad input string: $s") |
||||
} |
||||
|
||||
val len = s.length |
||||
val data = ByteArray(len / 2) |
||||
var i = 0 |
||||
while (i < len) { |
||||
data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte() |
||||
i += 2 |
||||
} |
||||
return data |
||||
} |
||||
|
||||
@JvmOverloads |
||||
fun byteArrayToInt(b: ByteArray, offset: Int = 0, length: Int = b.size): Int { |
||||
return byteArrayToLong(b, offset, length).toInt() |
||||
} |
||||
|
||||
fun byteArrayToLong(b: ByteArray, offset: Int, length: Int): Long { |
||||
if (b.size < length) |
||||
throw IllegalArgumentException("length must be less than or equal to b.length") |
||||
|
||||
var value: Long = 0 |
||||
for (i in 0 until length) { |
||||
val shift = (length - 1 - i) * 8 |
||||
value += ((b[i + offset].toInt() and 0x000000FF).toLong() shl shift) |
||||
} |
||||
|
||||
return value |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
fun byteArraySlice(b: ByteArray, offset: Int, length: Int): ByteArray { |
||||
val ret = ByteArray(length) |
||||
for (i in 0 until length) |
||||
ret[i] = b[offset + i] |
||||
return ret |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
@Throws(Exception::class) |
||||
fun xmlNodeToString(node: Node): String { |
||||
// The amount of code required to do simple things in Java is incredible. |
||||
val source = DOMSource(node) |
||||
val stringWriter = StringWriter() |
||||
val result = StreamResult(stringWriter) |
||||
val factory = TransformerFactory.newInstance() |
||||
val transformer = factory.newTransformer() |
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes") |
||||
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") |
||||
transformer.uriResolver = null |
||||
transformer.transform(source, result) |
||||
return stringWriter.buffer.toString() |
||||
} |
||||
|
||||
fun getErrorMessage(ex: Throwable): String { |
||||
var errorMessage: String? = ex.localizedMessage |
||||
if (errorMessage == null) |
||||
errorMessage = ex.message |
||||
if (errorMessage == null) |
||||
errorMessage = ex.toString() |
||||
|
||||
if (ex.cause != null) { |
||||
var causeMessage: String? = ex.cause!!.localizedMessage |
||||
if (causeMessage == null) |
||||
causeMessage = ex.cause!!.message |
||||
if (causeMessage == null) |
||||
causeMessage = ex.cause.toString() |
||||
|
||||
errorMessage += ": $causeMessage" |
||||
} |
||||
|
||||
return errorMessage |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
fun <T> findInList(list: List<T>, matcher: Matcher<T>): T? { |
||||
for (item in list) { |
||||
if (matcher.matches(item)) { |
||||
return item |
||||
} |
||||
} |
||||
return null |
||||
} |
||||
|
||||
interface Matcher<T> { |
||||
fun matches(t: T): Boolean |
||||
} |
||||
|
||||
fun selectAppFile(tag: DesfireProtocol, appID: Int, fileID: Int): DesfireFileSettings? { |
||||
try { |
||||
tag.selectApp(appID) |
||||
} catch (e: DesfireException) { |
||||
Log.w(TAG, "App not found") |
||||
return null |
||||
} |
||||
|
||||
return try { |
||||
tag.getFileSettings(fileID) |
||||
} catch (e: DesfireException) { |
||||
Log.w(TAG, "File not found") |
||||
null |
||||
} |
||||
|
||||
} |
||||
|
||||
fun arrayContains(arr: IntArray, item: Int): Boolean { |
||||
for (i in arr) |
||||
if (i == item) |
||||
return true |
||||
return false |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
fun containsAppFile(tag: DesfireProtocol, appID: Int, fileID: Int): Boolean { |
||||
try { |
||||
tag.selectApp(appID) |
||||
} catch (e: DesfireException) { |
||||
Log.w(TAG, "App not found") |
||||
Log.w(TAG, e) |
||||
return false |
||||
} |
||||
|
||||
return try { |
||||
arrayContains(tag.fileList, fileID) |
||||
} catch (e: DesfireException) { |
||||
Log.w(TAG, "File not found") |
||||
Log.w(TAG, e) |
||||
false |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,9 @@
|
||||
package com.codebutler.farebot.card.desfire |
||||
|
||||
/** |
||||
* Created by Jakob Wenzel on 16.11.13. |
||||
*/ |
||||
class DesfireException : Exception { |
||||
constructor(message: String) : super(message) |
||||
constructor(cause: Throwable) : super(cause) |
||||
} |
@ -0,0 +1,104 @@
|
||||
/** |
||||
* DesfireFile.kt |
||||
* |
||||
* Copyright (C) 2011 Eric Butler |
||||
* Copyright (C) 2019 <seil0@mosad.xyz> |
||||
* |
||||
* Authors: |
||||
* Eric Butler <eric@codebutler.com> |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
*/ |
||||
|
||||
package com.codebutler.farebot.card.desfire |
||||
|
||||
import android.os.Parcel |
||||
import android.os.Parcelable |
||||
import com.codebutler.farebot.card.desfire.DesfireFileSettings.RecordDesfireFileSettings |
||||
import org.apache.commons.lang3.ArrayUtils |
||||
|
||||
|
||||
open class DesfireFile private constructor(val id: Int, private val fileSettings: DesfireFileSettings?, val data: ByteArray) : |
||||
Parcelable { |
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) { |
||||
parcel.writeInt(id) |
||||
if (this is InvalidDesfireFile) { |
||||
parcel.writeInt(1) |
||||
parcel.writeString(this.errorMessage) |
||||
} else { |
||||
parcel.writeInt(0) |
||||
parcel.writeParcelable(fileSettings, 0) |
||||
parcel.writeInt(data.size) |
||||
parcel.writeByteArray(data) |
||||
} |
||||
} |
||||
|
||||
override fun describeContents(): Int { |
||||
return 0 |
||||
} |
||||
|
||||
class RecordDesfireFile(fileId: Int, fileSettings: DesfireFileSettings, fileData: ByteArray) : |
||||
DesfireFile(fileId, fileSettings, fileData) { |
||||
private val records: Array<DesfireRecord?> |
||||
|
||||
init { |
||||
|
||||
val settings = fileSettings as RecordDesfireFileSettings |
||||
|
||||
val records = arrayOfNulls<DesfireRecord>(settings.curRecords) |
||||
for (i in 0 until settings.curRecords) { |
||||
val offset = settings.recordSize * i |
||||
records[i] = DesfireRecord(ArrayUtils.subarray(data, offset, offset + settings.recordSize)) |
||||
} |
||||
this.records = records |
||||
} |
||||
} |
||||
|
||||
class InvalidDesfireFile(fileId: Int, val errorMessage: String?) : DesfireFile(fileId, null, ByteArray(0)) |
||||
|
||||
companion object { |
||||
|
||||
fun create(fileId: Int, fileSettings: DesfireFileSettings, fileData: ByteArray): DesfireFile { |
||||
return (fileSettings as? RecordDesfireFileSettings)?.let { RecordDesfireFile(fileId, it, fileData) } |
||||
?: DesfireFile(fileId, fileSettings, fileData) |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
@JvmField |
||||
val CREATOR: Parcelable.Creator<DesfireFile> = object : Parcelable.Creator<DesfireFile> { |
||||
override fun createFromParcel(source: Parcel): DesfireFile { |
||||
val fileId = source.readInt() |
||||
|
||||
val isError = source.readInt() == 1 |
||||
|
||||
return if (!isError) { |
||||
val fileSettings = |
||||
source.readParcelable<Parcelable>(DesfireFileSettings::class.java.classLoader) as DesfireFileSettings |
||||
val dataLength = source.readInt() |
||||
val fileData = ByteArray(dataLength) |
||||
source.readByteArray(fileData) |
||||
|
||||
create(fileId, fileSettings, fileData) |
||||
} else { |
||||
InvalidDesfireFile(fileId, source.readString()) |
||||
} |
||||
} |
||||
|
||||
override fun newArray(size: Int): Array<DesfireFile?> { |
||||
return arrayOfNulls(size) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,248 @@
|
||||
/** |
||||
* DesfireFileSettings.kt |
||||
* |
||||
* Copyright (C) 2011 Eric Butler |
||||
* Copyright (C) 2019 <seil0@mosad.xyz> |
||||
* |
||||
* Authors: |
||||
* Eric Butler <eric@codebutler.com> |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
*/ |
||||
|
||||
package com.codebutler.farebot.card.desfire |
||||
|
||||
import android.os.Parcel |
||||
import android.os.Parcelable |
||||
import com.codebutler.farebot.Utils |
||||
import org.apache.commons.lang3.ArrayUtils |
||||
import java.io.ByteArrayInputStream |
||||
|
||||
abstract class DesfireFileSettings : Parcelable { |
||||
private val fileType: Byte |
||||
private val commSetting: Byte |
||||
private val accessRights: ByteArray |
||||
|
||||
@Suppress("unused") |
||||
val fileTypeName: String |
||||
get() { |
||||
return when (fileType) { |
||||
STANDARD_DATA_FILE -> "Standard" |
||||
BACKUP_DATA_FILE -> "Backup" |
||||
VALUE_FILE -> "Value" |
||||
LINEAR_RECORD_FILE -> "Linear Record" |
||||
CYCLIC_RECORD_FILE -> "Cyclic Record" |
||||
else -> "Unknown" |
||||
} |
||||
} |
||||
|
||||
private constructor(stream: ByteArrayInputStream) { |
||||
fileType = stream.read().toByte() |
||||
commSetting = stream.read().toByte() |
||||
|
||||
accessRights = ByteArray(2) |
||||
stream.read(accessRights, 0, accessRights.size) |
||||
} |
||||
|
||||
private constructor(fileType: Byte, commSetting: Byte, accessRights: ByteArray) { |
||||
this.fileType = fileType |
||||
this.commSetting = commSetting |
||||
this.accessRights = accessRights |
||||
} |
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) { |
||||
parcel.writeByte(fileType) |
||||
parcel.writeByte(commSetting) |
||||
parcel.writeInt(accessRights.size) |
||||
parcel.writeByteArray(accessRights) |
||||
} |
||||
|
||||
override fun describeContents(): Int { |
||||
return 0 |
||||
} |
||||
|
||||
class StandardDesfireFileSettings : DesfireFileSettings { |
||||
private val fileSize: Int |
||||
|
||||
internal constructor(stream: ByteArrayInputStream) : super(stream) { |
||||
val buf = ByteArray(3) |
||||
stream.read(buf, 0, buf.size) |
||||
ArrayUtils.reverse(buf) |
||||
fileSize = Utils.byteArrayToInt(buf) |
||||
} |
||||
|
||||
internal constructor( |
||||
fileType: Byte, |
||||
commSetting: Byte, |
||||
accessRights: ByteArray, |
||||
fileSize: Int |
||||
) : super(fileType, commSetting, accessRights) { |
||||
this.fileSize = fileSize |
||||
} |
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) { |
||||
super.writeToParcel(parcel, flags) |
||||
parcel.writeInt(fileSize) |
||||
} |
||||
} |
||||
|
||||
class RecordDesfireFileSettings : DesfireFileSettings { |
||||
private val maxRecords: Int |
||||
val recordSize: Int |
||||
val curRecords: Int |
||||
|
||||
constructor(stream: ByteArrayInputStream) : super(stream) { |
||||
|
||||
var buf = ByteArray(3) |
||||
stream.read(buf, 0, buf.size) |
||||
ArrayUtils.reverse(buf) |
||||
recordSize = Utils.byteArrayToInt(buf) |
||||
|
||||
buf = ByteArray(3) |
||||
stream.read(buf, 0, buf.size) |
||||
ArrayUtils.reverse(buf) |
||||
maxRecords = Utils.byteArrayToInt(buf) |
||||
|
||||
buf = ByteArray(3) |
||||
stream.read(buf, 0, buf.size) |
||||
ArrayUtils.reverse(buf) |
||||
curRecords = Utils.byteArrayToInt(buf) |
||||
} |
||||
|
||||
internal constructor( |
||||
fileType: Byte, |
||||
commSetting: Byte, |
||||
accessRights: ByteArray, |
||||
recordSize: Int, |
||||
maxRecords: Int, |
||||
curRecords: Int |
||||
) : super(fileType, commSetting, accessRights) { |
||||
this.recordSize = recordSize |
||||
this.maxRecords = maxRecords |
||||
this.curRecords = curRecords |
||||
} |
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) { |
||||
super.writeToParcel(parcel, flags) |
||||
parcel.writeInt(recordSize) |
||||
parcel.writeInt(maxRecords) |
||||
parcel.writeInt(curRecords) |
||||
} |
||||
} |
||||
|
||||
|
||||
class ValueDesfireFileSettings(stream: ByteArrayInputStream) : DesfireFileSettings(stream) { |
||||
private val lowerLimit: Int |
||||
private val upperLimit: Int |
||||
val value: Int |
||||
private val limitedCreditEnabled: Byte |
||||
|
||||
init { |
||||
|
||||
var buf = ByteArray(4) |
||||
stream.read(buf, 0, buf.size) |
||||
ArrayUtils.reverse(buf) |
||||
lowerLimit = Utils.byteArrayToInt(buf) |
||||
|
||||
buf = ByteArray(4) |
||||
stream.read(buf, 0, buf.size) |
||||
ArrayUtils.reverse(buf) |
||||
upperLimit = Utils.byteArrayToInt(buf) |
||||
|
||||
buf = ByteArray(4) |
||||
stream.read(buf, 0, buf.size) |
||||
ArrayUtils.reverse(buf) |
||||
value = Utils.byteArrayToInt(buf) |
||||
|
||||
|
||||
buf = ByteArray(1) |
||||
stream.read(buf, 0, buf.size) |
||||
limitedCreditEnabled = buf[0] |
||||
|
||||
//http://www.skyetek.com/docs/m2/desfire.pdf |
||||
//http://neteril.org/files/M075031_desfire.pdf |
||||
} |
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) { |
||||
super.writeToParcel(parcel, flags) |
||||
parcel.writeInt(lowerLimit) |
||||
parcel.writeInt(upperLimit) |
||||
parcel.writeInt(value) |
||||
parcel.writeByte(limitedCreditEnabled) |
||||
} |
||||
} |
||||
|
||||
class UnsupportedDesfireFileSettings(fileType: Byte) : |
||||
DesfireFileSettings(fileType, java.lang.Byte.MIN_VALUE, ByteArray(0)) |
||||
|
||||
companion object { |
||||
|
||||
/* DesfireFile Types */ |
||||
internal const val STANDARD_DATA_FILE = 0x00.toByte() |
||||
internal const val BACKUP_DATA_FILE = 0x01.toByte() |
||||
internal const val VALUE_FILE = 0x02.toByte() |
||||
internal const val LINEAR_RECORD_FILE = 0x03.toByte() |
||||
internal const val CYCLIC_RECORD_FILE = 0x04.toByte() |
||||
|
||||
@Throws(DesfireException::class) |
||||
fun create(data: ByteArray): DesfireFileSettings { |
||||
val fileType = data[0] |
||||
|
||||
val stream = ByteArrayInputStream(data) |
||||
|
||||
return if (fileType == STANDARD_DATA_FILE || fileType == BACKUP_DATA_FILE) |
||||
StandardDesfireFileSettings(stream) |
||||
else if (fileType == LINEAR_RECORD_FILE || fileType == CYCLIC_RECORD_FILE) |
||||
RecordDesfireFileSettings(stream) |
||||
else if (fileType == VALUE_FILE) |
||||
ValueDesfireFileSettings(stream) |
||||
else |
||||
throw DesfireException("Unknown file type: " + Integer.toHexString(fileType.toInt())) |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
@JvmField |
||||
val CREATOR: Parcelable.Creator<DesfireFileSettings> = object : Parcelable.Creator<DesfireFileSettings> { |
||||
override fun createFromParcel(source: Parcel): DesfireFileSettings { |
||||
val fileType = source.readByte() |
||||
val commSetting = source.readByte() |
||||
val accessRights = ByteArray(source.readInt()) |
||||
source.readByteArray(accessRights) |
||||
|
||||
if (fileType == STANDARD_DATA_FILE || fileType == BACKUP_DATA_FILE) { |
||||
val fileSize = source.readInt() |
||||
return StandardDesfireFileSettings(fileType, commSetting, accessRights, fileSize) |
||||
} else if (fileType == LINEAR_RECORD_FILE || fileType == CYCLIC_RECORD_FILE) { |
||||
val recordSize = source.readInt() |
||||
val maxRecords = source.readInt() |
||||
val curRecords = source.readInt() |
||||
return RecordDesfireFileSettings( |
||||
fileType, |
||||
commSetting, |
||||
accessRights, |
||||
recordSize, |
||||
maxRecords, |
||||
curRecords |
||||
) |
||||
} else { |
||||
return UnsupportedDesfireFileSettings(fileType) |
||||
} |
||||
} |
||||
|
||||
override fun newArray(size: Int): Array<DesfireFileSettings?> { |
||||
return arrayOfNulls(size) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,181 @@
|
||||
/** |
||||
* DesfireManufacturingData.kt |
||||
* |
||||
* Copyright (C) 2011 Eric Butler |
||||
* Copyright (C) 2019 <seil0@mosad.xyz> |
||||
* |
||||
* Authors: |
||||
* Eric Butler <eric@codebutler.com> |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
*/ |
||||
|
||||
package com.codebutler.farebot.card.desfire |
||||
|
||||
import android.os.Parcel |
||||
import android.os.Parcelable |
||||
import com.codebutler.farebot.Utils |
||||
import org.w3c.dom.Element |
||||
|
||||
import java.io.ByteArrayInputStream |
||||
|
||||
class DesfireManufacturingData : Parcelable { |
||||
private val hwVendorID: Int |
||||
private val hwType: Int |
||||
private val hwSubType: Int |
||||
private val hwMajorVersion: Int |
||||
private val hwMinorVersion: Int |
||||
private val hwStorageSize: Int |
||||
private val hwProtocol: Int |
||||
|
||||
private val swVendorID: Int |
||||
private val swType: Int |
||||
private val swSubType: Int |
||||
private val swMajorVersion: Int |
||||
private val swMinorVersion: Int |
||||
private val swStorageSize: Int |
||||
private val swProtocol: Int |
||||
|
||||
private val uid: Int |
||||
private val batchNo: Int |
||||
private val weekProd: Int |
||||
private val yearProd: Int |
||||
|
||||
constructor(data: ByteArray) { |
||||
val stream = ByteArrayInputStream(data) |
||||
hwVendorID = stream.read() |
||||
hwType = stream.read() |
||||
hwSubType = stream.read() |
||||
hwMajorVersion = stream.read() |
||||
hwMinorVersion = stream.read() |
||||
hwStorageSize = stream.read() |
||||
hwProtocol = stream.read() |
||||
|
||||
swVendorID = stream.read() |
||||
swType = stream.read() |
||||
swSubType = stream.read() |
||||
swMajorVersion = stream.read() |
||||
swMinorVersion = stream.read() |
||||
swStorageSize = stream.read() |
||||
swProtocol = stream.read() |
||||
|
||||
// FIXME: This has fewer digits than what's contained in EXTRA_ID, why? |
||||
var buf = ByteArray(7) |
||||
stream.read(buf, 0, buf.size) |
||||
uid = Utils.byteArrayToInt(buf) |
||||
|
||||
// FIXME: This is returning a negative number. Probably is unsigned. |
||||
buf = ByteArray(5) |
||||
stream.read(buf, 0, buf.size) |
||||
batchNo = Utils.byteArrayToInt(buf) |
||||
|
||||
// FIXME: These numbers aren't making sense. |
||||
weekProd = stream.read() |
||||
yearProd = stream.read() |
||||
} |
||||
|
||||
private constructor(element: Element) { |
||||
hwVendorID = Integer.parseInt(element.getElementsByTagName("hw-vendor-id").item(0).textContent) |
||||
hwType = Integer.parseInt(element.getElementsByTagName("hw-type").item(0).textContent) |
||||
hwSubType = Integer.parseInt(element.getElementsByTagName("hw-sub-type").item(0).textContent) |
||||
hwMajorVersion = Integer.parseInt(element.getElementsByTagName("hw-major-version").item(0).textContent) |
||||
hwMinorVersion = Integer.parseInt(element.getElementsByTagName("hw-minor-version").item(0).textContent) |
||||
hwStorageSize = Integer.parseInt(element.getElementsByTagName("hw-storage-size").item(0).textContent) |
||||
hwProtocol = Integer.parseInt(element.getElementsByTagName("hw-protocol").item(0).textContent) |
||||
|
||||
swVendorID = Integer.parseInt(element.getElementsByTagName("sw-vendor-id").item(0).textContent) |
||||
swType = Integer.parseInt(element.getElementsByTagName("sw-type").item(0).textContent) |
||||
swSubType = Integer.parseInt(element.getElementsByTagName("sw-sub-type").item(0).textContent) |
||||
swMajorVersion = Integer.parseInt(element.getElementsByTagName("sw-major-version").item(0).textContent) |
||||
swMinorVersion = Integer.parseInt(element.getElementsByTagName("sw-minor-version").item(0).textContent) |
||||
swStorageSize = Integer.parseInt(element.getElementsByTagName("sw-storage-size").item(0).textContent) |
||||
swProtocol = Integer.parseInt(element.getElementsByTagName("sw-protocol").item(0).textContent) |
||||
|
||||
uid = Integer.parseInt(element.getElementsByTagName("uid").item(0).textContent) |
||||
batchNo = Integer.parseInt(element.getElementsByTagName("batch-no").item(0).textContent) |
||||
weekProd = Integer.parseInt(element.getElementsByTagName("week-prod").item(0).textContent) |
||||
yearProd = Integer.parseInt(element.getElementsByTagName("year-prod").item(0).textContent) |
||||
} |
||||
|
||||
private constructor(parcel: Parcel) { |
||||
hwVendorID = parcel.readInt() |
||||
hwType = parcel.readInt() |
||||
hwSubType = parcel.readInt() |
||||
hwMajorVersion = parcel.readInt() |
||||
hwMinorVersion = parcel.readInt() |
||||
hwStorageSize = parcel.readInt() |
||||
hwProtocol = parcel.readInt() |
||||
|
||||
swVendorID = parcel.readInt() |
||||
swType = parcel.readInt() |
||||
swSubType = parcel.readInt() |
||||
swMajorVersion = parcel.readInt() |
||||
swMinorVersion = parcel.readInt() |
||||
swStorageSize = parcel.readInt() |
||||
swProtocol = parcel.readInt() |
||||
|
||||
uid = parcel.readInt() |
||||
batchNo = parcel.readInt() |
||||
weekProd = parcel.readInt() |
||||
yearProd = parcel.readInt() |
||||
} |
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) { |
||||
parcel.writeInt(hwVendorID) |
||||
parcel.writeInt(hwType) |
||||
parcel.writeInt(hwSubType) |
||||
parcel.writeInt(hwMajorVersion) |
||||
parcel.writeInt(hwMinorVersion) |
||||
parcel.writeInt(hwStorageSize) |
||||
parcel.writeInt(hwProtocol) |
||||
|
||||
parcel.writeInt(swVendorID) |
||||
parcel.writeInt(swType) |
||||
parcel.writeInt(swSubType) |
||||
parcel.writeInt(swMajorVersion) |
||||
parcel.writeInt(swMinorVersion) |
||||
parcel.writeInt(swStorageSize) |
||||
parcel.writeInt(swProtocol) |
||||
|
||||
parcel.writeInt(uid) |
||||
parcel.writeInt(batchNo) |
||||
parcel.writeInt(weekProd) |
||||
parcel.writeInt(yearProd) |
||||
} |
||||
|
||||
override fun describeContents(): Int { |
||||
return 0 |
||||
} |
||||
|
||||
companion object { |
||||
|
||||
@Suppress("unused") |
||||
fun fromXml(element: Element): DesfireManufacturingData { |
||||
return DesfireManufacturingData(element) |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
@JvmField |
||||
val CREATOR: Parcelable.Creator<DesfireManufacturingData> = |
||||
object : Parcelable.Creator<DesfireManufacturingData> { |
||||
override fun createFromParcel(source: Parcel): DesfireManufacturingData { |
||||
return DesfireManufacturingData(source) |
||||
} |
||||
|
||||
override fun newArray(size: Int): Array<DesfireManufacturingData?> { |
||||
return arrayOfNulls(size) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,214 @@
|
||||
/** |
||||
* DesfireProtocol.kt |
||||
* |
||||
* Copyright (C) 2011 Eric Butler |
||||
* Copyright (C) 2019 <seil0@mosad.xyz> |
||||
* |
||||
* Authors: |
||||
* Eric Butler <eric@codebutler.com> |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
*/ |
||||
|
||||
package com.codebutler.farebot.card.desfire |
||||
|
||||
import android.nfc.tech.IsoDep |
||||
import com.codebutler.farebot.Utils |
||||
|
||||
import java.io.ByteArrayOutputStream |
||||
import java.io.IOException |
||||
|
||||
import org.apache.commons.lang3.ArrayUtils |
||||
import kotlin.experimental.and |
||||
|
||||
class DesfireProtocol(private val mTagTech: IsoDep) { |
||||
|
||||
@Suppress("unused") |
||||
val manufacturingData: DesfireManufacturingData |
||||
@Throws(DesfireException::class) |
||||
get() { |
||||
val respBuffer = sendRequest(GET_MANUFACTURING_DATA) |
||||
|
||||
if (respBuffer.size != 28) |
||||
throw DesfireException("Invalid response") |
||||
|
||||
return DesfireManufacturingData(respBuffer) |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
val appList: IntArray |
||||
@Throws(DesfireException::class) |
||||
get() { |
||||
val appDirBuf = sendRequest(GET_APPLICATION_DIRECTORY) |
||||
|
||||
val appIds = IntArray(appDirBuf.size / 3) |
||||
|
||||
var app = 0 |
||||
while (app < appDirBuf.size) { |
||||
val appId = ByteArray(3) |
||||
System.arraycopy(appDirBuf, app, appId, 0, 3) |
||||
|
||||
appIds[app / 3] = Utils.byteArrayToInt(appId) |
||||
app += 3 |
||||
} |
||||
|
||||
return appIds |
||||
} |
||||
|
||||
val fileList: IntArray |
||||
@Throws(DesfireException::class) |
||||
get() { |
||||
val buf = sendRequest(GET_FILES) |
||||
val fileIds = IntArray(buf.size) |
||||
for (x in buf.indices) { |
||||
fileIds[x] = buf[x].toInt() |
||||
} |
||||
return fileIds |
||||
} |
||||
|
||||
@Throws(DesfireException::class) |
||||
fun selectApp(appId: Int) { |
||||
val appIdBuff = ByteArray(3) |
||||
appIdBuff[0] = (appId and 0xFF0000 shr 16).toByte() |
||||
appIdBuff[1] = (appId and 0xFF00 shr 8).toByte() |
||||
appIdBuff[2] = (appId and 0xFF).toByte() |
||||
|
||||
sendRequest(SELECT_APPLICATION, appIdBuff) |
||||
} |
||||
|
||||
@Throws(DesfireException::class) |
||||
fun getFileSettings(fileNo: Int): DesfireFileSettings { |
||||
val data = sendRequest(GET_FILE_SETTINGS, byteArrayOf(fileNo.toByte())) |
||||
return DesfireFileSettings.create(data) |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
@Throws(DesfireException::class) |
||||
fun readFile(fileNo: Int): ByteArray { |
||||
return sendRequest( |
||||
READ_DATA, |
||||
byteArrayOf( |
||||
fileNo.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte() |
||||
) |
||||
) |
||||
} |
||||
|
||||
@Suppress("unused") |
||||
@Throws(DesfireException::class) |
||||
fun readRecord(fileNum: Int): ByteArray { |
||||
return sendRequest( |
||||
READ_RECORD, |
||||
byteArrayOf( |
||||
fileNum.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte(), |
||||
0x0.toByte() |
||||
) |
||||
) |
||||
} |
||||
|
||||
@Throws(DesfireException::class) |
||||
fun readValue(fileNum: Int): Int { |
||||
val buf = sendRequest(READ_VALUE, byteArrayOf(fileNum.toByte())) |
||||
ArrayUtils.reverse(buf) |
||||
return Utils.byteArrayToInt(buf) |
||||
} |
||||
|
||||
@Throws(DesfireException::class) |
||||
private fun sendRequest(command: Byte, parameters: ByteArray? = null): ByteArray { |
||||
val output = ByteArrayOutputStream() |
||||
|
||||
var recvBuffer: ByteArray |
||||
try { |
||||
recvBuffer = mTagTech.transceive(wrapMessage(command, parameters)) |
||||
} catch (e: IOException) { |
||||
throw DesfireException(e) |
||||
} |
||||
|
||||
while (true) { |
||||
if (recvBuffer[recvBuffer.size - 2] != 0x91.toByte()) |
||||
throw DesfireException("Invalid response") |
||||
|
||||
output.write(recvBuffer, 0, recvBuffer.size - 2) |
||||
|
||||
val status = recvBuffer[recvBuffer.size - 1] |
||||
if (status == OPERATION_OK) { |
||||
break |
||||
} else if (status == ADDITIONAL_FRAME) { |
||||
try { |
||||
recvBuffer = mTagTech.transceive(wrapMessage(GET_ADDITIONAL_FRAME, null)) |
||||
} catch (e: IOException) { |
||||
throw DesfireException(e) |
||||
} |
||||
|
||||
} else if (status == PERMISSION_DENIED) { |
||||
throw DesfireException("Permission denied") |
||||
} else { |
||||
throw DesfireException("Unknown status code: " + Integer.toHexString((status and 0xFF.toByte()).toInt())) |
||||
} |
||||
} |
||||
|
||||
return output.toByteArray() |
||||
} |
||||
|
||||
@Throws(DesfireException::class) |
||||
private fun wrapMessage(command: Byte, parameters: ByteArray?): ByteArray { |
||||
val stream = ByteArrayOutputStream() |
||||
|
||||
stream.write(0x90.toByte().toInt()) |
||||
stream.write(command.toInt()) |
||||
stream.write(0x00.toByte().toInt()) |
||||
stream.write(0x00.toByte().toInt()) |
||||
if (parameters != null) { |
||||
stream.write(parameters.size.toByte().toInt()) |
||||
try { |
||||
stream.write(parameters) |
||||
} catch (e: IOException) { |
||||
throw DesfireException(e) |
||||
} |
||||
|
||||
} |
||||
stream.write(0x00.toByte().toInt()) |
||||
|
||||
return stream.toByteArray() |
||||
} |
||||
|
||||
companion object { |
||||
/* Commands */ |
||||
internal const val GET_MANUFACTURING_DATA = 0x60.toByte() |
||||
internal const val GET_APPLICATION_DIRECTORY = 0x6A.toByte() |
||||
internal const val GET_ADDITIONAL_FRAME = 0xAF.toByte() |
||||
internal const val SELECT_APPLICATION = 0x5A.toByte() |
||||
internal const val READ_DATA = 0xBD.toByte() |
||||
internal const val READ_RECORD = 0xBB.toByte() |
||||
internal const val READ_VALUE = 0x6C.toByte() |
||||
internal const val GET_FILES = 0x6F.toByte() |
||||
internal const val GET_FILE_SETTINGS = 0xF5.toByte() |
||||
|
||||
/* Status codes */ |
||||
internal const val OPERATION_OK = 0x00.toByte() |
||||
internal const val PERMISSION_DENIED = 0x9D.toByte() |
||||
internal const val ADDITIONAL_FRAME = 0xAF.toByte() |
||||
} |
||||
|
||||
} |
@ -0,0 +1,26 @@
|
||||
/* |
||||
* DesfireRecord.kt |
||||
* |
||||
* Copyright (C) 2011 Eric Butler |
||||
* Copyright (C) 2019 <seil0@mosad.xyz> |
||||
* |
||||
* Authors: |
||||
* Eric Butler <eric@codebutler.com> |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
*/ |
||||
|
||||
package com.codebutler.farebot.card.desfire |
||||
|
||||
class DesfireRecord(val data: ByteArray) |