From 733b675ffa0f5410b945d503843878734d4f9b93 Mon Sep 17 00:00:00 2001 From: Seil0 Date: Sat, 17 Aug 2019 18:59:28 +0200 Subject: [PATCH] first working implementation of the mensa card reader * based on farebot's desfire protocol implementation * see #21 --- app/build.gradle | 4 + .../java/com/codebutler/farebot/Utils.java | 268 ++++++++++++++++++ .../card/desfire/DesfireApplication.java | 77 +++++ .../card/desfire/DesfireException.java | 13 + .../farebot/card/desfire/DesfireFile.java | 135 +++++++++ .../card/desfire/DesfireFileSettings.java | 241 ++++++++++++++++ .../desfire/DesfireManufacturingData.java | 173 +++++++++++ .../farebot/card/desfire/DesfireProtocol.java | 188 ++++++++++++ .../farebot/card/desfire/DesfireRecord.java | 35 +++ .../mosad/seil0/projectlaogai/MainActivity.kt | 52 +++- app/src/main/res/raw/notices.xml | 8 +- 11 files changed, 1185 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/codebutler/farebot/Utils.java create mode 100644 app/src/main/java/com/codebutler/farebot/card/desfire/DesfireApplication.java create mode 100644 app/src/main/java/com/codebutler/farebot/card/desfire/DesfireException.java create mode 100644 app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFile.java create mode 100644 app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFileSettings.java create mode 100644 app/src/main/java/com/codebutler/farebot/card/desfire/DesfireManufacturingData.java create mode 100644 app/src/main/java/com/codebutler/farebot/card/desfire/DesfireProtocol.java create mode 100644 app/src/main/java/com/codebutler/farebot/card/desfire/DesfireRecord.java diff --git a/app/build.gradle b/app/build.gradle index 23778aa..ebfb9cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,6 +25,9 @@ android { shrinkResources false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } + debug { + versionNameSuffix "-debug" + } } compileOptions { @@ -48,6 +51,7 @@ dependencies { implementation 'com.afollestad.material-dialogs:color:3.1.0' implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0' + implementation 'org.apache.commons:commons-lang3:3.9' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/app/src/main/java/com/codebutler/farebot/Utils.java b/app/src/main/java/com/codebutler/farebot/Utils.java new file mode 100644 index 0000000..c77b93c --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/Utils.java @@ -0,0 +1,268 @@ +/* + * Utils.java + * + * Copyright (C) 2011 Eric Butler + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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 findInList(List list, Matcher matcher) { + for (T item : list) { + if (matcher.matches(item)) { + return item; + } + } + return null; + } + + public static interface Matcher { + 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; + } + } +} diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireApplication.java b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireApplication.java new file mode 100644 index 0000000..bda03a6 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireApplication.java @@ -0,0 +1,77 @@ +/* + * DesfireApplication.java + * + * Copyright (C) 2011 Eric Butler + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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 CREATOR = new Parcelable.Creator() { + 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireException.java b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireException.java new file mode 100644 index 0000000..b737d38 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireException.java @@ -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); + } +} diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFile.java b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFile.java new file mode 100644 index 0000000..80ae5fc --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFile.java @@ -0,0 +1,135 @@ +/* + * DesfireFile.java + * + * Copyright (C) 2011 Eric Butler + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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 CREATOR = new Parcelable.Creator() { + 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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFileSettings.java b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFileSettings.java new file mode 100644 index 0000000..17220b7 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireFileSettings.java @@ -0,0 +1,241 @@ +/* + * DesfireFileSettings.java + * + * Copyright (C) 2011 Eric Butler + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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 CREATOR = new Parcelable.Creator() { + 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]); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireManufacturingData.java b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireManufacturingData.java new file mode 100644 index 0000000..becc763 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireManufacturingData.java @@ -0,0 +1,173 @@ +/* + * DesfireManufacturingData.java + * + * Copyright (C) 2011 Eric Butler + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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 CREATOR = new Parcelable.Creator() { + public DesfireManufacturingData createFromParcel(Parcel source) { + return new DesfireManufacturingData(source); + } + + public DesfireManufacturingData[] newArray(int size) { + return new DesfireManufacturingData[size]; + } + }; +} \ No newline at end of file diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireProtocol.java b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireProtocol.java new file mode 100644 index 0000000..28404d7 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireProtocol.java @@ -0,0 +1,188 @@ +/* + * DesfireProtocol.java + * + * Copyright (C) 2011 Eric Butler + * + * Authors: + * Eric Butler + * + * 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 . + */ + +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(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireRecord.java b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireRecord.java new file mode 100644 index 0000000..2dfeda4 --- /dev/null +++ b/app/src/main/java/com/codebutler/farebot/card/desfire/DesfireRecord.java @@ -0,0 +1,35 @@ +/* + * DesfireRecord.java + * + * Copyright (C) 2011 Eric Butler + * + * Authors: + * Eric Butler + * + * 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 . + */ + +package com.codebutler.farebot.card.desfire; + +public class DesfireRecord { + private byte[] mData; + + public DesfireRecord (byte[] data) { + mData = data; + } + + public byte[] getData () { + return mData; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt index 454365b..c151b8d 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt @@ -22,9 +22,12 @@ package org.mosad.seil0.projectlaogai +import android.content.Intent import android.graphics.Color +import android.nfc.NdefMessage import android.nfc.NfcAdapter import android.nfc.Tag +import android.nfc.tech.IsoDep import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -35,6 +38,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import com.afollestad.aesthetic.Aesthetic import com.afollestad.materialdialogs.MaterialDialog +import com.codebutler.farebot.card.desfire.DesfireProtocol import com.google.android.material.navigation.NavigationView import kotlinx.android.synthetic.main.activity_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.fragments.* 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 class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { @@ -74,16 +82,50 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte nav_view.setNavigationItemSelectedListener(this) // 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) - MaterialDialog(this) - .title(text = "nfc tag detected") - .message(text = tag.toString()) - .show() + + 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) + .title(text = "Mensa balance") + .message(text = "current: ${data / 1000}.${(data % 1000) / 10}€\n" + + "latest: ${settings.value / 1000}.${(settings.value % 1000) / 10}€") + .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 = 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() { super.onResume() Aesthetic.resume(this) diff --git a/app/src/main/res/raw/notices.xml b/app/src/main/res/raw/notices.xml index 152730c..6f1de65 100644 --- a/app/src/main/res/raw/notices.xml +++ b/app/src/main/res/raw/notices.xml @@ -25,10 +25,10 @@ Apache Software License 2.0 - Android Sliding Up Panel - https://github.com/umano/AndroidSlidingUpPanel + farebot part for desfire cards + https://github.com/codebutler/farebot - Apache Software License 2.0 + GNU General Public License v3.0 Android Support Libraries @@ -36,4 +36,4 @@ Copyright (C) 2016 The Android Open Source Project Apache Software License 2.0 - \ No newline at end of file +