aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--OpenKeychain/src/main/AndroidManifest.xml23
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java34
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java65
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java113
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java755
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java58
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java304
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java37
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java18
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java53
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java15
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java11
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java160
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UsbEventReceiverActivity.java59
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java33
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java860
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/UsbConnectionDispatcher.java97
-rw-r--r--OpenKeychain/src/main/res/xml/usb_device_filter.xml27
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java11
21 files changed, 1847 insertions, 898 deletions
diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml
index 74bf936b4..c6795d9f7 100644
--- a/OpenKeychain/src/main/AndroidManifest.xml
+++ b/OpenKeychain/src/main/AndroidManifest.xml
@@ -43,6 +43,11 @@
android:name="android.hardware.screen.portrait"
android:required="false" />
+ <!-- For OTG tokens -->
+ <uses-feature
+ android:name="android.hardware.usb.host"
+ android:required="false" />
+
<!-- TemporaryStorageProvider should be writable by OpenKeychain only, thus signature-level permission -->
<permission
android:name="${applicationId}.WRITE_TEMPORARY_STORAGE"
@@ -872,6 +877,24 @@
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_import_keys" />
+ <!-- Usb interceptor activity -->
+ <activity
+ android:name=".ui.UsbEventReceiverActivity"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.Keychain.Transparent"
+ android:noHistory="true"
+ android:excludeFromRecents="true"
+ android:taskAffinity="com.example.taskAffinityUsbEventReceiver"
+ android:process=":UsbEventReceiverActivityProcess"
+ android:exported="false">
+
+ <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
+ android:resource="@xml/usb_device_filter" />
+ <intent-filter>
+ <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
+ </intent-filter>
+ </activity>
+
<!-- DEPRECATED service,
using this service may lead to truncated data being returned to the caller -->
<service
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java
index 690a4d1a2..46bade950 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java
@@ -50,9 +50,9 @@ public class ApiPendingIntentFactory {
CryptoInputParcel cryptoInput) {
switch (requiredInput.mType) {
- case NFC_MOVE_KEY_TO_CARD:
- case NFC_DECRYPT:
- case NFC_SIGN: {
+ case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
+ case SECURITY_TOKEN_DECRYPT:
+ case SECURITY_TOKEN_SIGN: {
return createNfcOperationPendingIntent(data, requiredInput, cryptoInput);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java
new file mode 100644
index 000000000..905deca90
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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 org.sufficientlysecure.keychain.securitytoken;
+
+import java.io.IOException;
+
+public class CardException extends IOException {
+ private short mResponseCode;
+
+ public CardException(String detailMessage, short responseCode) {
+ super(detailMessage);
+ mResponseCode = responseCode;
+ }
+
+ public short getResponseCode() {
+ return mResponseCode;
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java
new file mode 100644
index 000000000..0e28f022b
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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 org.sufficientlysecure.keychain.securitytoken;
+
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
+
+public enum KeyType {
+ SIGN(0, 0xB6, 0xCE, 0xC7),
+ ENCRYPT(1, 0xB8, 0xCF, 0xC8),
+ AUTH(2, 0xA4, 0xD0, 0xC9),;
+
+ private final int mIdx;
+ private final int mSlot;
+ private final int mTimestampObjectId;
+ private final int mFingerprintObjectId;
+
+ KeyType(final int idx, final int slot, final int timestampObjectId, final int fingerprintObjectId) {
+ this.mIdx = idx;
+ this.mSlot = slot;
+ this.mTimestampObjectId = timestampObjectId;
+ this.mFingerprintObjectId = fingerprintObjectId;
+ }
+
+ public static KeyType from(final CanonicalizedSecretKey key) {
+ if (key.canSign() || key.canCertify()) {
+ return SIGN;
+ } else if (key.canEncrypt()) {
+ return ENCRYPT;
+ } else if (key.canAuthenticate()) {
+ return AUTH;
+ }
+ return null;
+ }
+
+ public int getIdx() {
+ return mIdx;
+ }
+
+ public int getmSlot() {
+ return mSlot;
+ }
+
+ public int getTimestampObjectId() {
+ return mTimestampObjectId;
+ }
+
+ public int getmFingerprintObjectId() {
+ return mFingerprintObjectId;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java
new file mode 100644
index 000000000..3b2dd838d
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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 org.sufficientlysecure.keychain.securitytoken;
+
+import android.nfc.Tag;
+
+import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
+
+import java.io.IOException;
+
+import nordpol.IsoCard;
+import nordpol.android.AndroidCard;
+
+public class NfcTransport implements Transport {
+ // timeout is set to 100 seconds to avoid cancellation during calculation
+ private static final int TIMEOUT = 100 * 1000;
+ private final Tag mTag;
+ private IsoCard mIsoCard;
+
+ public NfcTransport(Tag tag) {
+ this.mTag = tag;
+ }
+
+ /**
+ * Transmit and receive data
+ * @param data data to transmit
+ * @return received data
+ * @throws IOException
+ */
+ @Override
+ public byte[] transceive(final byte[] data) throws IOException {
+ return mIsoCard.transceive(data);
+ }
+
+ /**
+ * Disconnect and release connection
+ */
+ @Override
+ public void release() {
+ // Not supported
+ }
+
+ @Override
+ public boolean isConnected() {
+ return mIsoCard != null && mIsoCard.isConnected();
+ }
+
+ /**
+ * Check if Transport supports persistent connections e.g connections which can
+ * handle multiple operations in one session
+ * @return true if transport supports persistent connections
+ */
+ @Override
+ public boolean isPersistentConnectionAllowed() {
+ return false;
+ }
+
+ /**
+ * Connect to NFC device.
+ * <p/>
+ * On general communication, see also
+ * http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-a.aspx
+ * <p/>
+ * References to pages are generally related to the OpenPGP Application
+ * on ISO SmartCard Systems specification.
+ */
+ @Override
+ public void connect() throws IOException {
+ mIsoCard = AndroidCard.get(mTag);
+ if (mIsoCard == null) {
+ throw new BaseSecurityTokenNfcActivity.IsoDepNotSupportedException("Tag does not support ISO-DEP (ISO 14443-4)");
+ }
+
+ mIsoCard.setTimeout(TIMEOUT);
+ mIsoCard.connect();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final NfcTransport that = (NfcTransport) o;
+
+ if (mTag != null ? !mTag.equals(that.mTag) : that.mTag != null) return false;
+ if (mIsoCard != null ? !mIsoCard.equals(that.mIsoCard) : that.mIsoCard != null)
+ return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mTag != null ? mTag.hashCode() : 0;
+ result = 31 * result + (mIsoCard != null ? mIsoCard.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java
new file mode 100644
index 000000000..e3f280e18
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java
@@ -0,0 +1,755 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ * Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ * Copyright (C) 2013-2014 Signe Rüsch
+ * Copyright (C) 2013-2014 Philipp Jakubeit
+ *
+ * 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 org.sufficientlysecure.keychain.securitytoken;
+
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.util.Arrays;
+import org.bouncycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.util.Iso7816TLV;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.interfaces.RSAPrivateCrtKey;
+
+import nordpol.Apdu;
+
+/**
+ * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant
+ * devices.
+ * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
+ */
+public class SecurityTokenHelper {
+ // Fidesmo constants
+ private static final String FIDESMO_APPS_AID_PREFIX = "A000000617";
+
+ private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+ private Transport mTransport;
+
+ private Passphrase mPin;
+ private Passphrase mAdminPin;
+ private boolean mPw1ValidForMultipleSignatures;
+ private boolean mPw1ValidatedForSignature;
+ private boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming?
+ private boolean mPw3Validated;
+
+ protected SecurityTokenHelper() {
+ }
+
+ public static SecurityTokenHelper getInstance() {
+ return LazyHolder.SECURITY_TOKEN_HELPER;
+ }
+
+ private static String getHex(byte[] raw) {
+ return new String(Hex.encode(raw));
+ }
+
+ private String getHolderName(String name) {
+ try {
+ 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;
+ } catch (IndexOutOfBoundsException e) {
+ // try-catch for https://github.com/FluffyKaon/OpenPGP-Card
+ // Note: This should not happen, but happens with
+ // https://github.com/FluffyKaon/OpenPGP-Card, thus return an empty string for now!
+
+ Log.e(Constants.TAG, "Couldn't get holder name, returning empty string!", e);
+ return "";
+ }
+ }
+
+ public Passphrase getPin() {
+ return mPin;
+ }
+
+ public void setPin(final Passphrase pin) {
+ this.mPin = pin;
+ }
+
+ public Passphrase getAdminPin() {
+ return mAdminPin;
+ }
+
+ public void setAdminPin(final Passphrase adminPin) {
+ this.mAdminPin = adminPin;
+ }
+
+ public void changeKey(CanonicalizedSecretKey secretKey, Passphrase passphrase) throws IOException {
+ long keyGenerationTimestamp = secretKey.getCreationTime().getTime() / 1000;
+ byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
+ KeyType keyType = KeyType.from(secretKey);
+
+ if (keyType == null) {
+ throw new IOException("Inappropriate key flags for smart card key.");
+ }
+
+ // Slot is empty, or contains this key already. PUT KEY operation is safe
+ boolean canPutKey = isSlotEmpty(keyType)
+ || keyMatchesFingerPrint(keyType, secretKey.getFingerprint());
+
+ if (!canPutKey) {
+ throw new IOException(String.format("Key slot occupied; card must be reset to put new %s key.",
+ keyType.toString()));
+ }
+
+ putKey(keyType.getmSlot(), secretKey, passphrase);
+ putData(keyType.getmFingerprintObjectId(), secretKey.getFingerprint());
+ putData(keyType.getTimestampObjectId(), timestampBytes);
+ }
+
+ private boolean isSlotEmpty(KeyType keyType) throws IOException {
+ // Note: special case: This should not happen, but happens with
+ // https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true
+ if (getMasterKeyFingerprint(keyType.getIdx()) == null) return true;
+
+ return keyMatchesFingerPrint(keyType, BLANK_FINGERPRINT);
+ }
+
+ public boolean keyMatchesFingerPrint(KeyType keyType, byte[] fingerprint) throws IOException {
+ return java.util.Arrays.equals(getMasterKeyFingerprint(keyType.getIdx()), fingerprint);
+ }
+
+ /**
+ * Connect to device and select pgp applet
+ *
+ * @throws IOException
+ */
+ public void connectToDevice() throws IOException {
+ // Connect on transport layer
+ mTransport.connect();
+
+ // Connect on smartcard layer
+
+ // 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
+ String response = communicate(opening); // activate connection
+ if (!response.endsWith(accepted)) {
+ throw new CardException("Initialization failed!", parseCardStatus(response));
+ }
+
+ byte[] pwStatusBytes = getPwStatusBytes();
+ mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1);
+ mPw1ValidatedForSignature = false;
+ mPw1ValidatedForDecrypt = false;
+ mPw3Validated = false;
+ }
+
+ /**
+ * Parses out the status word from a JavaCard response string.
+ *
+ * @param response A hex string with the response from the card
+ * @return A short indicating the SW1/SW2, or 0 if a status could not be determined.
+ */
+ private short parseCardStatus(String response) {
+ if (response.length() < 4) {
+ return 0; // invalid input
+ }
+
+ try {
+ return Short.parseShort(response.substring(response.length() - 4), 16);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for
+ * conformance to the token's requirements for key length.
+ *
+ * @param pw For PW1, this is 0x81. For PW3 (Admin PIN), mode is 0x83.
+ * @param newPin The new PW1 or PW3.
+ */
+ public void modifyPin(int pw, byte[] newPin) throws IOException {
+ final int MAX_PW1_LENGTH_INDEX = 1;
+ final int MAX_PW3_LENGTH_INDEX = 3;
+
+ byte[] pwStatusBytes = getPwStatusBytes();
+
+ if (pw == 0x81) {
+ if (newPin.length < 6 || newPin.length > pwStatusBytes[MAX_PW1_LENGTH_INDEX]) {
+ throw new IOException("Invalid PIN length");
+ }
+ } else if (pw == 0x83) {
+ if (newPin.length < 8 || newPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) {
+ throw new IOException("Invalid PIN length");
+ }
+ } else {
+ throw new IOException("Invalid PW index for modify PIN operation");
+ }
+
+ byte[] pin;
+ if (pw == 0x83) {
+ pin = mAdminPin.toStringUnsafe().getBytes();
+ } else {
+ pin = mPin.toStringUnsafe().getBytes();
+ }
+
+ // Command APDU for CHANGE REFERENCE DATA command (page 32)
+ String changeReferenceDataApdu = "00" // CLA
+ + "24" // INS
+ + "00" // P1
+ + String.format("%02x", pw) // P2
+ + String.format("%02x", pin.length + newPin.length) // Lc
+ + getHex(pin)
+ + getHex(newPin);
+ String response = communicate(changeReferenceDataApdu); // change PIN
+ if (!response.equals("9000")) {
+ throw new CardException("Failed to change PIN", parseCardStatus(response));
+ }
+ }
+
+ /**
+ * Call DECIPHER command
+ *
+ * @param encryptedSessionKey the encoded session key
+ * @return the decoded session key
+ */
+ public byte[] decryptSessionKey(byte[] encryptedSessionKey) throws IOException {
+ if (!mPw1ValidatedForDecrypt) {
+ verifyPin(0x82); // (Verify PW1 with mode 82 for decryption)
+ }
+
+ String firstApdu = "102a8086fe";
+ String secondApdu = "002a808603";
+ String le = "00";
+
+ byte[] one = new byte[254];
+ // leave out first byte:
+ System.arraycopy(encryptedSessionKey, 1, one, 0, one.length);
+
+ byte[] two = new byte[encryptedSessionKey.length - 1 - one.length];
+ for (int i = 0; i < two.length; i++) {
+ two[i] = encryptedSessionKey[i + one.length + 1];
+ }
+
+ communicate(firstApdu + getHex(one));
+ String second = communicate(secondApdu + getHex(two) + le);
+
+ String decryptedSessionKey = getDataField(second);
+
+ return Hex.decode(decryptedSessionKey);
+ }
+
+ /**
+ * Verifies the user's PW1 or PW3 with the appropriate mode.
+ *
+ * @param mode For PW1, this is 0x81 for signing, 0x82 for everything else.
+ * For PW3 (Admin PIN), mode is 0x83.
+ */
+ private void verifyPin(int mode) throws IOException {
+ if (mPin != null || mode == 0x83) {
+
+ byte[] pin;
+ if (mode == 0x83) {
+ pin = mAdminPin.toStringUnsafe().getBytes();
+ } else {
+ pin = mPin.toStringUnsafe().getBytes();
+ }
+
+ // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
+ // See specification, page 51
+ String accepted = "9000";
+ String response = tryPin(mode, pin); // login
+ if (!response.equals(accepted)) {
+ throw new CardException("Bad PIN!", parseCardStatus(response));
+ }
+
+ if (mode == 0x81) {
+ mPw1ValidatedForSignature = true;
+ } else if (mode == 0x82) {
+ mPw1ValidatedForDecrypt = true;
+ } else if (mode == 0x83) {
+ mPw3Validated = true;
+ }
+ }
+ }
+
+ /**
+ * Stores a data object on the token. Automatically validates the proper PIN for the operation.
+ * Supported for all data objects < 255 bytes in length. Only the cardholder certificate
+ * (0x7F21) can exceed this length.
+ *
+ * @param dataObject The data object to be stored.
+ * @param data The data to store in the object
+ */
+ private void putData(int dataObject, byte[] data) throws IOException {
+ if (data.length > 254) {
+ throw new IOException("Cannot PUT DATA with length > 254");
+ }
+ if (dataObject == 0x0101 || dataObject == 0x0103) {
+ if (!mPw1ValidatedForDecrypt) {
+ verifyPin(0x82); // (Verify PW1 for non-signing operations)
+ }
+ } else if (!mPw3Validated) {
+ verifyPin(0x83); // (Verify PW3)
+ }
+
+ String putDataApdu = "00" // CLA
+ + "DA" // INS
+ + String.format("%02x", (dataObject & 0xFF00) >> 8) // P1
+ + String.format("%02x", dataObject & 0xFF) // P2
+ + String.format("%02x", data.length) // Lc
+ + getHex(data);
+
+ String response = communicate(putDataApdu); // put data
+ if (!response.equals("9000")) {
+ throw new CardException("Failed to put data.", parseCardStatus(response));
+ }
+ }
+
+
+ /**
+ * Puts a key on the token in the given slot.
+ *
+ * @param slot The slot on the token where the key should be stored:
+ * 0xB6: Signature Key
+ * 0xB8: Decipherment Key
+ * 0xA4: Authentication Key
+ */
+ private void putKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
+ throws IOException {
+ if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
+ throw new IOException("Invalid key slot");
+ }
+
+ RSAPrivateCrtKey crtSecretKey;
+ try {
+ secretKey.unlock(passphrase);
+ crtSecretKey = secretKey.getCrtSecretKey();
+ } catch (PgpGeneralException e) {
+ throw new IOException(e.getMessage());
+ }
+
+ // Shouldn't happen; the UI should block the user from getting an incompatible key this far.
+ if (crtSecretKey.getModulus().bitLength() > 2048) {
+ throw new IOException("Key too large to export to Security Token.");
+ }
+
+ // Should happen only rarely; all GnuPG keys since 2006 use public exponent 65537.
+ if (!crtSecretKey.getPublicExponent().equals(new BigInteger("65537"))) {
+ throw new IOException("Invalid public exponent for smart Security Token.");
+ }
+
+ if (!mPw3Validated) {
+ verifyPin(0x83); // (Verify PW3 with mode 83)
+ }
+
+ byte[] header = Hex.decode(
+ "4D82" + "03A2" // Extended header list 4D82, length of 930 bytes. (page 23)
+ + String.format("%02x", slot) + "00" // CRT to indicate targeted key, no length
+ + "7F48" + "15" // Private key template 0x7F48, length 21 (decimal, 0x15 hex)
+ + "9103" // Public modulus, length 3
+ + "928180" // Prime P, length 128
+ + "938180" // Prime Q, length 128
+ + "948180" // Coefficient (1/q mod p), length 128
+ + "958180" // Prime exponent P (d mod (p - 1)), length 128
+ + "968180" // Prime exponent Q (d mod (1 - 1)), length 128
+ + "97820100" // Modulus, length 256, last item in private key template
+ + "5F48" + "820383");// DO 5F48; 899 bytes of concatenated key data will follow
+ byte[] dataToSend = new byte[934];
+ byte[] currentKeyObject;
+ int offset = 0;
+
+ System.arraycopy(header, 0, dataToSend, offset, header.length);
+ offset += header.length;
+ currentKeyObject = crtSecretKey.getPublicExponent().toByteArray();
+ System.arraycopy(currentKeyObject, 0, dataToSend, offset, 3);
+ offset += 3;
+ // NOTE: For a 2048-bit key, these lengths are fixed. However, bigint includes a leading 0
+ // in the array to represent sign, so we take care to set the offset to 1 if necessary.
+ currentKeyObject = crtSecretKey.getPrimeP().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getPrimeQ().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getCrtCoefficient().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getPrimeExponentP().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getPrimeExponentQ().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getModulus().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 256, dataToSend, offset, 256);
+
+ String putKeyCommand = "10DB3FFF";
+ String lastPutKeyCommand = "00DB3FFF";
+
+ // Now we're ready to communicate with the token.
+ offset = 0;
+ String response;
+ while (offset < dataToSend.length) {
+ int dataRemaining = dataToSend.length - offset;
+ if (dataRemaining > 254) {
+ response = communicate(
+ putKeyCommand + "FE" + Hex.toHexString(dataToSend, offset, 254)
+ );
+ offset += 254;
+ } else {
+ int length = dataToSend.length - offset;
+ response = communicate(
+ lastPutKeyCommand + String.format("%02x", length)
+ + Hex.toHexString(dataToSend, offset, length));
+ offset += length;
+ }
+
+ if (!response.endsWith("9000")) {
+ throw new CardException("Key export to Security Token failed", parseCardStatus(response));
+ }
+ }
+
+ // Clear array with secret data before we return.
+ Arrays.fill(dataToSend, (byte) 0);
+ }
+
+ /**
+ * Return fingerprints of all keys from application specific data stored
+ * on tag, or null if data not available.
+ *
+ * @return The fingerprints of all subkeys in a contiguous byte array.
+ */
+ public byte[] getFingerprints() throws IOException {
+ String data = "00CA006E00";
+ byte[] buf = mTransport.transceive(Hex.decode(data));
+
+ Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true);
+ Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint());
+
+ Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5);
+ if (fptlv == null) {
+ return null;
+ }
+ return fptlv.mV;
+ }
+
+ /**
+ * Return the PW Status Bytes from the token. This is a simple DO; no TLV decoding needed.
+ *
+ * @return Seven bytes in fixed format, plus 0x9000 status word at the end.
+ */
+ private byte[] getPwStatusBytes() throws IOException {
+ String data = "00CA00C400";
+ return mTransport.transceive(Hex.decode(data));
+ }
+
+ public byte[] getAid() throws IOException {
+ String info = "00CA004F00";
+ return mTransport.transceive(Hex.decode(info));
+ }
+
+ public String getUserId() throws IOException {
+ String info = "00CA006500";
+ return getHolderName(communicate(info));
+ }
+
+ /**
+ * Call COMPUTE DIGITAL SIGNATURE command and returns the MPI value
+ *
+ * @param hash the hash for signing
+ * @return a big integer representing the MPI for the given hash
+ */
+ public byte[] calculateSignature(byte[] hash, int hashAlgo) throws IOException {
+ if (!mPw1ValidatedForSignature) {
+ verifyPin(0x81); // (Verify PW1 with mode 81 for signing)
+ }
+
+ // dsi, including Lc
+ String dsi;
+
+ Log.i(Constants.TAG, "Hash: " + hashAlgo);
+ switch (hashAlgo) {
+ case HashAlgorithmTags.SHA1:
+ if (hash.length != 20) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 10!");
+ }
+ dsi = "23" // Lc
+ + "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
+ + "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
+ + "0605" + "2B0E03021A" // OID of SHA1
+ + "0500" // TLV coding of ZERO
+ + "0414" + getHex(hash); // 0x14 are 20 hash bytes
+ break;
+ case HashAlgorithmTags.RIPEMD160:
+ if (hash.length != 20) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 20!");
+ }
+ dsi = "233021300906052B2403020105000414" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA224:
+ if (hash.length != 28) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 28!");
+ }
+ dsi = "2F302D300D06096086480165030402040500041C" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA256:
+ if (hash.length != 32) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 32!");
+ }
+ dsi = "333031300D060960864801650304020105000420" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA384:
+ if (hash.length != 48) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 48!");
+ }
+ dsi = "433041300D060960864801650304020205000430" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA512:
+ if (hash.length != 64) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 64!");
+ }
+ dsi = "533051300D060960864801650304020305000440" + getHex(hash);
+ break;
+ default:
+ throw new IOException("Not supported hash algo!");
+ }
+
+ // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
+ String apdu =
+ "002A9E9A" // CLA, INS, P1, P2
+ + dsi // digital signature input
+ + "00"; // Le
+
+ String response = communicate(apdu);
+
+ if (response.length() < 4) {
+ throw new CardException("Bad response", (short) 0);
+ }
+ // split up response into signature and status
+ String status = response.substring(response.length() - 4);
+ String signature = response.substring(0, response.length() - 4);
+
+ // while we are getting 0x61 status codes, retrieve more data
+ while (status.substring(0, 2).equals("61")) {
+ Log.d(Constants.TAG, "requesting more data, status " + status);
+ // Send GET RESPONSE command
+ response = communicate("00C00000" + status.substring(2));
+ status = response.substring(response.length() - 4);
+ signature += response.substring(0, response.length() - 4);
+ }
+
+ Log.d(Constants.TAG, "final response:" + status);
+
+ if (!mPw1ValidForMultipleSignatures) {
+ mPw1ValidatedForSignature = false;
+ }
+
+ if (!"9000".equals(status)) {
+ throw new CardException("Bad NFC response code: " + status, parseCardStatus(response));
+ }
+
+ // Make sure the signature we received is actually the expected number of bytes long!
+ if (signature.length() != 256 && signature.length() != 512) {
+ throw new IOException("Bad signature length! Expected 128 or 256 bytes, got " + signature.length() / 2);
+ }
+
+ return Hex.decode(signature);
+ }
+
+ /**
+ * Transceive data via NFC encoded as Hex
+ */
+ private String communicate(String apdu) throws IOException {
+ return getHex(mTransport.transceive(Hex.decode(apdu)));
+ }
+
+ public Transport getTransport() {
+ return mTransport;
+ }
+
+ public void setTransport(Transport mTransport) {
+ this.mTransport = mTransport;
+ }
+
+ public boolean isFidesmoToken() {
+ if (isConnected()) { // Check if we can still talk to the card
+ try {
+ // By trying to select any apps that have the Fidesmo AID prefix we can
+ // see if it is a Fidesmo device or not
+ byte[] mSelectResponse = mTransport.transceive(Apdu.select(FIDESMO_APPS_AID_PREFIX));
+ // Compare the status returned by our select with the OK status code
+ return Apdu.hasStatus(mSelectResponse, Apdu.OK_APDU);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Card communication failed!", e);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Generates a key on the card in the given slot. If the slot is 0xB6 (the signature key),
+ * this command also has the effect of resetting the digital signature counter.
+ * NOTE: This does not set the key fingerprint data object! After calling this command, you
+ * must construct a public key packet using the returned public key data objects, compute the
+ * key fingerprint, and store it on the card using: putData(0xC8, key.getFingerprint())
+ *
+ * @param slot The slot on the card where the key should be generated:
+ * 0xB6: Signature Key
+ * 0xB8: Decipherment Key
+ * 0xA4: Authentication Key
+ * @return the public key data objects, in TLV format. For RSA this will be the public modulus
+ * (0x81) and exponent (0x82). These may come out of order; proper TLV parsing is required.
+ */
+ public byte[] generateKey(int slot) throws IOException {
+ if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
+ throw new IOException("Invalid key slot");
+ }
+
+ if (!mPw3Validated) {
+ verifyPin(0x83); // (Verify PW3 with mode 83)
+ }
+
+ String generateKeyApdu = "0047800002" + String.format("%02x", slot) + "0000";
+ String getResponseApdu = "00C00000";
+
+ String first = communicate(generateKeyApdu);
+ String second = communicate(getResponseApdu);
+
+ if (!second.endsWith("9000")) {
+ throw new IOException("On-card key generation failed");
+ }
+
+ String publicKeyData = getDataField(first) + getDataField(second);
+
+ Log.d(Constants.TAG, "Public Key Data Objects: " + publicKeyData);
+
+ return Hex.decode(publicKeyData);
+ }
+
+ private String getDataField(String output) {
+ return output.substring(0, output.length() - 4);
+ }
+
+ private String tryPin(int mode, byte[] pin) throws IOException {
+ // Command APDU for VERIFY command (page 32)
+ String login =
+ "00" // CLA
+ + "20" // INS
+ + "00" // P1
+ + String.format("%02x", mode) // P2
+ + String.format("%02x", pin.length) // Lc
+ + Hex.toHexString(pin);
+
+ return communicate(login);
+ }
+
+ /**
+ * Resets security token, which deletes all keys and data objects.
+ * This works by entering a wrong PIN and then Admin PIN 4 times respectively.
+ * Afterwards, the token is reactivated.
+ */
+ public void resetAndWipeToken() throws IOException {
+ String accepted = "9000";
+
+ // try wrong PIN 4 times until counter goes to C0
+ byte[] pin = "XXXXXX".getBytes();
+ for (int i = 0; i <= 4; i++) {
+ String response = tryPin(0x81, pin);
+ if (response.equals(accepted)) { // Should NOT accept!
+ throw new CardException("Should never happen, XXXXXX has been accepted!", parseCardStatus(response));
+ }
+ }
+
+ // try wrong Admin PIN 4 times until counter goes to C0
+ byte[] adminPin = "XXXXXXXX".getBytes();
+ for (int i = 0; i <= 4; i++) {
+ String response = tryPin(0x83, adminPin);
+ if (response.equals(accepted)) { // Should NOT accept!
+ throw new CardException("Should never happen, XXXXXXXX has been accepted", parseCardStatus(response));
+ }
+ }
+
+ // reactivate token!
+ String reactivate1 = "00" + "e6" + "00" + "00";
+ String reactivate2 = "00" + "44" + "00" + "00";
+ String response1 = communicate(reactivate1);
+ String response2 = communicate(reactivate2);
+ if (!response1.equals(accepted) || !response2.equals(accepted)) {
+ throw new CardException("Reactivating failed!", parseCardStatus(response1));
+ }
+
+ }
+
+ /**
+ * Return the fingerprint from application specific data stored on tag, or
+ * null if it doesn't exist.
+ *
+ * @param idx Index of the key to return the fingerprint from.
+ * @return The fingerprint of the requested key, or null if not found.
+ */
+ public byte[] getMasterKeyFingerprint(int idx) throws IOException {
+ byte[] data = getFingerprints();
+ if (data == null) {
+ return null;
+ }
+
+ // return the master key fingerprint
+ ByteBuffer fpbuf = ByteBuffer.wrap(data);
+ byte[] fp = new byte[20];
+ fpbuf.position(idx * 20);
+ fpbuf.get(fp, 0, 20);
+
+ return fp;
+ }
+
+ public boolean isPersistentConnectionAllowed() {
+ return mTransport != null && mTransport.isPersistentConnectionAllowed();
+ }
+
+ public boolean isConnected() {
+ return mTransport != null && mTransport.isConnected();
+ }
+
+ private static class LazyHolder {
+ private static final SecurityTokenHelper SECURITY_TOKEN_HELPER = new SecurityTokenHelper();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java
new file mode 100644
index 000000000..294eaa9ee
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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 org.sufficientlysecure.keychain.securitytoken;
+
+import java.io.IOException;
+
+/**
+ * Abstraction for transmitting APDU commands
+ */
+public interface Transport {
+ /**
+ * Transmit and receive data
+ * @param data data to transmit
+ * @return received data
+ * @throws IOException
+ */
+ byte[] transceive(byte[] data) throws IOException;
+
+ /**
+ * Disconnect and release connection
+ */
+ void release();
+
+ /**
+ * Check if device is was connected to and still is connected
+ * @return connection status
+ */
+ boolean isConnected();
+
+ /**
+ * Check if Transport supports persistent connections e.g connections which can
+ * handle multiple operations in one session
+ * @return true if transport supports persistent connections
+ */
+ boolean isPersistentConnectionAllowed();
+
+
+ /**
+ * Connect to device
+ * @throws IOException
+ */
+ void connect() throws IOException;
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java
new file mode 100644
index 000000000..dfe91427e
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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 org.sufficientlysecure.keychain.securitytoken;
+
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbEndpoint;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+
+import org.bouncycastle.util.Arrays;
+import org.bouncycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Based on USB CCID Specification rev. 1.1
+ * http://www.usb.org/developers/docs/devclass_docs/DWG_Smart-Card_CCID_Rev110.pdf
+ * Implements small subset of these features
+ */
+public class UsbTransport implements Transport {
+ private static final int USB_CLASS_SMARTCARD = 11;
+ private static final int TIMEOUT = 20 * 1000; // 20s
+
+ private final UsbManager mUsbManager;
+ private final UsbDevice mUsbDevice;
+ private UsbInterface mUsbInterface;
+ private UsbEndpoint mBulkIn;
+ private UsbEndpoint mBulkOut;
+ private UsbDeviceConnection mConnection;
+ private byte mCounter;
+
+ public UsbTransport(UsbDevice usbDevice, UsbManager usbManager) {
+ mUsbDevice = usbDevice;
+ mUsbManager = usbManager;
+ }
+
+
+ /**
+ * Manage ICC power, Yubikey requires to power on ICC
+ * Spec: 6.1.1 PC_to_RDR_IccPowerOn; 6.1.2 PC_to_RDR_IccPowerOff
+ *
+ * @param on true to turn ICC on, false to turn it off
+ * @throws UsbTransportException
+ */
+ private void setIccPower(boolean on) throws UsbTransportException {
+ final byte[] iccPowerCommand = {
+ (byte) (on ? 0x62 : 0x63),
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00,
+ mCounter++,
+ 0x00,
+ 0x00, 0x00
+ };
+
+ sendRaw(iccPowerCommand);
+ byte[] bytes;
+ do {
+ bytes = receive();
+ } while (isDataBlockNotReady(bytes));
+ checkDataBlockResponse(bytes);
+ }
+
+ /**
+ * Get first class 11 (Chip/Smartcard) interface of the device
+ *
+ * @param device {@link UsbDevice} which will be searched
+ * @return {@link UsbInterface} of smartcard or null if it doesn't exist
+ */
+ @Nullable
+ private static UsbInterface getSmartCardInterface(UsbDevice device) {
+ for (int i = 0; i < device.getInterfaceCount(); i++) {
+ UsbInterface anInterface = device.getInterface(i);
+ if (anInterface.getInterfaceClass() == USB_CLASS_SMARTCARD) {
+ return anInterface;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get device's bulk-in and bulk-out endpoints
+ *
+ * @param usbInterface usb device interface
+ * @return pair of builk-in and bulk-out endpoints respectively
+ */
+ @NonNull
+ private static Pair<UsbEndpoint, UsbEndpoint> getIoEndpoints(final UsbInterface usbInterface) {
+ UsbEndpoint bulkIn = null, bulkOut = null;
+ for (int i = 0; i < usbInterface.getEndpointCount(); i++) {
+ final UsbEndpoint endpoint = usbInterface.getEndpoint(i);
+ if (endpoint.getType() != UsbConstants.USB_ENDPOINT_XFER_BULK) {
+ continue;
+ }
+
+ if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) {
+ bulkIn = endpoint;
+ } else if (endpoint.getDirection() == UsbConstants.USB_DIR_OUT) {
+ bulkOut = endpoint;
+ }
+ }
+ return new Pair<>(bulkIn, bulkOut);
+ }
+
+ /**
+ * Release interface and disconnect
+ */
+ @Override
+ public void release() {
+ if (mConnection != null) {
+ mConnection.releaseInterface(mUsbInterface);
+ mConnection.close();
+ mConnection = null;
+ }
+
+ Log.d(Constants.TAG, "Usb transport disconnected");
+ }
+
+ /**
+ * Check if device is was connected to and still is connected
+ * @return true if device is connected
+ */
+ @Override
+ public boolean isConnected() {
+ return mConnection != null && mUsbManager.getDeviceList().containsValue(mUsbDevice) &&
+ mConnection.getSerial() != null;
+ }
+
+ /**
+ * Check if Transport supports persistent connections e.g connections which can
+ * handle multiple operations in one session
+ * @return true if transport supports persistent connections
+ */
+ @Override
+ public boolean isPersistentConnectionAllowed() {
+ return true;
+ }
+
+ /**
+ * Connect to OTG device
+ * @throws IOException
+ */
+ @Override
+ public void connect() throws IOException {
+ mCounter = 0;
+ mUsbInterface = getSmartCardInterface(mUsbDevice);
+ if (mUsbInterface == null) {
+ // Shouldn't happen as we whitelist only class 11 devices
+ throw new UsbTransportException("USB error - device doesn't have class 11 interface");
+ }
+
+ final Pair<UsbEndpoint, UsbEndpoint> ioEndpoints = getIoEndpoints(mUsbInterface);
+ mBulkIn = ioEndpoints.first;
+ mBulkOut = ioEndpoints.second;
+
+ if (mBulkIn == null || mBulkOut == null) {
+ throw new UsbTransportException("USB error - invalid class 11 interface");
+ }
+
+ mConnection = mUsbManager.openDevice(mUsbDevice);
+ if (mConnection == null) {
+ throw new UsbTransportException("USB error - failed to connect to device");
+ }
+
+ if (!mConnection.claimInterface(mUsbInterface, true)) {
+ throw new UsbTransportException("USB error - failed to claim interface");
+ }
+
+ setIccPower(true);
+ Log.d(Constants.TAG, "Usb transport connected");
+ }
+
+ /**
+ * Transmit and receive data
+ * @param data data to transmit
+ * @return received data
+ * @throws UsbTransportException
+ */
+ @Override
+ public byte[] transceive(byte[] data) throws UsbTransportException {
+ sendXfrBlock(data);
+ byte[] bytes;
+ do {
+ bytes = receive();
+ } while (isDataBlockNotReady(bytes));
+
+ checkDataBlockResponse(bytes);
+ // Discard header
+ return Arrays.copyOfRange(bytes, 10, bytes.length);
+ }
+
+ /**
+ * Transmits XfrBlock
+ * 6.1.4 PC_to_RDR_XfrBlock
+ * @param payload payload to transmit
+ * @throws UsbTransportException
+ */
+ private void sendXfrBlock(byte[] payload) throws UsbTransportException {
+ int l = payload.length;
+ byte[] data = Arrays.concatenate(new byte[]{
+ 0x6f,
+ (byte) l, (byte) (l >> 8), (byte) (l >> 16), (byte) (l >> 24),
+ 0x00,
+ mCounter++,
+ 0x00,
+ 0x00, 0x00},
+ payload);
+
+ int send = 0;
+ while (send < data.length) {
+ final int len = Math.min(mBulkIn.getMaxPacketSize(), data.length - send);
+ sendRaw(Arrays.copyOfRange(data, send, send + len));
+ send += len;
+ }
+ }
+
+ private byte[] receive() throws UsbTransportException {
+ byte[] buffer = new byte[mBulkIn.getMaxPacketSize()];
+ byte[] result = null;
+ int readBytes = 0, totalBytes = 0;
+
+ do {
+ int res = mConnection.bulkTransfer(mBulkIn, buffer, buffer.length, TIMEOUT);
+ if (res < 0) {
+ throw new UsbTransportException("USB error - failed to receive response " + res);
+ }
+ if (result == null) {
+ if (res < 10) {
+ throw new UsbTransportException("USB-CCID error - failed to receive CCID header");
+ }
+ totalBytes = ByteBuffer.wrap(buffer, 1, 4).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get() + 10;
+ result = new byte[totalBytes];
+ }
+ System.arraycopy(buffer, 0, result, readBytes, res);
+ readBytes += res;
+ } while (readBytes < totalBytes);
+
+ return result;
+ }
+
+ private void sendRaw(final byte[] data) throws UsbTransportException {
+ final int tr1 = mConnection.bulkTransfer(mBulkOut, data, data.length, TIMEOUT);
+ if (tr1 != data.length) {
+ throw new UsbTransportException("USB error - failed to transmit data " + tr1);
+ }
+ }
+
+ private byte getStatus(byte[] bytes) {
+ return (byte) ((bytes[7] >> 6) & 0x03);
+ }
+
+ private void checkDataBlockResponse(byte[] bytes) throws UsbTransportException {
+ final byte status = getStatus(bytes);
+ if (status != 0) {
+ throw new UsbTransportException("USB-CCID error - status " + status + " error code: " + Hex.toHexString(bytes, 8, 1));
+ }
+ }
+
+ private boolean isDataBlockNotReady(byte[] bytes) {
+ return getStatus(bytes) == 2;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final UsbTransport that = (UsbTransport) o;
+
+ return mUsbDevice != null ? mUsbDevice.equals(that.mUsbDevice) : that.mUsbDevice == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return mUsbDevice != null ? mUsbDevice.hashCode() : 0;
+ }
+
+ public UsbDevice getUsbDevice() {
+ return mUsbDevice;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java
new file mode 100644
index 000000000..6d9212d9f
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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 org.sufficientlysecure.keychain.securitytoken;
+
+import java.io.IOException;
+
+public class UsbTransportException extends IOException {
+ public UsbTransportException() {
+ }
+
+ public UsbTransportException(final String detailMessage) {
+ super(detailMessage);
+ }
+
+ public UsbTransportException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public UsbTransportException(final Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java
index 429d7a7e5..efc574124 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java
@@ -14,8 +14,8 @@ import java.util.Date;
public class RequiredInputParcel implements Parcelable {
public enum RequiredInputType {
- PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, NFC_SIGN, NFC_DECRYPT,
- NFC_MOVE_KEY_TO_CARD, NFC_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY,
+ PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, SECURITY_TOKEN_SIGN, SECURITY_TOKEN_DECRYPT,
+ SECURITY_TOKEN_MOVE_KEY_TO_CARD, SECURITY_TOKEN_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY,
}
public Date mSignatureTime;
@@ -92,19 +92,19 @@ public class RequiredInputParcel implements Parcelable {
public static RequiredInputParcel createNfcSignOperation(
long masterKeyId, long subKeyId,
byte[] inputHash, int signAlgo, Date signatureTime) {
- return new RequiredInputParcel(RequiredInputType.NFC_SIGN,
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_SIGN,
new byte[][] { inputHash }, new int[] { signAlgo },
signatureTime, masterKeyId, subKeyId);
}
public static RequiredInputParcel createNfcDecryptOperation(
long masterKeyId, long subKeyId, byte[] encryptedSessionKey) {
- return new RequiredInputParcel(RequiredInputType.NFC_DECRYPT,
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_DECRYPT,
new byte[][] { encryptedSessionKey }, null, null, masterKeyId, subKeyId);
}
public static RequiredInputParcel createNfcReset() {
- return new RequiredInputParcel(RequiredInputType.NFC_RESET_CARD,
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_RESET_CARD,
null, null, null, null, null);
}
@@ -209,7 +209,7 @@ public class RequiredInputParcel implements Parcelable {
signAlgos[i] = mSignAlgos.get(i);
}
- return new RequiredInputParcel(RequiredInputType.NFC_SIGN,
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_SIGN,
inputHashes, signAlgos, mSignatureTime, mMasterKeyId, mSubKeyId);
}
@@ -222,7 +222,7 @@ public class RequiredInputParcel implements Parcelable {
if (!mSignatureTime.equals(input.mSignatureTime)) {
throw new AssertionError("input times must match, this is a programming error!");
}
- if (input.mType != RequiredInputType.NFC_SIGN) {
+ if (input.mType != RequiredInputType.SECURITY_TOKEN_SIGN) {
throw new AssertionError("operation types must match, this is a progrmming error!");
}
@@ -264,7 +264,7 @@ public class RequiredInputParcel implements Parcelable {
ByteBuffer buf = ByteBuffer.wrap(mSubkeysToExport.get(0));
// We need to pass in a subkey here...
- return new RequiredInputParcel(RequiredInputType.NFC_MOVE_KEY_TO_CARD,
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD,
inputData, null, null, mMasterKeyId, buf.getLong());
}
@@ -287,7 +287,7 @@ public class RequiredInputParcel implements Parcelable {
if (!mMasterKeyId.equals(input.mMasterKeyId)) {
throw new AssertionError("Master keys must match, this is a programming error!");
}
- if (input.mType != RequiredInputType.NFC_MOVE_KEY_TO_CARD) {
+ if (input.mType != RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD) {
throw new AssertionError("Operation types must match, this is a programming error!");
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java
index b1fec3aae..44b185c52 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java
@@ -47,9 +47,9 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
public static final String EXTRA_SECURITY_TOKEN_PIN = "yubi_key_pin";
public static final String EXTRA_SECURITY_TOKEN_ADMIN_PIN = "yubi_key_admin_pin";
- public static final String EXTRA_NFC_USER_ID = "nfc_user_id";
- public static final String EXTRA_NFC_AID = "nfc_aid";
- public static final String EXTRA_NFC_FINGERPRINTS = "nfc_fingerprints";
+ public static final String EXTRA_SECURITY_TOKEN_USER_ID = "nfc_user_id";
+ public static final String EXTRA_SECURITY_TOKEN_AID = "nfc_aid";
+ public static final String EXTRA_SECURITY_FINGERPRINTS = "nfc_fingerprints";
public static final String FRAGMENT_TAG = "currentFragment";
@@ -66,8 +66,8 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
byte[] mScannedFingerprints;
- byte[] mNfcAid;
- String mNfcUserId;
+ byte[] mSecurityTokenAid;
+ String mSecurityTokenUserId;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -107,10 +107,10 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
mFirstTime = intent.getBooleanExtra(EXTRA_FIRST_TIME, false);
mCreateSecurityToken = intent.getBooleanExtra(EXTRA_CREATE_SECURITY_TOKEN, false);
- if (intent.hasExtra(EXTRA_NFC_FINGERPRINTS)) {
- byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_NFC_FINGERPRINTS);
- String nfcUserId = intent.getStringExtra(EXTRA_NFC_USER_ID);
- byte[] nfcAid = intent.getByteArrayExtra(EXTRA_NFC_AID);
+ if (intent.hasExtra(EXTRA_SECURITY_FINGERPRINTS)) {
+ byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_SECURITY_FINGERPRINTS);
+ String nfcUserId = intent.getStringExtra(EXTRA_SECURITY_TOKEN_USER_ID);
+ byte[] nfcAid = intent.getByteArrayExtra(EXTRA_SECURITY_TOKEN_AID);
if (containsKeys(nfcFingerprints)) {
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
@@ -143,24 +143,29 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
}
@Override
- protected void doNfcInBackground() throws IOException {
- if (mCurrentFragment instanceof NfcListenerFragment) {
- ((NfcListenerFragment) mCurrentFragment).doNfcInBackground();
+ protected void doSecurityTokenInBackground() throws IOException {
+ if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
+ ((SecurityTokenListenerFragment) mCurrentFragment).doSecurityTokenInBackground();
return;
}
- mScannedFingerprints = nfcGetFingerprints();
- mNfcAid = nfcGetAid();
- mNfcUserId = nfcGetUserId();
+ mScannedFingerprints = mSecurityTokenHelper.getFingerprints();
+ mSecurityTokenAid = mSecurityTokenHelper.getAid();
+ mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
}
@Override
- protected void onNfcPostExecute() {
- if (mCurrentFragment instanceof NfcListenerFragment) {
- ((NfcListenerFragment) mCurrentFragment).onNfcPostExecute();
+ protected void onSecurityTokenPostExecute() {
+ if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
+ ((SecurityTokenListenerFragment) mCurrentFragment).onSecurityTokenPostExecute();
return;
}
+ // We don't want get back to wait activity mainly because it looks weird with otg token
+ if (mCurrentFragment instanceof CreateSecurityTokenWaitFragment) {
+ getSupportFragmentManager().popBackStackImmediate();
+ }
+
if (containsKeys(mScannedFingerprints)) {
try {
long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mScannedFingerprints);
@@ -169,15 +174,15 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
Intent intent = new Intent(this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mScannedFingerprints);
startActivity(intent);
finish();
} catch (PgpKeyNotFoundException e) {
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
- mScannedFingerprints, mNfcAid, mNfcUserId);
+ mScannedFingerprints, mSecurityTokenAid, mSecurityTokenUserId);
loadFragment(frag, FragAction.TO_RIGHT);
}
} else {
@@ -255,9 +260,9 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
}
- interface NfcListenerFragment {
- void doNfcInBackground() throws IOException;
- void onNfcPostExecute();
+ interface SecurityTokenListenerFragment {
+ void doSecurityTokenInBackground() throws IOException;
+ void onSecurityTokenPostExecute();
}
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java
index ea57fe558..a716cb20d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java
@@ -25,7 +25,6 @@ import java.util.ArrayList;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
-import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
@@ -43,7 +42,7 @@ import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
-import org.sufficientlysecure.keychain.ui.CreateKeyActivity.NfcListenerFragment;
+import org.sufficientlysecure.keychain.ui.CreateKeyActivity.SecurityTokenListenerFragment;
import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Preferences;
@@ -51,7 +50,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
public class CreateSecurityTokenImportResetFragment
extends QueueingCryptoOperationFragment<ImportKeyringParcel, ImportKeyResult>
- implements NfcListenerFragment {
+ implements SecurityTokenListenerFragment {
private static final int REQUEST_CODE_RESET = 0x00005001;
@@ -248,11 +247,11 @@ public class CreateSecurityTokenImportResetFragment
}
@Override
- public void doNfcInBackground() throws IOException {
+ public void doSecurityTokenInBackground() throws IOException {
- mTokenFingerprints = mCreateKeyActivity.nfcGetFingerprints();
- mTokenAid = mCreateKeyActivity.nfcGetAid();
- mTokenUserId = mCreateKeyActivity.nfcGetUserId();
+ mTokenFingerprints = mCreateKeyActivity.getSecurityTokenHelper().getFingerprints();
+ mTokenAid = mCreateKeyActivity.getSecurityTokenHelper().getAid();
+ mTokenUserId = mCreateKeyActivity.getSecurityTokenHelper().getUserId();
byte[] fp = new byte[20];
ByteBuffer.wrap(fp).put(mTokenFingerprints, 0, 20);
@@ -260,7 +259,7 @@ public class CreateSecurityTokenImportResetFragment
}
@Override
- public void onNfcPostExecute() {
+ public void onSecurityTokenPostExecute() {
setData();
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java
index a3ea38e40..5dc2c478b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java
@@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui;
import android.app.Activity;
import android.os.Bundle;
+import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
@@ -26,6 +27,7 @@ import android.view.ViewGroup;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
+import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
public class CreateSecurityTokenWaitFragment extends Fragment {
@@ -34,6 +36,15 @@ public class CreateSecurityTokenWaitFragment extends Fragment {
View mBackButton;
@Override
+ public void onCreate(@Nullable final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (this.getActivity() instanceof BaseSecurityTokenNfcActivity) {
+ ((BaseSecurityTokenNfcActivity) this.getActivity()).checkDeviceConnection();
+ }
+ }
+
+ @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.create_security_token_wait_fragment, container, false);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java
index d2ecce242..39cd74fd2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java
@@ -3,6 +3,7 @@
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* Copyright (C) 2013-2014 Signe Rüsch
* Copyright (C) 2013-2014 Philipp Jakubeit
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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
@@ -69,8 +70,6 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
private RequiredInputParcel mRequiredInput;
- private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
-
private CryptoInputParcel mInputParcel;
@Override
@@ -137,9 +136,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
private void obtainPassphraseIfRequired() {
// obtain passphrase for this subkey
- if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD
- && mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_RESET_CARD) {
+ if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD
+ && mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_RESET_CARD) {
obtainSecurityTokenPin(mRequiredInput);
+ checkPinAvailability();
+ } else {
+ checkDeviceConnection();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (REQUEST_CODE_PIN == requestCode) {
+ checkPinAvailability();
+ }
+ }
+
+ private void checkPinAvailability() {
+ try {
+ Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
+ mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
+ if (passphrase != null) {
+ checkDeviceConnection();
+ }
+ } catch (PassphraseCacheService.KeyNotFoundException e) {
+ throw new AssertionError(
+ "tried to find passphrase for non-existing key. this is a programming error!");
}
}
@@ -149,39 +172,39 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
- public void onNfcPreExecute() {
+ public void onSecurityTokenPreExecute() {
// start with indeterminate progress
vAnimator.setDisplayedChild(1);
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.TRANSFERRING);
}
@Override
- protected void doNfcInBackground() throws IOException {
+ protected void doSecurityTokenInBackground() throws IOException {
switch (mRequiredInput.mType) {
- case NFC_DECRYPT: {
+ case SECURITY_TOKEN_DECRYPT: {
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] encryptedSessionKey = mRequiredInput.mInputData[i];
- byte[] decryptedSessionKey = nfcDecryptSessionKey(encryptedSessionKey);
+ byte[] decryptedSessionKey = mSecurityTokenHelper.decryptSessionKey(encryptedSessionKey);
mInputParcel.addCryptoData(encryptedSessionKey, decryptedSessionKey);
}
break;
}
- case NFC_SIGN: {
+ case SECURITY_TOKEN_SIGN: {
mInputParcel.addSignatureTime(mRequiredInput.mSignatureTime);
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] hash = mRequiredInput.mInputData[i];
int algo = mRequiredInput.mSignAlgos[i];
- byte[] signedHash = nfcCalculateSignature(hash, algo);
+ byte[] signedHash = mSecurityTokenHelper.calculateSignature(hash, algo);
mInputParcel.addCryptoData(hash, signedHash);
}
break;
}
- case NFC_MOVE_KEY_TO_CARD: {
+ case SECURITY_TOKEN_MOVE_KEY_TO_CARD: {
// TODO: assume PIN and Admin PIN to be default for this operation
- mPin = new Passphrase("123456");
- mAdminPin = new Passphrase("12345678");
+ mSecurityTokenHelper.setPin(new Passphrase("123456"));
+ mSecurityTokenHelper.setAdminPin(new Passphrase("12345678"));
ProviderHelper providerHelper = new ProviderHelper(this);
CanonicalizedSecretKeyRing secretKeyRing;
@@ -202,11 +225,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
long subkeyId = buf.getLong();
CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId);
-
- long keyGenerationTimestampMillis = key.getCreationTime().getTime();
- long keyGenerationTimestamp = keyGenerationTimestampMillis / 1000;
- byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
- byte[] tokenSerialNumber = Arrays.copyOf(nfcGetAid(), 16);
+ byte[] tokenSerialNumber = Arrays.copyOf(mSecurityTokenHelper.getAid(), 16);
Passphrase passphrase;
try {
@@ -216,46 +235,20 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
throw new IOException("Unable to get cached passphrase!");
}
- if (key.canSign() || key.canCertify()) {
- if (shouldPutKey(key.getFingerprint(), 0)) {
- nfcPutKey(0xB6, key, passphrase);
- nfcPutData(0xCE, timestampBytes);
- nfcPutData(0xC7, key.getFingerprint());
- } else {
- throw new IOException("Key slot occupied; token must be reset to put new signature key.");
- }
- } else if (key.canEncrypt()) {
- if (shouldPutKey(key.getFingerprint(), 1)) {
- nfcPutKey(0xB8, key, passphrase);
- nfcPutData(0xCF, timestampBytes);
- nfcPutData(0xC8, key.getFingerprint());
- } else {
- throw new IOException("Key slot occupied; token must be reset to put new decryption key.");
- }
- } else if (key.canAuthenticate()) {
- if (shouldPutKey(key.getFingerprint(), 2)) {
- nfcPutKey(0xA4, key, passphrase);
- nfcPutData(0xD0, timestampBytes);
- nfcPutData(0xC9, key.getFingerprint());
- } else {
- throw new IOException("Key slot occupied; token must be reset to put new authentication key.");
- }
- } else {
- throw new IOException("Inappropriate key flags for Security Token key.");
- }
+ mSecurityTokenHelper.changeKey(key, passphrase);
// TODO: Is this really used anywhere?
mInputParcel.addCryptoData(subkeyBytes, tokenSerialNumber);
}
// change PINs afterwards
- nfcModifyPin(0x81, newPin);
- nfcModifyPin(0x83, newAdminPin);
+ mSecurityTokenHelper.modifyPin(0x81, newPin);
+ mSecurityTokenHelper.modifyPin(0x83, newAdminPin);
break;
}
- case NFC_RESET_CARD: {
- nfcReset();
+ case SECURITY_TOKEN_RESET_CARD: {
+ mSecurityTokenHelper.resetAndWipeToken();
break;
}
@@ -267,7 +260,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
- protected final void onNfcPostExecute() {
+ protected final void onSecurityTokenPostExecute() {
handleResult(mInputParcel);
// show finish
@@ -275,28 +268,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.DONE);
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- // check all 200ms if Security Token has been taken away
- while (true) {
- if (isNfcConnected()) {
- try {
- Thread.sleep(200);
- } catch (InterruptedException ignored) {
+ if (mSecurityTokenHelper.isPersistentConnectionAllowed()) {
+ // Just close
+ finish();
+ } else {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ // check all 200ms if Security Token has been taken away
+ while (true) {
+ if (isSecurityTokenConnected()) {
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException ignored) {
+ }
+ } else {
+ return null;
}
- } else {
- return null;
}
}
- }
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
- finish();
- }
- }.execute();
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ finish();
+ }
+ }.execute();
+ }
}
/**
@@ -311,7 +309,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
- protected void onNfcError(String error) {
+ protected void onSecurityTokenError(String error) {
pauseTagHandling();
vErrorText.setText(error + "\n\n" + getString(R.string.security_token_nfc_try_again_text));
@@ -321,31 +319,11 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
- public void onNfcPinError(String error) {
- onNfcError(error);
+ public void onSecurityTokenPinError(String error) {
+ onSecurityTokenError(error);
// clear (invalid) passphrase
PassphraseCacheService.clearCachedPassphrase(
this, mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
}
-
- private boolean shouldPutKey(byte[] fingerprint, int idx) throws IOException {
- byte[] tokenFingerprint = nfcGetMasterKeyFingerprint(idx);
-
- // Note: special case: This should not happen, but happens with
- // https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true
- if (tokenFingerprint == null) {
- return true;
- }
-
- // Slot is empty, or contains this key already. PUT KEY operation is safe
- if (Arrays.equals(tokenFingerprint, BLANK_FINGERPRINT) ||
- Arrays.equals(tokenFingerprint, fingerprint)) {
- return true;
- }
-
- // Slot already contains a different key; don't overwrite it.
- return false;
- }
-
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UsbEventReceiverActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UsbEventReceiverActivity.java
new file mode 100644
index 000000000..05b30b1ae
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UsbEventReceiverActivity.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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 org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class UsbEventReceiverActivity extends Activity {
+ public static final String ACTION_USB_PERMISSION =
+ "org.sufficientlysecure.keychain.ui.USB_PERMISSION";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ final UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
+
+ Intent intent = getIntent();
+ if (intent != null) {
+ if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
+ UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+
+ Log.d(Constants.TAG, "Requesting permission for " + usbDevice.getDeviceName());
+ usbManager.requestPermission(usbDevice,
+ PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0));
+ }
+ }
+
+ // Close the activity
+ finish();
+ }
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
index d81797454..e47ca1db9 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
@@ -32,7 +32,6 @@ import android.app.ActivityOptions;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
-import android.graphics.Color;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.nfc.NfcAdapter;
@@ -172,9 +171,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
private byte[] mFingerprint;
private String mFingerprintString;
- private byte[] mNfcFingerprints;
- private String mNfcUserId;
- private byte[] mNfcAid;
+ private byte[] mSecurityTokenFingerprints;
+ private String mSecurityTokenUserId;
+ private byte[] mSecurityTokenAid;
@SuppressLint("InflateParams")
@Override
@@ -647,17 +646,17 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
}
@Override
- protected void doNfcInBackground() throws IOException {
+ protected void doSecurityTokenInBackground() throws IOException {
- mNfcFingerprints = nfcGetFingerprints();
- mNfcUserId = nfcGetUserId();
- mNfcAid = nfcGetAid();
+ mSecurityTokenFingerprints = mSecurityTokenHelper.getFingerprints();
+ mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
+ mSecurityTokenAid = mSecurityTokenHelper.getAid();
}
@Override
- protected void onNfcPostExecute() {
+ protected void onSecurityTokenPostExecute() {
- long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints);
+ long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mSecurityTokenFingerprints);
try {
@@ -668,7 +667,7 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
// if the master key of that key matches this one, just show the token dialog
if (KeyFormattingUtils.convertFingerprintToHex(candidateFp).equals(mFingerprintString)) {
- showSecurityTokenFragment(mNfcFingerprints, mNfcUserId, mNfcAid);
+ showSecurityTokenFragment(mSecurityTokenFingerprints, mSecurityTokenUserId, mSecurityTokenAid);
return;
}
@@ -681,9 +680,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
Intent intent = new Intent(
ViewKeyActivity.this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
finish();
}
@@ -696,9 +695,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
public void onAction() {
Intent intent = new Intent(
ViewKeyActivity.this, CreateKeyActivity.class);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
finish();
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java
index 77044cd2b..f4c0a9365 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java
@@ -3,6 +3,7 @@
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* Copyright (C) 2013-2014 Signe Rüsch
* Copyright (C) 2013-2014 Philipp Jakubeit
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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
@@ -20,41 +21,33 @@
package org.sufficientlysecure.keychain.ui.base;
-import java.io.IOException;
-import java.math.BigInteger;
-import java.nio.ByteBuffer;
-import java.security.interfaces.RSAPrivateCrtKey;
-
import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.TagLostException;
import android.os.AsyncTask;
import android.os.Bundle;
-import nordpol.Apdu;
-import nordpol.android.TagDispatcher;
-import nordpol.android.AndroidCard;
-import nordpol.android.OnDiscoveredTagListener;
-import nordpol.IsoCard;
-
-import org.bouncycastle.bcpg.HashAlgorithmTags;
-import org.bouncycastle.util.Arrays;
-import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
-import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
-import org.sufficientlysecure.keychain.service.PassphraseCacheService.KeyNotFoundException;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
+import org.sufficientlysecure.keychain.securitytoken.CardException;
+import org.sufficientlysecure.keychain.securitytoken.NfcTransport;
+import org.sufficientlysecure.keychain.securitytoken.SecurityTokenHelper;
+import org.sufficientlysecure.keychain.securitytoken.Transport;
+import org.sufficientlysecure.keychain.util.UsbConnectionDispatcher;
+import org.sufficientlysecure.keychain.securitytoken.UsbTransport;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity;
import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
@@ -63,56 +56,52 @@ import org.sufficientlysecure.keychain.ui.dialog.FidesmoPgpInstallDialog;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
-import org.sufficientlysecure.keychain.util.Iso7816TLV;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Passphrase;
-public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implements OnDiscoveredTagListener {
+import java.io.IOException;
+
+import nordpol.android.OnDiscoveredTagListener;
+import nordpol.android.TagDispatcher;
+
+public abstract class BaseSecurityTokenNfcActivity extends BaseActivity
+ implements OnDiscoveredTagListener, UsbConnectionDispatcher.OnDiscoveredUsbDeviceListener {
public static final int REQUEST_CODE_PIN = 1;
public static final String EXTRA_TAG_HANDLING_ENABLED = "tag_handling_enabled";
- // Fidesmo constants
- private static final String FIDESMO_APPS_AID_PREFIX = "A000000617";
private static final String FIDESMO_APP_PACKAGE = "com.fidesmo.sec.android";
- protected Passphrase mPin;
- protected Passphrase mAdminPin;
- protected boolean mPw1ValidForMultipleSignatures;
- protected boolean mPw1ValidatedForSignature;
- protected boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming?
- protected boolean mPw3Validated;
+ protected SecurityTokenHelper mSecurityTokenHelper = SecurityTokenHelper.getInstance();
protected TagDispatcher mTagDispatcher;
- private IsoCard mIsoCard;
+ protected UsbConnectionDispatcher mUsbDispatcher;
private boolean mTagHandlingEnabled;
- private static final int TIMEOUT = 100000;
-
- private byte[] mNfcFingerprints;
- private String mNfcUserId;
- private byte[] mNfcAid;
+ private byte[] mSecurityTokenFingerprints;
+ private String mSecurityTokenUserId;
+ private byte[] mSecurityTokenAid;
/**
- * Override to change UI before NFC handling (UI thread)
+ * Override to change UI before SecurityToken handling (UI thread)
*/
- protected void onNfcPreExecute() {
+ protected void onSecurityTokenPreExecute() {
}
/**
- * Override to implement NFC operations (background thread)
+ * Override to implement SecurityToken operations (background thread)
*/
- protected void doNfcInBackground() throws IOException {
- mNfcFingerprints = nfcGetFingerprints();
- mNfcUserId = nfcGetUserId();
- mNfcAid = nfcGetAid();
+ protected void doSecurityTokenInBackground() throws IOException {
+ mSecurityTokenFingerprints = mSecurityTokenHelper.getFingerprints();
+ mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
+ mSecurityTokenAid = mSecurityTokenHelper.getAid();
}
/**
- * Override to handle result of NFC operations (UI thread)
+ * Override to handle result of SecurityToken operations (UI thread)
*/
- protected void onNfcPostExecute() {
+ protected void onSecurityTokenPostExecute() {
- final long subKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints);
+ final long subKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mSecurityTokenFingerprints);
try {
CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(
@@ -121,15 +110,15 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
Intent intent = new Intent(this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
} catch (PgpKeyNotFoundException e) {
Intent intent = new Intent(this, CreateKeyActivity.class);
- intent.putExtra(CreateKeyActivity.EXTRA_NFC_AID, mNfcAid);
- intent.putExtra(CreateKeyActivity.EXTRA_NFC_USER_ID, mNfcUserId);
- intent.putExtra(CreateKeyActivity.EXTRA_NFC_FINGERPRINTS, mNfcFingerprints);
+ intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
+ intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
}
}
@@ -137,32 +126,49 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
/**
* Override to use something different than Notify (UI thread)
*/
- protected void onNfcError(String error) {
+ protected void onSecurityTokenError(String error) {
Notify.create(this, error, Style.WARN).show();
}
/**
* Override to do something when PIN is wrong, e.g., clear passphrases (UI thread)
*/
- protected void onNfcPinError(String error) {
- onNfcError(error);
+ protected void onSecurityTokenPinError(String error) {
+ onSecurityTokenError(error);
}
public void tagDiscovered(final Tag tag) {
// Actual NFC operations are executed in doInBackground to not block the UI thread
- if(!mTagHandlingEnabled)
+ if (!mTagHandlingEnabled)
+ return;
+
+ securityTokenDiscovered(new NfcTransport(tag));
+ }
+
+ public void usbDeviceDiscovered(final UsbDevice usbDevice) {
+ // Actual USB operations are executed in doInBackground to not block the UI thread
+ if (!mTagHandlingEnabled)
+ return;
+
+ UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
+ securityTokenDiscovered(new UsbTransport(usbDevice, usbManager));
+ }
+
+ public void securityTokenDiscovered(final Transport transport) {
+ // Actual Security Token operations are executed in doInBackground to not block the UI thread
+ if (!mTagHandlingEnabled)
return;
new AsyncTask<Void, Void, IOException>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
- onNfcPreExecute();
+ onSecurityTokenPreExecute();
}
@Override
protected IOException doInBackground(Void... params) {
try {
- handleTagDiscovered(tag);
+ handleSecurityToken(transport);
} catch (IOException e) {
return e;
}
@@ -175,11 +181,11 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
super.onPostExecute(exception);
if (exception != null) {
- handleNfcError(exception);
+ handleSecurityTokenError(exception);
return;
}
- onNfcPostExecute();
+ onSecurityTokenPostExecute();
}
}.execute();
}
@@ -197,6 +203,7 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
super.onCreate(savedInstanceState);
mTagDispatcher = TagDispatcher.get(this, this, false, false, true, false);
+ mUsbDispatcher = new UsbConnectionDispatcher(this, this);
// Check whether we're recreating a previously destroyed instance
if (savedInstanceState != null) {
@@ -230,15 +237,15 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
mTagDispatcher.interceptIntent(intent);
}
- private void handleNfcError(IOException e) {
+ private void handleSecurityTokenError(IOException e) {
if (e instanceof TagLostException) {
- onNfcError(getString(R.string.security_token_error_tag_lost));
+ onSecurityTokenError(getString(R.string.security_token_error_tag_lost));
return;
}
if (e instanceof IsoDepNotSupportedException) {
- onNfcError(getString(R.string.security_token_error_iso_dep_not_supported));
+ onSecurityTokenError(getString(R.string.security_token_error_iso_dep_not_supported));
return;
}
@@ -253,7 +260,7 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
if ((status & (short) 0xFFF0) == 0x63C0) {
int tries = status & 0x000F;
// hook to do something different when PIN is wrong
- onNfcPinError(getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries));
+ onSecurityTokenPinError(getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries));
return;
}
@@ -262,61 +269,61 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
// These errors should not occur in everyday use; if they are returned, it means we
// made a mistake sending data to the token, or the token is misbehaving.
case 0x6A80: {
- onNfcError(getString(R.string.security_token_error_bad_data));
+ onSecurityTokenError(getString(R.string.security_token_error_bad_data));
break;
}
case 0x6883: {
- onNfcError(getString(R.string.security_token_error_chaining_error));
+ onSecurityTokenError(getString(R.string.security_token_error_chaining_error));
break;
}
case 0x6B00: {
- onNfcError(getString(R.string.security_token_error_header, "P1/P2"));
+ onSecurityTokenError(getString(R.string.security_token_error_header, "P1/P2"));
break;
}
case 0x6D00: {
- onNfcError(getString(R.string.security_token_error_header, "INS"));
+ onSecurityTokenError(getString(R.string.security_token_error_header, "INS"));
break;
}
case 0x6E00: {
- onNfcError(getString(R.string.security_token_error_header, "CLA"));
+ onSecurityTokenError(getString(R.string.security_token_error_header, "CLA"));
break;
}
// These error conditions are more likely to be experienced by an end user.
case 0x6285: {
- onNfcError(getString(R.string.security_token_error_terminated));
+ onSecurityTokenError(getString(R.string.security_token_error_terminated));
break;
}
case 0x6700: {
- onNfcPinError(getString(R.string.security_token_error_wrong_length));
+ onSecurityTokenPinError(getString(R.string.security_token_error_wrong_length));
break;
}
case 0x6982: {
- onNfcError(getString(R.string.security_token_error_security_not_satisfied));
+ onSecurityTokenError(getString(R.string.security_token_error_security_not_satisfied));
break;
}
case 0x6983: {
- onNfcError(getString(R.string.security_token_error_authentication_blocked));
+ onSecurityTokenError(getString(R.string.security_token_error_authentication_blocked));
break;
}
case 0x6985: {
- onNfcError(getString(R.string.security_token_error_conditions_not_satisfied));
+ onSecurityTokenError(getString(R.string.security_token_error_conditions_not_satisfied));
break;
}
// 6A88 is "Not Found" in the spec, but Yubikey also returns 6A83 for this in some cases.
case 0x6A88:
case 0x6A83: {
- onNfcError(getString(R.string.security_token_error_data_not_found));
+ onSecurityTokenError(getString(R.string.security_token_error_data_not_found));
break;
}
// 6F00 is a JavaCard proprietary status code, SW_UNKNOWN, and usually represents an
// unhandled exception on the security token.
case 0x6F00: {
- onNfcError(getString(R.string.security_token_error_unknown));
+ onSecurityTokenError(getString(R.string.security_token_error_unknown));
break;
}
// 6A82 app not installed on security token!
case 0x6A82: {
- if (isFidesmoToken()) {
+ if (mSecurityTokenHelper.isFidesmoToken()) {
// Check if the Fidesmo app is installed
if (isAndroidAppInstalled(FIDESMO_APP_PACKAGE)) {
promptFidesmoPgpInstall();
@@ -324,12 +331,12 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
promptFidesmoAppInstall();
}
} else { // Other (possibly) compatible hardware
- onNfcError(getString(R.string.security_token_error_pgp_app_not_installed));
+ onSecurityTokenError(getString(R.string.security_token_error_pgp_app_not_installed));
}
break;
}
default: {
- onNfcError(getString(R.string.security_token_error, e.getMessage()));
+ onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
break;
}
}
@@ -363,7 +370,7 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
requiredInput.getMasterKeyId(), requiredInput.getSubKeyId());
if (passphrase != null) {
- mPin = passphrase;
+ mSecurityTokenHelper.setPin(passphrase);
return;
}
@@ -371,7 +378,7 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT,
RequiredInputParcel.createRequiredPassphrase(requiredInput));
startActivityForResult(intent, REQUEST_CODE_PIN);
- } catch (KeyNotFoundException e) {
+ } catch (PassphraseCacheService.KeyNotFoundException e) {
throw new AssertionError(
"tried to find passphrase for non-existing key. this is a programming error!");
}
@@ -388,7 +395,7 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
return;
}
CryptoInputParcel input = data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT);
- mPin = input.getPassphrase();
+ mSecurityTokenHelper.setPin(input.getPassphrase());
break;
}
default:
@@ -396,639 +403,22 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
}
}
- /** 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 appropriate 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.
- *
- */
- protected void handleTagDiscovered(Tag tag) throws IOException {
-
- // Connect to the detected tag, setting a couple of settings
- mIsoCard = AndroidCard.get(tag);
- if (mIsoCard == null) {
- throw new IsoDepNotSupportedException("Tag does not support ISO-DEP (ISO 14443-4)");
- }
- mIsoCard.setTimeout(TIMEOUT); // timeout is set to 100 seconds to avoid cancellation during calculation
- mIsoCard.connect();
-
- // 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
- String response = nfcCommunicate(opening); // activate connection
- if ( ! response.endsWith(accepted) ) {
- throw new CardException("Initialization failed!", parseCardStatus(response));
- }
-
- byte[] pwStatusBytes = nfcGetPwStatusBytes();
- mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1);
- mPw1ValidatedForSignature = false;
- mPw1ValidatedForDecrypt = false;
- mPw3Validated = false;
-
- doNfcInBackground();
-
- }
-
- public boolean isNfcConnected() {
- return mIsoCard.isConnected();
- }
-
- /** Return the key id from application specific data stored on tag, or null
- * if it doesn't exist.
- *
- * @param idx Index of the key to return the fingerprint from.
- * @return The long key id of the requested key, or null if not found.
- */
- public Long nfcGetKeyId(int idx) throws IOException {
- byte[] fp = nfcGetMasterKeyFingerprint(idx);
- if (fp == null) {
- return null;
- }
- ByteBuffer buf = ByteBuffer.wrap(fp);
- // skip first 12 bytes of the fingerprint
- buf.position(12);
- // the last eight bytes are the key id (big endian, which is default order in ByteBuffer)
- return buf.getLong();
- }
-
- /** Return fingerprints of all keys from application specific data stored
- * on tag, or null if data not available.
- *
- * @return The fingerprints of all subkeys in a contiguous byte array.
- */
- public byte[] nfcGetFingerprints() throws IOException {
- String data = "00CA006E00";
- byte[] buf = mIsoCard.transceive(Hex.decode(data));
-
- Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true);
- Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint());
-
- Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5);
- if (fptlv == null) {
- return null;
- }
-
- return fptlv.mV;
- }
-
- /** Return the PW Status Bytes from the token. This is a simple DO; no TLV decoding needed.
- *
- * @return Seven bytes in fixed format, plus 0x9000 status word at the end.
- */
- public byte[] nfcGetPwStatusBytes() throws IOException {
- String data = "00CA00C400";
- return mIsoCard.transceive(Hex.decode(data));
- }
-
- /** Return the fingerprint from application specific data stored on tag, or
- * null if it doesn't exist.
- *
- * @param idx Index of the key to return the fingerprint from.
- * @return The fingerprint of the requested key, or null if not found.
- */
- public byte[] nfcGetMasterKeyFingerprint(int idx) throws IOException {
- byte[] data = nfcGetFingerprints();
- if (data == null) {
- return null;
- }
-
- // return the master key fingerprint
- ByteBuffer fpbuf = ByteBuffer.wrap(data);
- byte[] fp = new byte[20];
- fpbuf.position(idx * 20);
- fpbuf.get(fp, 0, 20);
-
- return fp;
- }
-
- public byte[] nfcGetAid() throws IOException {
- String info = "00CA004F00";
- return mIsoCard.transceive(Hex.decode(info));
- }
-
- public String nfcGetUserId() throws IOException {
- String info = "00CA006500";
- return getHolderName(nfcCommunicate(info));
- }
-
- /**
- * Call COMPUTE DIGITAL SIGNATURE command and returns the MPI value
- *
- * @param hash the hash for signing
- * @return a big integer representing the MPI for the given hash
- */
- public byte[] nfcCalculateSignature(byte[] hash, int hashAlgo) throws IOException {
- if (!mPw1ValidatedForSignature) {
- nfcVerifyPin(0x81); // (Verify PW1 with mode 81 for signing)
- }
-
- // dsi, including Lc
- String dsi;
-
- Log.i(Constants.TAG, "Hash: " + hashAlgo);
- switch (hashAlgo) {
- case HashAlgorithmTags.SHA1:
- if (hash.length != 20) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 10!");
- }
- dsi = "23" // Lc
- + "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
- + "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
- + "0605" + "2B0E03021A" // OID of SHA1
- + "0500" // TLV coding of ZERO
- + "0414" + getHex(hash); // 0x14 are 20 hash bytes
- break;
- case HashAlgorithmTags.RIPEMD160:
- if (hash.length != 20) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 20!");
- }
- dsi = "233021300906052B2403020105000414" + getHex(hash);
- break;
- case HashAlgorithmTags.SHA224:
- if (hash.length != 28) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 28!");
- }
- dsi = "2F302D300D06096086480165030402040500041C" + getHex(hash);
- break;
- case HashAlgorithmTags.SHA256:
- if (hash.length != 32) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 32!");
- }
- dsi = "333031300D060960864801650304020105000420" + getHex(hash);
- break;
- case HashAlgorithmTags.SHA384:
- if (hash.length != 48) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 48!");
- }
- dsi = "433041300D060960864801650304020205000430" + getHex(hash);
- break;
- case HashAlgorithmTags.SHA512:
- if (hash.length != 64) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 64!");
- }
- dsi = "533051300D060960864801650304020305000440" + getHex(hash);
- break;
- default:
- throw new IOException("Not supported hash algo!");
- }
-
- // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
- String apdu =
- "002A9E9A" // CLA, INS, P1, P2
- + dsi // digital signature input
- + "00"; // Le
-
- String response = nfcCommunicate(apdu);
-
- // split up response into signature and status
- String status = response.substring(response.length()-4);
- String signature = response.substring(0, response.length() - 4);
-
- // while we are getting 0x61 status codes, retrieve more data
- while (status.substring(0, 2).equals("61")) {
- Log.d(Constants.TAG, "requesting more data, status " + status);
- // Send GET RESPONSE command
- response = nfcCommunicate("00C00000" + status.substring(2));
- status = response.substring(response.length()-4);
- signature += response.substring(0, response.length()-4);
- }
-
- Log.d(Constants.TAG, "final response:" + status);
-
- if (!mPw1ValidForMultipleSignatures) {
- mPw1ValidatedForSignature = false;
- }
-
- if ( ! "9000".equals(status)) {
- throw new CardException("Bad NFC response code: " + status, parseCardStatus(response));
- }
-
- // Make sure the signature we received is actually the expected number of bytes long!
- if (signature.length() != 256 && signature.length() != 512) {
- throw new IOException("Bad signature length! Expected 128 or 256 bytes, got " + signature.length() / 2);
- }
-
- return Hex.decode(signature);
- }
-
- /**
- * Call DECIPHER command
- *
- * @param encryptedSessionKey the encoded session key
- * @return the decoded session key
- */
- public byte[] nfcDecryptSessionKey(byte[] encryptedSessionKey) throws IOException {
- if (!mPw1ValidatedForDecrypt) {
- nfcVerifyPin(0x82); // (Verify PW1 with mode 82 for decryption)
+ protected void handleSecurityToken(Transport transport) throws IOException {
+ // Don't reconnect if device was already connected
+ if (!(mSecurityTokenHelper.isPersistentConnectionAllowed()
+ && mSecurityTokenHelper.isConnected()
+ && mSecurityTokenHelper.getTransport().equals(transport))) {
+ mSecurityTokenHelper.setTransport(transport);
+ mSecurityTokenHelper.connectToDevice();
}
-
- String firstApdu = "102a8086fe";
- String secondApdu = "002a808603";
- String le = "00";
-
- byte[] one = new byte[254];
- // leave out first byte:
- System.arraycopy(encryptedSessionKey, 1, one, 0, one.length);
-
- byte[] two = new byte[encryptedSessionKey.length - 1 - one.length];
- for (int i = 0; i < two.length; i++) {
- two[i] = encryptedSessionKey[i + one.length + 1];
- }
-
- nfcCommunicate(firstApdu + getHex(one));
- String second = nfcCommunicate(secondApdu + getHex(two) + le);
-
- String decryptedSessionKey = getDataField(second);
-
- return Hex.decode(decryptedSessionKey);
+ doSecurityTokenInBackground();
}
- /** Verifies the user's PW1 or PW3 with the appropriate mode.
- *
- * @param mode For PW1, this is 0x81 for signing, 0x82 for everything else.
- * For PW3 (Admin PIN), mode is 0x83.
- */
- public void nfcVerifyPin(int mode) throws IOException {
- if (mPin != null || mode == 0x83) {
-
- byte[] pin;
- if (mode == 0x83) {
- pin = mAdminPin.toStringUnsafe().getBytes();
- } else {
- pin = mPin.toStringUnsafe().getBytes();
- }
-
- // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
- // See specification, page 51
- String accepted = "9000";
- String response = nfcTryPin(mode, pin); // login
- if (!response.equals(accepted)) {
- throw new CardException("Bad PIN!", parseCardStatus(response));
- }
-
- if (mode == 0x81) {
- mPw1ValidatedForSignature = true;
- } else if (mode == 0x82) {
- mPw1ValidatedForDecrypt = true;
- } else if (mode == 0x83) {
- mPw3Validated = true;
- }
- }
+ public boolean isSecurityTokenConnected() {
+ return mSecurityTokenHelper.isConnected();
}
- /**
- * Resets security token, which deletes all keys and data objects.
- * This works by entering a wrong PIN and then Admin PIN 4 times respectively.
- * Afterwards, the token is reactivated.
- */
- public void nfcReset() throws IOException {
- String accepted = "9000";
-
- // try wrong PIN 4 times until counter goes to C0
- byte[] pin = "XXXXXX".getBytes();
- for (int i = 0; i <= 4; i++) {
- String response = nfcTryPin(0x81, pin);
- if (response.equals(accepted)) { // Should NOT accept!
- throw new CardException("Should never happen, XXXXXX has been accepted!", parseCardStatus(response));
- }
- }
-
- // try wrong Admin PIN 4 times until counter goes to C0
- byte[] adminPin = "XXXXXXXX".getBytes();
- for (int i = 0; i <= 4; i++) {
- String response = nfcTryPin(0x83, adminPin);
- if (response.equals(accepted)) { // Should NOT accept!
- throw new CardException("Should never happen, XXXXXXXX has been accepted", parseCardStatus(response));
- }
- }
-
- // reactivate token!
- String reactivate1 = "00" + "e6" + "00" + "00";
- String reactivate2 = "00" + "44" + "00" + "00";
- String response1 = nfcCommunicate(reactivate1);
- String response2 = nfcCommunicate(reactivate2);
- if (!response1.equals(accepted) || !response2.equals(accepted)) {
- throw new CardException("Reactivating failed!", parseCardStatus(response1));
- }
-
- }
-
- private String nfcTryPin(int mode, byte[] pin) throws IOException {
- // Command APDU for VERIFY command (page 32)
- String login =
- "00" // CLA
- + "20" // INS
- + "00" // P1
- + String.format("%02x", mode) // P2
- + String.format("%02x", pin.length) // Lc
- + Hex.toHexString(pin);
-
- return nfcCommunicate(login);
- }
-
- /** Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for
- * conformance to the token's requirements for key length.
- *
- * @param pw For PW1, this is 0x81. For PW3 (Admin PIN), mode is 0x83.
- * @param newPin The new PW1 or PW3.
- */
- public void nfcModifyPin(int pw, byte[] newPin) throws IOException {
- final int MAX_PW1_LENGTH_INDEX = 1;
- final int MAX_PW3_LENGTH_INDEX = 3;
-
- byte[] pwStatusBytes = nfcGetPwStatusBytes();
-
- if (pw == 0x81) {
- if (newPin.length < 6 || newPin.length > pwStatusBytes[MAX_PW1_LENGTH_INDEX]) {
- throw new IOException("Invalid PIN length");
- }
- } else if (pw == 0x83) {
- if (newPin.length < 8 || newPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) {
- throw new IOException("Invalid PIN length");
- }
- } else {
- throw new IOException("Invalid PW index for modify PIN operation");
- }
-
- byte[] pin;
- if (pw == 0x83) {
- pin = mAdminPin.toStringUnsafe().getBytes();
- } else {
- pin = mPin.toStringUnsafe().getBytes();
- }
-
- // Command APDU for CHANGE REFERENCE DATA command (page 32)
- String changeReferenceDataApdu = "00" // CLA
- + "24" // INS
- + "00" // P1
- + String.format("%02x", pw) // P2
- + String.format("%02x", pin.length + newPin.length) // Lc
- + getHex(pin)
- + getHex(newPin);
- String response = nfcCommunicate(changeReferenceDataApdu); // change PIN
- if (!response.equals("9000")) {
- throw new CardException("Failed to change PIN", parseCardStatus(response));
- }
- }
-
- /**
- * Stores a data object on the token. Automatically validates the proper PIN for the operation.
- * Supported for all data objects < 255 bytes in length. Only the cardholder certificate
- * (0x7F21) can exceed this length.
- *
- * @param dataObject The data object to be stored.
- * @param data The data to store in the object
- */
- public void nfcPutData(int dataObject, byte[] data) throws IOException {
- if (data.length > 254) {
- throw new IOException("Cannot PUT DATA with length > 254");
- }
- if (dataObject == 0x0101 || dataObject == 0x0103) {
- if (!mPw1ValidatedForDecrypt) {
- nfcVerifyPin(0x82); // (Verify PW1 for non-signing operations)
- }
- } else if (!mPw3Validated) {
- nfcVerifyPin(0x83); // (Verify PW3)
- }
-
- String putDataApdu = "00" // CLA
- + "DA" // INS
- + String.format("%02x", (dataObject & 0xFF00) >> 8) // P1
- + String.format("%02x", dataObject & 0xFF) // P2
- + String.format("%02x", data.length) // Lc
- + getHex(data);
-
- String response = nfcCommunicate(putDataApdu); // put data
- if (!response.equals("9000")) {
- throw new CardException("Failed to put data.", parseCardStatus(response));
- }
- }
-
- /**
- * Puts a key on the token in the given slot.
- *
- * @param slot The slot on the token where the key should be stored:
- * 0xB6: Signature Key
- * 0xB8: Decipherment Key
- * 0xA4: Authentication Key
- */
- public void nfcPutKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
- throws IOException {
- if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
- throw new IOException("Invalid key slot");
- }
-
- RSAPrivateCrtKey crtSecretKey;
- try {
- secretKey.unlock(passphrase);
- crtSecretKey = secretKey.getCrtSecretKey();
- } catch (PgpGeneralException e) {
- throw new IOException(e.getMessage());
- }
-
- // Shouldn't happen; the UI should block the user from getting an incompatible key this far.
- if (crtSecretKey.getModulus().bitLength() > 2048) {
- throw new IOException("Key too large to export to Security Token.");
- }
-
- // Should happen only rarely; all GnuPG keys since 2006 use public exponent 65537.
- if (!crtSecretKey.getPublicExponent().equals(new BigInteger("65537"))) {
- throw new IOException("Invalid public exponent for smart Security Token.");
- }
-
- if (!mPw3Validated) {
- nfcVerifyPin(0x83); // (Verify PW3 with mode 83)
- }
-
- byte[] header= Hex.decode(
- "4D82" + "03A2" // Extended header list 4D82, length of 930 bytes. (page 23)
- + String.format("%02x", slot) + "00" // CRT to indicate targeted key, no length
- + "7F48" + "15" // Private key template 0x7F48, length 21 (decimal, 0x15 hex)
- + "9103" // Public modulus, length 3
- + "928180" // Prime P, length 128
- + "938180" // Prime Q, length 128
- + "948180" // Coefficient (1/q mod p), length 128
- + "958180" // Prime exponent P (d mod (p - 1)), length 128
- + "968180" // Prime exponent Q (d mod (1 - 1)), length 128
- + "97820100" // Modulus, length 256, last item in private key template
- + "5F48" + "820383");// DO 5F48; 899 bytes of concatenated key data will follow
- byte[] dataToSend = new byte[934];
- byte[] currentKeyObject;
- int offset = 0;
-
- System.arraycopy(header, 0, dataToSend, offset, header.length);
- offset += header.length;
- currentKeyObject = crtSecretKey.getPublicExponent().toByteArray();
- System.arraycopy(currentKeyObject, 0, dataToSend, offset, 3);
- offset += 3;
- // NOTE: For a 2048-bit key, these lengths are fixed. However, bigint includes a leading 0
- // in the array to represent sign, so we take care to set the offset to 1 if necessary.
- currentKeyObject = crtSecretKey.getPrimeP().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getPrimeQ().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getCrtCoefficient().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getPrimeExponentP().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getPrimeExponentQ().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getModulus().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 256, dataToSend, offset, 256);
-
- String putKeyCommand = "10DB3FFF";
- String lastPutKeyCommand = "00DB3FFF";
-
- // Now we're ready to communicate with the token.
- offset = 0;
- String response;
- while(offset < dataToSend.length) {
- int dataRemaining = dataToSend.length - offset;
- if (dataRemaining > 254) {
- response = nfcCommunicate(
- putKeyCommand + "FE" + Hex.toHexString(dataToSend, offset, 254)
- );
- offset += 254;
- } else {
- int length = dataToSend.length - offset;
- response = nfcCommunicate(
- lastPutKeyCommand + String.format("%02x", length)
- + Hex.toHexString(dataToSend, offset, length));
- offset += length;
- }
-
- if (!response.endsWith("9000")) {
- throw new CardException("Key export to Security Token failed", parseCardStatus(response));
- }
- }
-
- // Clear array with secret data before we return.
- Arrays.fill(dataToSend, (byte) 0);
- }
-
- /**
- * Generates a key on the card in the given slot. If the slot is 0xB6 (the signature key),
- * this command also has the effect of resetting the digital signature counter.
- * NOTE: This does not set the key fingerprint data object! After calling this command, you
- * must construct a public key packet using the returned public key data objects, compute the
- * key fingerprint, and store it on the card using: nfcPutData(0xC8, key.getFingerprint())
- *
- * @param slot The slot on the card where the key should be generated:
- * 0xB6: Signature Key
- * 0xB8: Decipherment Key
- * 0xA4: Authentication Key
- * @return the public key data objects, in TLV format. For RSA this will be the public modulus
- * (0x81) and exponent (0x82). These may come out of order; proper TLV parsing is required.
- */
- public byte[] nfcGenerateKey(int slot) throws IOException {
- if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
- throw new IOException("Invalid key slot");
- }
-
- if (!mPw3Validated) {
- nfcVerifyPin(0x83); // (Verify PW3 with mode 83)
- }
-
- String generateKeyApdu = "0047800002" + String.format("%02x", slot) + "0000";
- String getResponseApdu = "00C00000";
-
- String first = nfcCommunicate(generateKeyApdu);
- String second = nfcCommunicate(getResponseApdu);
-
- if (!second.endsWith("9000")) {
- throw new IOException("On-card key generation failed");
- }
-
- String publicKeyData = getDataField(first) + getDataField(second);
-
- Log.d(Constants.TAG, "Public Key Data Objects: " + publicKeyData);
-
- return Hex.decode(publicKeyData);
- }
-
- /**
- * Transceive data via NFC encoded as Hex
- */
- public String nfcCommunicate(String apdu) throws IOException {
- return getHex(mIsoCard.transceive(Hex.decode(apdu)));
- }
-
- /**
- * Parses out the status word from a JavaCard response string.
- *
- * @param response A hex string with the response from the token
- * @return A short indicating the SW1/SW2, or 0 if a status could not be determined.
- */
- short parseCardStatus(String response) {
- if (response.length() < 4) {
- return 0; // invalid input
- }
-
- try {
- return Short.parseShort(response.substring(response.length() - 4), 16);
- } catch (NumberFormatException e) {
- return 0;
- }
- }
-
- public String getHolderName(String name) {
- try {
- 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;
- } catch (IndexOutOfBoundsException e) {
- // try-catch for https://github.com/FluffyKaon/OpenPGP-Card
- // Note: This should not happen, but happens with
- // https://github.com/FluffyKaon/OpenPGP-Card, thus return an empty string for now!
-
- Log.e(Constants.TAG, "Couldn't get holder name, returning empty string!", e);
- return "";
- }
- }
-
- private String getDataField(String output) {
- return output.substring(0, output.length() - 4);
- }
-
- public static String getHex(byte[] raw) {
- return new String(Hex.encode(raw));
- }
-
- public class IsoDepNotSupportedException extends IOException {
+ public static class IsoDepNotSupportedException extends IOException {
public IsoDepNotSupportedException(String detailMessage) {
super(detailMessage);
@@ -1036,38 +426,9 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
}
- public class CardException extends IOException {
- private short mResponseCode;
-
- public CardException(String detailMessage, short responseCode) {
- super(detailMessage);
- mResponseCode = responseCode;
- }
-
- public short getResponseCode() {
- return mResponseCode;
- }
-
- }
-
- private boolean isFidesmoToken() {
- if (isNfcConnected()) { // Check if we can still talk to the card
- try {
- // By trying to select any apps that have the Fidesmo AID prefix we can
- // see if it is a Fidesmo device or not
- byte[] mSelectResponse = mIsoCard.transceive(Apdu.select(FIDESMO_APPS_AID_PREFIX));
- // Compare the status returned by our select with the OK status code
- return Apdu.hasStatus(mSelectResponse, Apdu.OK_APDU);
- } catch (IOException e) {
- Log.e(Constants.TAG, "Card communication failed!", e);
- }
- }
- return false;
- }
-
/**
* Ask user if she wants to install PGP onto her Fidesmo token
- */
+ */
private void promptFidesmoPgpInstall() {
FidesmoPgpInstallDialog fidesmoPgpInstallDialog = new FidesmoPgpInstallDialog();
fidesmoPgpInstallDialog.show(getSupportFragmentManager(), "fidesmoPgpInstallDialog");
@@ -1084,6 +445,7 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
/**
* Use the package manager to detect if an application is installed on the phone
+ *
* @param uri an URI identifying the application's package
* @return 'true' if the app is installed
*/
@@ -1099,4 +461,28 @@ public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implemen
}
return mAppInstalled;
}
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mUsbDispatcher.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mUsbDispatcher.onStart();
+ }
+
+ public SecurityTokenHelper getSecurityTokenHelper() {
+ return mSecurityTokenHelper;
+ }
+
+ /**
+ * Run Security Token routines if last used token is connected and supports
+ * persistent connections
+ */
+ public void checkDeviceConnection() {
+ mUsbDispatcher.rescanDevices();
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
index 451065d6b..ad15c8f68 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
@@ -130,9 +130,9 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
switch (requiredInput.mType) {
// always use CryptoOperationHelper.startActivityForResult!
- case NFC_MOVE_KEY_TO_CARD:
- case NFC_DECRYPT:
- case NFC_SIGN: {
+ case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
+ case SECURITY_TOKEN_DECRYPT:
+ case SECURITY_TOKEN_SIGN: {
Intent intent = new Intent(activity, SecurityTokenOperationActivity.class);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/UsbConnectionDispatcher.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/UsbConnectionDispatcher.java
new file mode 100644
index 000000000..60fc84dba
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/UsbConnectionDispatcher.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.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 org.sufficientlysecure.keychain.util;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.securitytoken.UsbTransport;
+import org.sufficientlysecure.keychain.ui.UsbEventReceiverActivity;
+
+public class UsbConnectionDispatcher {
+ private Activity mActivity;
+
+ private OnDiscoveredUsbDeviceListener mListener;
+ private UsbManager mUsbManager;
+ /**
+ * Receives broadcast when a supported USB device get permission.
+ */
+ private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ switch (action) {
+ case UsbEventReceiverActivity.ACTION_USB_PERMISSION: {
+ android.hardware.usb.UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+ boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,
+ false);
+ if (permission) {
+ Log.d(Constants.TAG, "Got permission for " + usbDevice.getDeviceName());
+ mListener.usbDeviceDiscovered(usbDevice);
+ }
+ break;
+ }
+ }
+ }
+ };
+
+ public UsbConnectionDispatcher(final Activity activity, final OnDiscoveredUsbDeviceListener listener) {
+ this.mActivity = activity;
+ this.mListener = listener;
+ this.mUsbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE);
+ }
+
+ public void onStart() {
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(UsbEventReceiverActivity.ACTION_USB_PERMISSION);
+
+ mActivity.registerReceiver(mUsbReceiver, intentFilter);
+ }
+
+ public void onStop() {
+ mActivity.unregisterReceiver(mUsbReceiver);
+ }
+
+ /**
+ * Rescans devices and triggers {@link OnDiscoveredUsbDeviceListener}
+ */
+ public void rescanDevices() {
+ // Note: we don't check devices VID/PID because
+ // we check for permisssion instead.
+ // We should have permission only for matching devices
+ for (UsbDevice device : mUsbManager.getDeviceList().values()) {
+ if (mUsbManager.hasPermission(device)) {
+ if (mListener != null) {
+ mListener.usbDeviceDiscovered(device);
+ }
+ break;
+ }
+ }
+ }
+
+ public interface OnDiscoveredUsbDeviceListener {
+ void usbDeviceDiscovered(UsbDevice usbDevice);
+ }
+}
diff --git a/OpenKeychain/src/main/res/xml/usb_device_filter.xml b/OpenKeychain/src/main/res/xml/usb_device_filter.xml
new file mode 100644
index 000000000..789e4ffb7
--- /dev/null
+++ b/OpenKeychain/src/main/res/xml/usb_device_filter.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Based on https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h
+ Note that values are decimal.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Yubikey NEO OTP + CCID-->
+ <usb-device class="11" vendor-id="4176" product-id="273"/>
+ <!-- Yubikey NEO CCID-->
+ <usb-device class="11" vendor-id="4176" product-id="274"/>
+ <!-- Yubikey NEO U2F + CCID-->
+ <usb-device class="11" vendor-id="4176" product-id="277"/>
+ <!-- Yubikey NEO OTP + U2F + CCID-->
+ <usb-device class="11" vendor-id="4176" product-id="278"/>
+
+
+ <!-- Yubikey 4 CCID-->
+ <usb-device class="11" vendor-id="4176" product-id="1028"/>
+ <!-- Yubikey 4 OTP + CCID-->
+ <usb-device class="11" vendor-id="4176" product-id="1029"/>
+ <!-- Yubikey 4 U2F + CCID-->
+ <usb-device class="11" vendor-id="4176" product-id="1030"/>
+ <!-- Yubikey 4 OTP + U2F + CCID-->
+ <usb-device class="11" vendor-id="4176" product-id="1031"/>
+
+</resources> \ No newline at end of file
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java
index 666adc0f6..b87bc1cfb 100644
--- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java
@@ -45,7 +45,6 @@ import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
-import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
@@ -877,7 +876,7 @@ public class PgpKeyOperationTest {
long keyId = KeyringTestingHelper.getSubkeyId(ringSecurityToken, 1);
- { // moveKeyToSecurityToken should return a pending NFC_MOVE_KEY_TO_CARD result when presented with the RSA-2048
+ { // moveKeyToSecurityToken should return a pending SECURITY_TOKEN_MOVE_KEY_TO_CARD result when presented with the RSA-2048
// key, and then make key divert-to-card when it gets a serial in the cryptoInputParcel.
parcelSecurityToken.reset();
parcelSecurityToken.mChangeSubKeys.add(new SubkeyChange(keyId, false, true));
@@ -887,8 +886,8 @@ public class PgpKeyOperationTest {
PgpKeyOperation op = new PgpKeyOperation(null);
PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, parcelSecurityToken);
Assert.assertTrue("moveKeyToSecurityToken operation should be pending", result.isPending());
- Assert.assertEquals("required input should be RequiredInputType.NFC_MOVE_KEY_TO_CARD",
- result.getRequiredInputParcel().mType, RequiredInputType.NFC_MOVE_KEY_TO_CARD);
+ Assert.assertEquals("required input should be RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD",
+ result.getRequiredInputParcel().mType, RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD);
// Create a cryptoInputParcel that matches what the SecurityTokenOperationActivity would return.
byte[] keyIdBytes = new byte[8];
@@ -921,8 +920,8 @@ public class PgpKeyOperationTest {
PgpKeyOperation op = new PgpKeyOperation(null);
PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, parcelSecurityToken);
Assert.assertTrue("moveKeyToSecurityToken operation should be pending", result.isPending());
- Assert.assertEquals("required input should be RequiredInputType.NFC_SIGN",
- RequiredInputType.NFC_SIGN, result.getRequiredInputParcel().mType);
+ Assert.assertEquals("required input should be RequiredInputType.SECURITY_TOKEN_SIGN",
+ RequiredInputType.SECURITY_TOKEN_SIGN, result.getRequiredInputParcel().mType);
}
}