aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/util/Iso7816TLVTest.java99
-rw-r--r--OpenKeychain/src/main/AndroidManifest.xml12
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcIntentActivity.java312
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Iso7816TLV.java195
4 files changed, 618 insertions, 0 deletions
diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/util/Iso7816TLVTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/util/Iso7816TLVTest.java
new file mode 100644
index 000000000..fccecc3cc
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/util/Iso7816TLVTest.java
@@ -0,0 +1,99 @@
+/* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.util.Iso7816TLV.Iso7816CompositeTLV;
+
+@RunWith(RobolectricTestRunner.class)
+@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
+public class Iso7816TLVTest {
+
+ @Before
+ public void setUp() throws Exception {
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testDecode() throws Exception {
+
+ // this is an Application Related Data packet, received from my Yubikey
+ String input = "6e81dd4f10d27600012401020000000000000100005f520f0073000080000000000000000000007300c00af00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f030303c53c1efdb4845ca242ca6977fddb1f788094fd3b430af1114c28a08d8c5afda81191cc50ca9bf51bc99fe8e6ca03a9d4d40e7b5925cd154813df381655b2c63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5423590e5423590e5423590e9000";
+ byte[] data = Hex.decode(input);
+
+ Iso7816TLV tlv = Iso7816TLV.readSingle(data, true);
+ Assert.assertNotNull("tlv parse must succeed", tlv);
+
+ Assert.assertEquals("top packet must be 'application related data' tag", 0x6e, tlv.mT);
+ Assert.assertEquals("length must be correct", 221, tlv.mL);
+
+ Assert.assertTrue("top packet must be composite", tlv instanceof Iso7816CompositeTLV);
+
+ Iso7816CompositeTLV ctlv = (Iso7816CompositeTLV) tlv;
+
+ Assert.assertEquals("top packet must have 11 sub packets", 11, ctlv.mSubs.length);
+
+ Assert.assertEquals("sub packet #1 must have expected tag", 0x4f, ctlv.mSubs[0].mT);
+ Assert.assertEquals("sub packet #1 must have expected length", 16, ctlv.mSubs[0].mL);
+
+ Assert.assertEquals("sub packet #2 must have expected tag", 0x5f52, ctlv.mSubs[1].mT);
+ Assert.assertEquals("sub packet #2 must have expected length", 15, ctlv.mSubs[1].mL);
+
+ Assert.assertEquals("sub packet #3 must have expected tag", 0x73, ctlv.mSubs[2].mT);
+ Assert.assertEquals("sub packet #3 must have expected length", 0, ctlv.mSubs[2].mL);
+ Assert.assertTrue("sub packet #3 muse be composite", ctlv.mSubs[2] instanceof Iso7816CompositeTLV);
+
+ Assert.assertEquals("sub packet #4 must have expected tag", 0xc0, ctlv.mSubs[3].mT);
+ Assert.assertEquals("sub packet #4 must have expected length", 10, ctlv.mSubs[3].mL);
+
+ Assert.assertEquals("sub packet #5 must have expected tag", 0xc1, ctlv.mSubs[4].mT);
+ Assert.assertEquals("sub packet #5 must have expected length", 6, ctlv.mSubs[4].mL);
+
+ Assert.assertEquals("sub packet #6 must have expected tag", 0xc2, ctlv.mSubs[5].mT);
+ Assert.assertEquals("sub packet #6 must have expected length", 6, ctlv.mSubs[5].mL);
+
+ Assert.assertEquals("sub packet #7 must have expected tag", 0xc3, ctlv.mSubs[6].mT);
+ Assert.assertEquals("sub packet #7 must have expected length", 6, ctlv.mSubs[6].mL);
+
+ Assert.assertEquals("sub packet #8 must have expected tag", 0xc4, ctlv.mSubs[7].mT);
+ Assert.assertEquals("sub packet #8 must have expected length", 7, ctlv.mSubs[7].mL);
+
+ Assert.assertEquals("sub packet #9 must have expected tag", 0xc5, ctlv.mSubs[8].mT);
+ Assert.assertEquals("sub packet #9 must have expected length", 60, ctlv.mSubs[8].mL);
+
+ {
+ // this is my pubkey fingerprint
+ String fingerprint = "1efdb4845ca242ca6977fddb1f788094fd3b430a";
+ byte[] V1 = new byte[20];
+ System.arraycopy(ctlv.mSubs[8].mV, 0, V1, 0, 20);
+ Assert.assertArrayEquals("fingerprint must match", V1, Hex.decode(fingerprint));
+ }
+
+ Assert.assertEquals("sub packet #10 must have expected tag", 0xc6, ctlv.mSubs[9].mT);
+ Assert.assertEquals("sub packet #10 must have expected length", 60, ctlv.mSubs[9].mL);
+
+ Assert.assertEquals("sub packet #11 must have expected tag", 0xcd, ctlv.mSubs[10].mT);
+ Assert.assertEquals("sub packet #11 must have expected length", 12, ctlv.mSubs[10].mL);
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml
index 2c8de0372..ddec381c8 100644
--- a/OpenKeychain/src/main/AndroidManifest.xml
+++ b/OpenKeychain/src/main/AndroidManifest.xml
@@ -632,6 +632,18 @@
android:launchMode="singleTop"
android:taskAffinity=":Nfc"
android:allowTaskReparenting="true" />
+
+ <activity
+ android:name=".ui.NfcIntentActivity"
+ android:launchMode="singleTop">
+ <intent-filter>
+ <action android:name="android.nfc.action.NDEF_DISCOVERED" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:host="my.yubico.com" android:scheme="https"/>
+ </intent-filter>
+ </activity>
+
<activity
android:name=".ui.HelpActivity"
android:label="@string/title_help" />
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcIntentActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcIntentActivity.java
new file mode 100644
index 000000000..cb15dbec2
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcIntentActivity.java
@@ -0,0 +1,312 @@
+/**
+ * Copyright (c) 2013-2014 Philipp Jakubeit, Signe Rüsch, Dominik Schürmann
+ *
+ * Licensed under the Bouncy Castle License (MIT license). See LICENSE file for details.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.tech.IsoDep;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import org.spongycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.util.Iso7816TLV;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant
+ * NFC devices.
+ *
+ * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
+ */
+@TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1)
+public class NfcIntentActivity extends ActionBarActivity {
+
+ // special extra for OpenPgpService
+ public static final String EXTRA_DATA = "data";
+
+ private Intent mServiceIntent;
+
+ private static final int TIMEOUT = 100000;
+
+ private NfcAdapter mNfcAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(Constants.TAG, "NfcActivity.onCreate");
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ setContentView(R.layout.nfc_activity);
+
+ Intent intent = getIntent();
+ Bundle data = intent.getExtras();
+ String action = intent.getAction();
+
+ Log.d(Constants.TAG, action);
+ Log.d(Constants.TAG, intent.getDataString());
+
+ // TODO check fingerprint
+ // mFingerprint = data.getByteArray(EXTRA_FINGERPRINT);
+
+ if (!NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
+ Log.d(Constants.TAG, "Action not supported: " + action);
+ finish();
+ }
+
+ try {
+ Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+
+ // Connect to the detected tag, setting a couple of settings
+ IsoDep isoDep = IsoDep.get(detectedTag);
+ isoDep.setTimeout(TIMEOUT); // timeout is set to 100 seconds to avoid cancellation during calculation
+ isoDep.connect();
+
+ nfcGreet(isoDep);
+ // nfcPin(isoDep, "yoloswag");
+ nfcGetFingerprint(isoDep);
+
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "IOException!", e);
+ finish();
+ }
+
+ }
+
+ /**
+ * Called when the system is about to start resuming a previous activity,
+ * disables NFC Foreground Dispatch
+ */
+ public void onPause() {
+ super.onPause();
+ Log.d(Constants.TAG, "NfcActivity.onPause");
+
+ // disableNfcForegroundDispatch();
+ }
+
+ /**
+ * Called when the activity will start interacting with the user,
+ * enables NFC Foreground Dispatch
+ */
+ public void onResume() {
+ super.onResume();
+ Log.d(Constants.TAG, "NfcActivity.onResume");
+
+ // enableNfcForegroundDispatch();
+ }
+
+ /** Handle NFC communication and return a result.
+ *
+ * This method is called by onNewIntent above upon discovery of an NFC tag.
+ * It handles initialization and login to the application, subsequently
+ * calls either nfcCalculateSignature() or nfcDecryptSessionKey(), then
+ * finishes the activity with an appropiate result.
+ *
+ * On general communication, see also
+ * http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-a.aspx
+ *
+ * References to pages are generally related to the OpenPGP Application
+ * on ISO SmartCard Systems specification.
+ *
+ */
+ private void nfcGreet(IsoDep isoDep) throws IOException {
+
+ // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
+ // See specification, page 51
+ String accepted = "9000";
+
+ // Command APDU (page 51) for SELECT FILE command (page 29)
+ String opening =
+ "00" // CLA
+ + "A4" // INS
+ + "04" // P1
+ + "00" // P2
+ + "06" // Lc (number of bytes)
+ + "D27600012401" // Data (6 bytes)
+ + "00"; // Le
+ if (!card(isoDep, opening).equals(accepted)) { // activate connection
+ toast("Opening Error!");
+ setResult(RESULT_CANCELED, mServiceIntent);
+ finish();
+ }
+ }
+
+ private void nfcPin(IsoDep isoDep, String pin) throws IOException {
+
+ String data = "00CA006E00";
+ String fingerprint = card(isoDep, data);
+
+ // Command APDU for VERIFY command (page 32)
+ String login =
+ "00" // CLA
+ + "20" // INS
+ + "00" // P1
+ + "82" // P2 (PW1)
+ + String.format("%02x", pin.length()) // Lc
+ + Hex.toHexString(pin.getBytes());
+ if ( ! card(isoDep, login).equals("9000")) { // login
+ toast("Pin Error!");
+ setResult(RESULT_CANCELED, mServiceIntent);
+ finish();
+ }
+
+ }
+
+ /**
+ * Gets the user ID
+ *
+ * @return the user id as "name <email>"
+ * @throws java.io.IOException
+ */
+ public static String getUserId(IsoDep isoDep) throws IOException {
+ String info = "00CA006500";
+ String data = "00CA005E00";
+ return getName(card(isoDep, info)) + " <" + (new String(Hex.decode(getDataField(card(isoDep, data))))) + ">";
+ }
+
+ /**
+ * Gets the long key ID
+ *
+ * @return the long key id (last 16 bytes of the fingerprint)
+ * @throws java.io.IOException
+ */
+ public static long getKeyId(IsoDep isoDep) throws IOException {
+ String keyId = nfcGetFingerprint(isoDep).substring(24);
+ Log.d(Constants.TAG, "keyId: " + keyId);
+ return Long.parseLong(keyId, 16);
+ }
+
+ /**
+ * Gets the fingerprint of the signature key
+ *
+ * @return the fingerprint
+ * @throws java.io.IOException
+ */
+ public static String nfcGetFingerprint(IsoDep isoDep) throws IOException {
+ String data = "00CA006E00";
+ byte[] buf = isoDep.transceive(Hex.decode(data));
+
+ Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true);
+ Log.d(Constants.TAG, "nfc tlv data:\n" + tlv.prettyPrint());
+
+ Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5);
+ if (fptlv != null) {
+ ByteBuffer fpbuf = ByteBuffer.wrap(fptlv.mV);
+ byte[] fp = new byte[20];
+ fpbuf.get(fp);
+ Log.d(Constants.TAG, "fingerprint 1: " + KeyFormattingUtils.convertFingerprintToHex(fp));
+ fpbuf.get(fp);
+ Log.d(Constants.TAG, "fingerprint 2: " + KeyFormattingUtils.convertFingerprintToHex(fp));
+ fpbuf.get(fp);
+ Log.d(Constants.TAG, "fingerprint 3: " + KeyFormattingUtils.convertFingerprintToHex(fp));
+ }
+
+ return "nope";
+ }
+
+ /**
+ * Prints a message to the screen
+ *
+ * @param text the text which should be contained within the toast
+ */
+ private void toast(String text) {
+ Toast.makeText(this, text, Toast.LENGTH_LONG).show();
+ }
+
+ /**
+ * Receive new NFC Intents to this activity only by enabling foreground dispatch.
+ * This can only be done in onResume!
+ */
+ public void enableNfcForegroundDispatch() {
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ Intent nfcI = new Intent(this, NfcIntentActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ PendingIntent nfcPendingIntent = PendingIntent.getActivity(this, 0, nfcI, 0);
+ IntentFilter[] writeTagFilters = new IntentFilter[]{
+ new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
+ };
+
+ // https://code.google.com/p/android/issues/detail?id=62918
+ // maybe mNfcAdapter.enableReaderMode(); ?
+ try {
+ mNfcAdapter.enableForegroundDispatch(this, nfcPendingIntent, writeTagFilters, null);
+ } catch (IllegalStateException e) {
+ Log.i(Constants.TAG, "NfcForegroundDispatch Error!", e);
+ }
+ Log.d(Constants.TAG, "NfcForegroundDispatch has been enabled!");
+ }
+
+ /**
+ * Disable foreground dispatch in onPause!
+ */
+ public void disableNfcForegroundDispatch() {
+ mNfcAdapter.disableForegroundDispatch(this);
+ Log.d(Constants.TAG, "NfcForegroundDispatch has been disabled!");
+ }
+
+ /**
+ * Gets the name of the user out of the raw card output regarding card holder related data
+ *
+ * @param name the raw card holder related data from the card
+ * @return the name given in this data
+ */
+ public static String getName(String name) {
+ String slength;
+ int ilength;
+ name = name.substring(6);
+ slength = name.substring(0, 2);
+ ilength = Integer.parseInt(slength, 16) * 2;
+ name = name.substring(2, ilength + 2);
+ name = (new String(Hex.decode(name))).replace('<', ' ');
+ return (name);
+ }
+
+ /**
+ * Reduces the raw data from the card by four characters
+ *
+ * @param output the raw data from the card
+ * @return the data field of that data
+ */
+ private static String getDataField(String output) {
+ return output.substring(0, output.length() - 4);
+ }
+
+ /**
+ * Communicates with the OpenPgpCard via the APDU
+ *
+ * @param hex the hexadecimal APDU
+ * @return The answer from the card
+ * @throws java.io.IOException throws an exception if something goes wrong
+ */
+ public static String card(IsoDep isoDep, String hex) throws IOException {
+ return getHex(isoDep.transceive(Hex.decode(hex)));
+ }
+
+ /**
+ * Converts a byte array into an hex string
+ *
+ * @param raw the byte array representation
+ * @return the hexadecimal string representation
+ */
+ public static String getHex(byte[] raw) {
+ return new String(Hex.encode(raw));
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Iso7816TLV.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Iso7816TLV.java
new file mode 100644
index 000000000..90afd3bc0
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Iso7816TLV.java
@@ -0,0 +1,195 @@
+/* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+/** TLV data structure and parser implementation.
+ *
+ * This class is used for parsing and working with TLV packets as specified in
+ * the Functional Specification of the OpenPGP application on ISO Smart Card
+ * Operating Systems, and ISO 7816-4.
+ *
+ * Objects of this class are immutable data structs parsed from BER-TLV data.
+ * They include at least the T, L and V fields they were originally parsed
+ * from, subclasses may also carry more specific information.
+ *
+ * @see { @linktourl http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-d.aspx}
+ *
+ */
+public class Iso7816TLV {
+
+ public final int mT;
+ public final int mL;
+ public final byte[] mV;
+
+ private Iso7816TLV(int T, int L, byte[] V) {
+ mT = T;
+ mL = L;
+ mV = V;
+ }
+
+ public String prettyPrint() {
+ return prettyPrint(0);
+ }
+
+ public String prettyPrint(int indent) {
+ // lol
+ String padding = " ".substring(0, indent*2);
+ return padding + String.format("tag T %4x L %04d", mT, mL);
+ }
+
+ /** Read a single Iso7816 TLV packet from a given ByteBuffer, either
+ * recursively or flat.
+ *
+ * This is a convenience wrapper for readSingle with ByteBuffer argument.
+ */
+ public static Iso7816TLV readSingle(byte[] data, boolean recursive) throws IOException {
+ return readSingle(ByteBuffer.wrap(data), recursive);
+ }
+
+ /** Read a single Iso7816 TLV packet from a given ByteBuffer, either
+ * recursively or flat.
+ *
+ * If the recursive flag is true, a composite packet will be parsed and
+ * returned as an Iso7816CompositeTLV. Otherwise, a regular Iso7816TLV
+ * object will be returned regardless of actual packet type.
+ *
+ * This method is fail-fast, if any parsing error occurs it will throw an
+ * exception.
+ *
+ */
+ public static Iso7816TLV readSingle(ByteBuffer data, boolean recursive) throws IOException {
+
+ int T = data.get() & 0xff;
+ boolean composite = (T & 0x20) == 0x20;
+ if ((T & 0x1f) == 0x1f) {
+ int T2 = data.get() & 0xff;
+ if ((T2 & 0x1f) == 0x1f) {
+ throw new IOException("Only tags up to two bytes are supported!");
+ }
+ T = (T << 8) | (T2 & 0x7f);
+ }
+
+ // Log.d(Constants.TAG, String.format("T %02x", T));
+
+ // parse length, according to ISO 7816-4 (openpgp card 2.0 specs, page 24)
+ int L = data.get() & 0xff;
+ if (L == 0x81) {
+ L = data.get() & 0xff;
+ } else if (L == 0x82) {
+ L = data.get() & 0xff;
+ L = (L << 8) | (data.get() & 0xff);
+ } else if (L >= 0x80) {
+ throw new IOException("Invalid length field!");
+ }
+
+ // Log.d(Constants.TAG, String.format("L %02x", L));
+
+ // read L bytes into new buffer
+ byte[] V = new byte[L];
+ data.get(V);
+
+ // if we are supposed to parse composites, do that
+ if (recursive && composite) {
+ // Log.d(Constants.TAG, "parsing composite TLV");
+ Iso7816TLV[] subs = readList(V, true);
+ return new Iso7816CompositeTLV(T, L, V, subs);
+ }
+
+ return new Iso7816TLV(T, L, V);
+
+ }
+
+ /** Parse a list of TLV packets from byte data, recursively or flat.
+ *
+ * This method is fail-fast, if any parsing error occurs it will throw an
+ * exception.
+ *
+ */
+ public static Iso7816TLV[] readList(byte[] data, boolean recursive) throws IOException {
+ ByteBuffer buf = ByteBuffer.wrap(data);
+
+ ArrayList<Iso7816TLV> result = new ArrayList<Iso7816TLV>();
+
+ // read while data is available. this will fail if there is trailing data!
+ while (buf.hasRemaining()) {
+ // skip 0x00 and 0xFF filler bytes
+ buf.mark();
+ byte peek = buf.get();
+ if (peek == 0xff || peek == 0x00) {
+ continue;
+ }
+ buf.reset();
+
+ Iso7816TLV packet = readSingle(buf, recursive);
+ result.add(packet);
+ }
+
+ Iso7816TLV[] resultX = new Iso7816TLV[result.size()];
+ result.toArray(resultX);
+ return resultX;
+ }
+
+ /** This class represents a composite TLV packet.
+ *
+ * Note that only actual composite TLV packets are instances of this class.
+ * A regular non-composite Iso7816TLV object may however be a composite
+ * packet if it was parsed non-recursively.
+ *
+ */
+ public static class Iso7816CompositeTLV extends Iso7816TLV {
+
+ public final Iso7816TLV[] mSubs;
+
+ public Iso7816CompositeTLV(int T, int L, byte[] V, Iso7816TLV[] subs) {
+ super(T, L, V);
+
+ mSubs = subs;
+ }
+
+ public String prettyPrint(int indent) {
+ StringBuilder result = new StringBuilder();
+ // lol
+ result.append(" ".substring(0, indent*2));
+ result.append(String.format("composite tag T %4x L %04d", mT, mL));
+ for (Iso7816TLV sub : mSubs) {
+ result.append('\n');
+ result.append(sub.prettyPrint(indent+1));
+ }
+ return result.toString();
+ }
+
+ }
+
+ /** Recursively searches for a specific tag in a composite TLV packet structure, depth first. */
+ public static Iso7816TLV findRecursive(Iso7816TLV in, int tag) {
+ if (in.mT == tag) {
+ return in;
+ } else if (in instanceof Iso7816CompositeTLV) {
+ for (Iso7816TLV sub : ((Iso7816CompositeTLV) in).mSubs) {
+ Iso7816TLV result = findRecursive(sub, tag);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+
+}