first working implementation of the mensa card reader

* based on farebot's desfire protocol implementation
* see #21
This commit is contained in:
Jannik 2019-08-17 18:59:28 +02:00
parent 3a0a31f781
commit 733b675ffa
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
11 changed files with 1185 additions and 9 deletions

View File

@ -25,6 +25,9 @@ android {
shrinkResources false shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
debug {
versionNameSuffix "-debug"
}
} }
compileOptions { compileOptions {
@ -48,6 +51,7 @@ dependencies {
implementation 'com.afollestad.material-dialogs:color:3.1.0' implementation 'com.afollestad.material-dialogs:color:3.1.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0' implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation 'org.apache.commons:commons-lang3:3.9'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'

View File

@ -0,0 +1,268 @@
/*
* Utils.java
*
* Copyright (C) 2011 Eric Butler
*
* 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.content.DialogInterface;
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 java.util.List;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
public class Utils {
private static final String TAG = Utils.class.getName();
public static void showError (final Activity activity, Exception ex) {
Log.e(activity.getClass().getName(), ex.getMessage(), ex);
new AlertDialog.Builder(activity)
.setMessage(Utils.getErrorMessage(ex))
.show();
}
public static void showErrorAndFinish (final Activity activity, Exception ex) {
try {
Log.e(activity.getClass().getName(), Utils.getErrorMessage(ex));
ex.printStackTrace();
new AlertDialog.Builder(activity)
.setMessage(Utils.getErrorMessage(ex))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
activity.finish();
}
})
.show();
} catch (WindowManager.BadTokenException unused) {
/* Ignore... happens if the activity was destroyed */
}
}
public static String getHexString (byte[] b) throws Exception {
String result = "";
for (int i=0; i < b.length; i++) {
result += Integer.toString( ( b[i] & 0xff ) + 0x100, 16).substring( 1 );
}
return result;
}
public static String getHexString (byte[] b, String defaultResult) {
try {
return getHexString(b);
} catch (Exception ex) {
return defaultResult;
}
}
public static byte[] hexStringToByteArray (String s) {
if ((s.length() % 2) != 0) {
throw new IllegalArgumentException("Bad input string: " + s);
}
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
/*
public static byte[] intToByteArray(int value) {
return new byte[] {
(byte)(value >>> 24),
(byte)(value >>> 16),
(byte)(value >>> 8),
(byte)value};
}
*/
public static int byteArrayToInt(byte[] b) {
return byteArrayToInt(b, 0);
}
public static int byteArrayToInt(byte[] b, int offset) {
return byteArrayToInt(b, offset, b.length);
}
public static int byteArrayToInt(byte[] b, int offset, int length) {
return (int) byteArrayToLong(b, offset, length);
}
public static long byteArrayToLong(byte[] b, int offset, int length) {
if (b.length < length)
throw new IllegalArgumentException("length must be less than or equal to b.length");
long value = 0;
for (int i = 0; i < length; i++) {
int shift = (length - 1 - i) * 8;
value += (b[i + offset] & 0x000000FF) << shift;
}
return value;
}
public static byte[] byteArraySlice(byte[] b, int offset, int length) {
byte[] ret = new byte[length];
for (int i = 0; i < length; i++)
ret[i] = b[offset+i];
return ret;
}
public static String xmlNodeToString (Node node) throws Exception {
// The amount of code required to do simple things in Java is incredible.
Source source = new DOMSource(node);
StringWriter stringWriter = new StringWriter();
Result result = new StreamResult(stringWriter);
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
transformer.setURIResolver(null);
transformer.transform(source, result);
return stringWriter.getBuffer().toString();
}
public static String getErrorMessage (Throwable ex) {
String errorMessage = ex.getLocalizedMessage();
if (errorMessage == null)
errorMessage = ex.getMessage();
if (errorMessage == null)
errorMessage = ex.toString();
if (ex.getCause() != null) {
String causeMessage = ex.getCause().getLocalizedMessage();
if (causeMessage == null)
causeMessage = ex.getCause().getMessage();
if (causeMessage == null)
causeMessage = ex.getCause().toString();
if (causeMessage != null)
errorMessage += ": " + causeMessage;
}
return errorMessage;
}
public static <T> T findInList(List<T> list, Matcher<T> matcher) {
for (T item : list) {
if (matcher.matches(item)) {
return item;
}
}
return null;
}
public static interface Matcher<T> {
public boolean matches(T t);
}
public static int convertBCDtoInteger(byte data) {
return (((data & (char)0xF0) >> 4) * 10) + ((data & (char)0x0F));
}
public static int getBitsFromInteger(int buffer, int iStartBit, int iLength) {
return (buffer >> (iStartBit)) & ((char)0xFF >> (8 - iLength));
}
/* Based on function from mfocGUI by 'Huuf' (http://www.huuf.info/OV/) */
public static int getBitsFromBuffer(byte[] buffer, int iStartBit, int iLength) {
int iEndBit = iStartBit + iLength - 1;
int iSByte = iStartBit / 8;
int iSBit = iStartBit % 8;
int iEByte = iEndBit / 8;
int iEBit = iEndBit % 8;
if (iSByte == iEByte) {
return (int)(((char)buffer[iEByte] >> (7 - iEBit)) & ((char)0xFF >> (8 - iLength)));
} else {
int uRet = (((char)buffer[iSByte] & (char)((char)0xFF >> iSBit)) << (((iEByte - iSByte - 1) * 8) + (iEBit + 1)));
for (int i = iSByte + 1; i < iEByte; i++) {
uRet |= (((char)buffer[i] & (char)0xFF) << (((iEByte - i - 1) * 8) + (iEBit + 1)));
}
uRet |= (((char)buffer[iEByte] & (char)0xFF)) >> (7 - iEBit);
return uRet;
}
}
public static DesfireFileSettings selectAppFile(DesfireProtocol tag, int appID, int fileID) {
try {
tag.selectApp(appID);
} catch (DesfireException e) {
Log.w(TAG,"App not found");
return null;
}
try {
return tag.getFileSettings(fileID);
} catch (DesfireException e) {
Log.w(TAG,"File not found");
return null;
}
}
public static boolean arrayContains(int[] arr, int item) {
for (int i: arr)
if (i==item)
return true;
return false;
}
public static boolean containsAppFile(DesfireProtocol tag, int appID, int fileID) {
try {
tag.selectApp(appID);
} catch (DesfireException e) {
Log.w(TAG,"App not found");
Log.w(TAG, e);
return false;
}
try {
return arrayContains(tag.getFileList(),fileID);
} catch (DesfireException e) {
Log.w(TAG,"File not found");
Log.w(TAG, e);
return false;
}
}
}

View File

@ -0,0 +1,77 @@
/*
* DesfireApplication.java
*
* Copyright (C) 2011 Eric Butler
*
* 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;
public class DesfireApplication implements Parcelable {
private int mId;
private DesfireFile[] mFiles;
public DesfireApplication (int id, DesfireFile[] files) {
mId = id;
mFiles = files;
}
public int getId () {
return mId;
}
public DesfireFile[] getFiles () {
return mFiles;
}
public DesfireFile getFile (int fileId) {
for (DesfireFile file : mFiles) {
if (file.getId() == fileId)
return file;
}
return null;
}
public static final Parcelable.Creator<DesfireApplication> CREATOR = new Parcelable.Creator<DesfireApplication>() {
public DesfireApplication createFromParcel(Parcel source) {
int id = source.readInt();
DesfireFile[] files = new DesfireFile[source.readInt()];
source.readTypedArray(files, DesfireFile.CREATOR);
return new DesfireApplication(id, files);
}
public DesfireApplication[] newArray (int size) {
return new DesfireApplication[size];
}
};
public void writeToParcel (Parcel parcel, int flags) {
parcel.writeInt(mId);
parcel.writeInt(mFiles.length);
parcel.writeTypedArray(mFiles, flags);
}
public int describeContents () {
return 0;
}
}

View File

@ -0,0 +1,13 @@
package com.codebutler.farebot.card.desfire;
/**
* Created by Jakob Wenzel on 16.11.13.
*/
public class DesfireException extends Exception {
public DesfireException(String message) {
super(message);
}
public DesfireException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,135 @@
/*
* DesfireFile.java
*
* Copyright (C) 2011 Eric Butler
*
* 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 org.apache.commons.lang3.ArrayUtils;
import android.os.Parcel;
import android.os.Parcelable;
import com.codebutler.farebot.card.desfire.DesfireFileSettings.RecordDesfireFileSettings;
public class DesfireFile implements Parcelable {
private int mId;
private DesfireFileSettings mSettings;
private byte[] mData;
public static DesfireFile create (int fileId, DesfireFileSettings fileSettings, byte[] fileData) {
if (fileSettings instanceof RecordDesfireFileSettings)
return new RecordDesfireFile(fileId, fileSettings, fileData);
else
return new DesfireFile(fileId, fileSettings, fileData);
}
private DesfireFile (int fileId, DesfireFileSettings fileSettings, byte[] fileData) {
mId = fileId;
mSettings = fileSettings;
mData = fileData;
}
public DesfireFileSettings getFileSettings () {
return mSettings;
}
public int getId () {
return mId;
}
public byte[] getData () {
return mData;
}
public static final Parcelable.Creator<DesfireFile> CREATOR = new Parcelable.Creator<DesfireFile>() {
public DesfireFile createFromParcel(Parcel source) {
int fileId = source.readInt();
boolean isError = (source.readInt() == 1);
if (!isError) {
DesfireFileSettings fileSettings = (DesfireFileSettings) source.readParcelable(DesfireFileSettings.class.getClassLoader());
int dataLength = source.readInt();
byte[] fileData = new byte[dataLength];
source.readByteArray(fileData);
return DesfireFile.create(fileId, fileSettings, fileData);
} else {
return new InvalidDesfireFile(fileId, source.readString());
}
}
public DesfireFile[] newArray (int size) {
return new DesfireFile[size];
}
};
public void writeToParcel (Parcel parcel, int flags) {
parcel.writeInt(mId);
if (this instanceof InvalidDesfireFile) {
parcel.writeInt(1);
parcel.writeString(((InvalidDesfireFile)this).getErrorMessage());
} else {
parcel.writeInt(0);
parcel.writeParcelable(mSettings, 0);
parcel.writeInt(mData.length);
parcel.writeByteArray(mData);
}
}
public int describeContents () {
return 0;
}
public static class RecordDesfireFile extends DesfireFile {
private DesfireRecord[] mRecords;
private RecordDesfireFile (int fileId, DesfireFileSettings fileSettings, byte[] fileData) {
super(fileId, fileSettings, fileData);
RecordDesfireFileSettings settings = (RecordDesfireFileSettings) fileSettings;
DesfireRecord[] records = new DesfireRecord[settings.curRecords];
for (int i = 0; i < settings.curRecords; i++) {
int offset = settings.recordSize * i;
records[i] = new DesfireRecord(ArrayUtils.subarray(getData(), offset, offset + settings.recordSize));
}
mRecords = records;
}
public DesfireRecord[] getRecords () {
return mRecords;
}
}
public static class InvalidDesfireFile extends DesfireFile {
private String mErrorMessage;
public InvalidDesfireFile (int fileId, String errorMessage) {
super(fileId, null, new byte[0]);
mErrorMessage = errorMessage;
}
public String getErrorMessage () {
return mErrorMessage;
}
}
}

View File

@ -0,0 +1,241 @@
/*
* DesfireFileSettings.java
*
* Copyright (C) 2011 Eric Butler
*
* 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 java.io.ByteArrayInputStream;
import org.apache.commons.lang3.ArrayUtils;
import android.os.Parcel;
import android.os.Parcelable;
import com.codebutler.farebot.Utils;
public abstract class DesfireFileSettings implements Parcelable {
public final byte fileType;
public final byte commSetting;
public final byte[] accessRights;
/* DesfireFile Types */
static final byte STANDARD_DATA_FILE = (byte) 0x00;
static final byte BACKUP_DATA_FILE = (byte) 0x01;
static final byte VALUE_FILE = (byte) 0x02;
static final byte LINEAR_RECORD_FILE = (byte) 0x03;
static final byte CYCLIC_RECORD_FILE = (byte) 0x04;
public static DesfireFileSettings Create (byte[] data) throws DesfireException {
byte fileType = (byte) data[0];
ByteArrayInputStream stream = new ByteArrayInputStream(data);
if (fileType == STANDARD_DATA_FILE || fileType == BACKUP_DATA_FILE)
return new StandardDesfireFileSettings(stream);
else if (fileType == LINEAR_RECORD_FILE || fileType == CYCLIC_RECORD_FILE)
return new RecordDesfireFileSettings(stream);
else if (fileType == VALUE_FILE)
return new ValueDesfireFileSettings(stream);
else
throw new DesfireException("Unknown file type: " + Integer.toHexString(fileType));
}
private DesfireFileSettings (ByteArrayInputStream stream) {
fileType = (byte) stream.read();
commSetting = (byte) stream.read();
accessRights = new byte[2];
stream.read(accessRights, 0, accessRights.length);
}
private DesfireFileSettings (byte fileType, byte commSetting, byte[] accessRights) {
this.fileType = fileType;
this.commSetting = commSetting;
this.accessRights = accessRights;
}
public String getFileTypeName () {
switch (fileType) {
case STANDARD_DATA_FILE:
return "Standard";
case BACKUP_DATA_FILE:
return "Backup";
case VALUE_FILE:
return "Value";
case LINEAR_RECORD_FILE:
return "Linear Record";
case CYCLIC_RECORD_FILE:
return "Cyclic Record";
default:
return "Unknown";
}
}
public static final Parcelable.Creator<DesfireFileSettings> CREATOR = new Parcelable.Creator<DesfireFileSettings>() {
public DesfireFileSettings createFromParcel(Parcel source) {
byte fileType = source.readByte();
byte commSetting = source.readByte();
byte[] accessRights = new byte[source.readInt()];
source.readByteArray(accessRights);
if (fileType == STANDARD_DATA_FILE || fileType == BACKUP_DATA_FILE) {
int fileSize = source.readInt();
return new StandardDesfireFileSettings(fileType, commSetting, accessRights, fileSize);
} else if (fileType == LINEAR_RECORD_FILE || fileType == CYCLIC_RECORD_FILE) {
int recordSize = source.readInt();
int maxRecords = source.readInt();
int curRecords = source.readInt();
return new RecordDesfireFileSettings(fileType, commSetting, accessRights, recordSize, maxRecords, curRecords);
} else {
return new UnsupportedDesfireFileSettings(fileType);
}
}
public DesfireFileSettings[] newArray(int size) {
return new DesfireFileSettings[size];
}
};
public void writeToParcel (Parcel parcel, int flags) {
parcel.writeByte(fileType);
parcel.writeByte(commSetting);
parcel.writeInt(accessRights.length);
parcel.writeByteArray(accessRights);
}
public int describeContents () {
return 0;
}
public static class StandardDesfireFileSettings extends DesfireFileSettings {
public final int fileSize;
private StandardDesfireFileSettings (ByteArrayInputStream stream) {
super(stream);
byte[] buf = new byte[3];
stream.read(buf, 0, buf.length);
ArrayUtils.reverse(buf);
fileSize = Utils.byteArrayToInt(buf);
}
StandardDesfireFileSettings (byte fileType, byte commSetting, byte[] accessRights, int fileSize) {
super(fileType, commSetting, accessRights);
this.fileSize = fileSize;
}
@Override
public void writeToParcel (Parcel parcel, int flags) {
super.writeToParcel(parcel, flags);
parcel.writeInt(fileSize);
}
}
public static class RecordDesfireFileSettings extends DesfireFileSettings {
public final int recordSize;
public final int maxRecords;
public final int curRecords;
public RecordDesfireFileSettings(ByteArrayInputStream stream) {
super(stream);
byte[] buf = new byte[3];
stream.read(buf, 0, buf.length);
ArrayUtils.reverse(buf);
recordSize = Utils.byteArrayToInt(buf);
buf = new byte[3];
stream.read(buf, 0, buf.length);
ArrayUtils.reverse(buf);
maxRecords = Utils.byteArrayToInt(buf);
buf = new byte[3];
stream.read(buf, 0, buf.length);
ArrayUtils.reverse(buf);
curRecords = Utils.byteArrayToInt(buf);
}
RecordDesfireFileSettings (byte fileType, byte commSetting, byte[] accessRights, int recordSize, int maxRecords, int curRecords) {
super(fileType, commSetting, accessRights);
this.recordSize = recordSize;
this.maxRecords = maxRecords;
this.curRecords = curRecords;
}
@Override
public void writeToParcel (Parcel parcel, int flags) {
super.writeToParcel(parcel, flags);
parcel.writeInt(recordSize);
parcel.writeInt(maxRecords);
parcel.writeInt(curRecords);
}
}
public static class ValueDesfireFileSettings extends DesfireFileSettings {
public final int lowerLimit;
public final int upperLimit;
public final int value;
public final byte limitedCreditEnabled;
public ValueDesfireFileSettings(ByteArrayInputStream stream) {
super(stream);
byte[] buf = new byte[4];
stream.read(buf, 0, buf.length);
ArrayUtils.reverse(buf);
lowerLimit = Utils.byteArrayToInt(buf);
buf = new byte[4];
stream.read(buf, 0, buf.length);
ArrayUtils.reverse(buf);
upperLimit = Utils.byteArrayToInt(buf);
buf = new byte[4];
stream.read(buf, 0, buf.length);
ArrayUtils.reverse(buf);
value = Utils.byteArrayToInt(buf);
buf = new byte[1];
stream.read(buf, 0, buf.length);
limitedCreditEnabled = buf[0];
//http://www.skyetek.com/docs/m2/desfire.pdf
//http://neteril.org/files/M075031_desfire.pdf
}
@Override
public void writeToParcel (Parcel parcel, int flags) {
super.writeToParcel(parcel, flags);
parcel.writeInt(lowerLimit);
parcel.writeInt(upperLimit);
parcel.writeInt(value);
parcel.writeByte(limitedCreditEnabled);
}
}
public static class UnsupportedDesfireFileSettings extends DesfireFileSettings {
public UnsupportedDesfireFileSettings(byte fileType) {
super(fileType, Byte.MIN_VALUE, new byte[0]);
}
}
}

View File

@ -0,0 +1,173 @@
/*
* DesfireManufacturingData.java
*
* Copyright (C) 2011 Eric Butler
*
* 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;
public class DesfireManufacturingData implements Parcelable {
public final int hwVendorID;
public final int hwType;
public final int hwSubType;
public final int hwMajorVersion;
public final int hwMinorVersion;
public final int hwStorageSize;
public final int hwProtocol;
public final int swVendorID;
public final int swType;
public final int swSubType;
public final int swMajorVersion;
public final int swMinorVersion;
public final int swStorageSize;
public final int swProtocol;
public final int uid;
public final int batchNo;
public final int weekProd;
public final int yearProd;
public DesfireManufacturingData (byte[] data) {
ByteArrayInputStream stream = new 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?
byte[] buf = new byte[7];
stream.read(buf, 0, buf.length);
uid = Utils.byteArrayToInt(buf);
// FIXME: This is returning a negative number. Probably is unsigned.
buf = new byte[5];
stream.read(buf, 0, buf.length);
batchNo = Utils.byteArrayToInt(buf);
// FIXME: These numbers aren't making sense.
weekProd = stream.read();
yearProd = stream.read();
}
public static DesfireManufacturingData fromXml (Element element) {
return new DesfireManufacturingData(element);
}
private DesfireManufacturingData (Element element) {
hwVendorID = Integer.parseInt(element.getElementsByTagName("hw-vendor-id").item(0).getTextContent());
hwType = Integer.parseInt(element.getElementsByTagName("hw-type").item(0).getTextContent());
hwSubType = Integer.parseInt(element.getElementsByTagName("hw-sub-type").item(0).getTextContent());
hwMajorVersion = Integer.parseInt(element.getElementsByTagName("hw-major-version").item(0).getTextContent());
hwMinorVersion = Integer.parseInt(element.getElementsByTagName("hw-minor-version").item(0).getTextContent());
hwStorageSize = Integer.parseInt(element.getElementsByTagName("hw-storage-size").item(0).getTextContent());
hwProtocol = Integer.parseInt(element.getElementsByTagName("hw-protocol").item(0).getTextContent());
swVendorID = Integer.parseInt(element.getElementsByTagName("sw-vendor-id").item(0).getTextContent());
swType = Integer.parseInt(element.getElementsByTagName("sw-type").item(0).getTextContent());
swSubType = Integer.parseInt(element.getElementsByTagName("sw-sub-type").item(0).getTextContent());
swMajorVersion = Integer.parseInt(element.getElementsByTagName("sw-major-version").item(0).getTextContent());
swMinorVersion = Integer.parseInt(element.getElementsByTagName("sw-minor-version").item(0).getTextContent());
swStorageSize = Integer.parseInt(element.getElementsByTagName("sw-storage-size").item(0).getTextContent());
swProtocol = Integer.parseInt(element.getElementsByTagName("sw-protocol").item(0).getTextContent());
uid = Integer.parseInt(element.getElementsByTagName("uid").item(0).getTextContent());
batchNo = Integer.parseInt(element.getElementsByTagName("batch-no").item(0).getTextContent());
weekProd = Integer.parseInt(element.getElementsByTagName("week-prod").item(0).getTextContent());
yearProd = Integer.parseInt(element.getElementsByTagName("year-prod").item(0).getTextContent());
}
private DesfireManufacturingData (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();
}
public void writeToParcel(Parcel parcel, int flags) {
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);
}
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<DesfireManufacturingData> CREATOR = new Parcelable.Creator<DesfireManufacturingData>() {
public DesfireManufacturingData createFromParcel(Parcel source) {
return new DesfireManufacturingData(source);
}
public DesfireManufacturingData[] newArray(int size) {
return new DesfireManufacturingData[size];
}
};
}

View File

@ -0,0 +1,188 @@
/*
* DesfireProtocol.java
*
* Copyright (C) 2011 Eric Butler
*
* 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;
public class DesfireProtocol {
/* Commands */
static final byte GET_MANUFACTURING_DATA = (byte) 0x60;
static final byte GET_APPLICATION_DIRECTORY = (byte) 0x6A;
static final byte GET_ADDITIONAL_FRAME = (byte) 0xAF;
static final byte SELECT_APPLICATION = (byte) 0x5A;
static final byte READ_DATA = (byte) 0xBD;
static final byte READ_RECORD = (byte) 0xBB;
static final byte READ_VALUE = (byte) 0x6C;
static final byte GET_FILES = (byte) 0x6F;
static final byte GET_FILE_SETTINGS = (byte) 0xF5;
/* Status codes */
static final byte OPERATION_OK = (byte) 0x00;
static final byte PERMISSION_DENIED = (byte) 0x9D;
static final byte ADDITIONAL_FRAME = (byte) 0xAF;
private IsoDep mTagTech;
public DesfireProtocol(IsoDep tagTech) {
mTagTech = tagTech;
}
public DesfireManufacturingData getManufacturingData() throws DesfireException {
byte[] respBuffer = sendRequest(GET_MANUFACTURING_DATA);
if (respBuffer.length != 28)
throw new DesfireException("Invalid response");
return new DesfireManufacturingData(respBuffer);
}
public int[] getAppList() throws DesfireException {
byte[] appDirBuf = sendRequest(GET_APPLICATION_DIRECTORY);
int[] appIds = new int[appDirBuf.length / 3];
for (int app = 0; app < appDirBuf.length; app += 3) {
byte[] appId = new byte[3];
System.arraycopy(appDirBuf, app, appId, 0, 3);
appIds[app / 3] = Utils.byteArrayToInt(appId);
}
return appIds;
}
public void selectApp (int appId) throws DesfireException {
byte[] appIdBuff = new byte[3];
appIdBuff[0] = (byte) ((appId & 0xFF0000) >> 16);
appIdBuff[1] = (byte) ((appId & 0xFF00) >> 8);
appIdBuff[2] = (byte) (appId & 0xFF);
sendRequest(SELECT_APPLICATION, appIdBuff);
}
public int[] getFileList() throws DesfireException {
byte[] buf = sendRequest(GET_FILES);
int[] fileIds = new int[buf.length];
for (int x = 0; x < buf.length; x++) {
fileIds[x] = (int)buf[x];
}
return fileIds;
}
public DesfireFileSettings getFileSettings (int fileNo) throws DesfireException {
byte[] data = new byte[0];
data = sendRequest(GET_FILE_SETTINGS, new byte[] { (byte) fileNo });
return DesfireFileSettings.Create(data);
}
public byte[] readFile (int fileNo) throws DesfireException {
return sendRequest(READ_DATA, new byte[] {
(byte) fileNo,
(byte) 0x0, (byte) 0x0, (byte) 0x0,
(byte) 0x0, (byte) 0x0, (byte) 0x0
});
}
public byte[] readRecord (int fileNum) throws DesfireException {
return sendRequest(READ_RECORD, new byte[]{
(byte) fileNum,
(byte) 0x0, (byte) 0x0, (byte) 0x0,
(byte) 0x0, (byte) 0x0, (byte) 0x0
});
}
public int readValue(int fileNum) throws DesfireException {
byte[] buf = sendRequest(READ_VALUE, new byte[]{
(byte) fileNum
});
ArrayUtils.reverse(buf);
return Utils.byteArrayToInt(buf);
}
private byte[] sendRequest (byte command) throws DesfireException {
return sendRequest(command, null);
}
private byte[] sendRequest (byte command, byte[] parameters) throws DesfireException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] recvBuffer = new byte[0];
try {
recvBuffer = mTagTech.transceive(wrapMessage(command, parameters));
} catch (IOException e) {
throw new DesfireException(e);
}
while (true) {
if (recvBuffer[recvBuffer.length - 2] != (byte) 0x91)
throw new DesfireException("Invalid response");
output.write(recvBuffer, 0, recvBuffer.length - 2);
byte status = recvBuffer[recvBuffer.length - 1];
if (status == OPERATION_OK) {
break;
} else if (status == ADDITIONAL_FRAME) {
try {
recvBuffer = mTagTech.transceive(wrapMessage(GET_ADDITIONAL_FRAME, null));
} catch (IOException e) {
throw new DesfireException(e);
}
} else if (status == PERMISSION_DENIED) {
throw new DesfireException("Permission denied");
} else {
throw new DesfireException("Unknown status code: " + Integer.toHexString(status & 0xFF));
}
}
return output.toByteArray();
}
private byte[] wrapMessage (byte command, byte[] parameters) throws DesfireException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write((byte) 0x90);
stream.write(command);
stream.write((byte) 0x00);
stream.write((byte) 0x00);
if (parameters != null) {
stream.write((byte) parameters.length);
try {
stream.write(parameters);
} catch (IOException e) {
throw new DesfireException(e);
}
}
stream.write((byte) 0x00);
return stream.toByteArray();
}
}

View File

@ -0,0 +1,35 @@
/*
* DesfireRecord.java
*
* Copyright (C) 2011 Eric Butler
*
* 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;
public class DesfireRecord {
private byte[] mData;
public DesfireRecord (byte[] data) {
mData = data;
}
public byte[] getData () {
return mData;
}
}

View File

@ -22,9 +22,12 @@
package org.mosad.seil0.projectlaogai package org.mosad.seil0.projectlaogai
import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.nfc.NdefMessage
import android.nfc.NfcAdapter import android.nfc.NfcAdapter
import android.nfc.Tag import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -35,6 +38,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import com.afollestad.aesthetic.Aesthetic import com.afollestad.aesthetic.Aesthetic
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.codebutler.farebot.card.desfire.DesfireProtocol
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar_main.* import kotlinx.android.synthetic.main.app_bar_main.*
@ -44,6 +48,10 @@ import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.
import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cColorPrimary import org.mosad.seil0.projectlaogai.controller.PreferencesController.Companion.cColorPrimary
import org.mosad.seil0.projectlaogai.fragments.* import org.mosad.seil0.projectlaogai.fragments.*
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
import com.codebutler.farebot.Utils.selectAppFile
import com.codebutler.farebot.card.desfire.DesfireFileSettings
import java.lang.Exception
// TODO save the current fragment to show it when the app is restarted // TODO save the current fragment to show it when the app is restarted
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
@ -74,15 +82,49 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
nav_view.setNavigationItemSelectedListener(this) nav_view.setNavigationItemSelectedListener(this)
// TODO nfc stuff, needs to move to it's own function // TODO nfc stuff, needs to move to it's own function
if (intent.action == NfcAdapter.ACTION_TECH_DISCOVERED) { if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
val appId = 0x5F8415
val fileId = 1
val tag: Tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) val tag: Tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
val isoDep = IsoDep.get(tag)
isoDep.connect()
val card = DesfireProtocol(isoDep)
val settings = selectAppFile(card, appId, fileId)
if (settings is DesfireFileSettings.ValueDesfireFileSettings) {
val data = try {
card.readValue(fileId)
} catch (ex: Exception) { 0 }
MaterialDialog(this) MaterialDialog(this)
.title(text = "nfc tag detected") .title(text = "Mensa balance")
.message(text = tag.toString()) .message(text = "current: ${data / 1000}.${(data % 1000) / 10}\n" +
"latest: ${settings.value / 1000}.${(settings.value % 1000) / 10}")
.show() .show()
} }
} }
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_TAG)?.also { rawMessages ->
val messages: List<NdefMessage> = rawMessages.map { it as NdefMessage }
// Process the messages array.
MaterialDialog(this)
.title(text = "nfc tag detected (onNewIntent)")
.message(text = messages[0].toString())
.show()
}
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()

View File

@ -25,10 +25,10 @@
<license>Apache Software License 2.0</license> <license>Apache Software License 2.0</license>
</notice> </notice>
<notice> <notice>
<name>Android Sliding Up Panel</name> <name>farebot part for desfire cards</name>
<url>https://github.com/umano/AndroidSlidingUpPanel</url> <url>https://github.com/codebutler/farebot</url>
<copyright /> <copyright />
<license>Apache Software License 2.0</license> <license>GNU General Public License v3.0</license>
</notice> </notice>
<notice> <notice>
<name>Android Support Libraries</name> <name>Android Support Libraries</name>