Merge pull request #2195 from open-keychain/ccid-check

Change USB filtering
This commit is contained in:
Dominik Schürmann
2017-10-28 12:42:43 +02:00
committed by GitHub
14 changed files with 424 additions and 200 deletions

View File

@@ -18,10 +18,13 @@
package org.sufficientlysecure.keychain.securitytoken; package org.sufficientlysecure.keychain.securitytoken;
import android.nfc.Tag; import android.nfc.Tag;
import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity; import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import java.io.IOException; import java.io.IOException;
@@ -35,6 +38,14 @@ public class NfcTransport implements Transport {
private final Tag mTag; private final Tag mTag;
private IsoCard mIsoCard; private IsoCard mIsoCard;
public static class IsoDepNotSupportedException extends IOException {
IsoDepNotSupportedException(String detailMessage) {
super(detailMessage);
}
}
public NfcTransport(Tag tag) { public NfcTransport(Tag tag) {
this.mTag = tag; this.mTag = tag;
} }
@@ -96,13 +107,25 @@ public class NfcTransport implements Transport {
public void connect() throws IOException { public void connect() throws IOException {
mIsoCard = AndroidCard.get(mTag); mIsoCard = AndroidCard.get(mTag);
if (mIsoCard == null) { if (mIsoCard == null) {
throw new BaseSecurityTokenActivity.IsoDepNotSupportedException("Tag does not support ISO-DEP (ISO 14443-4)"); throw new IsoDepNotSupportedException("Tag does not support ISO-DEP (ISO 14443-4)");
} }
mIsoCard.setTimeout(TIMEOUT); mIsoCard.setTimeout(TIMEOUT);
mIsoCard.connect(); mIsoCard.connect();
} }
@Override
public TransportType getTransportType() {
return TransportType.NFC;
}
@Nullable
@Override
public TokenType getTokenTypeIfAvailable() {
// Sadly, the NFC transport has no direct information about the token type.
return null;
}
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (this == o) return true; if (this == o) return true;

View File

@@ -48,6 +48,8 @@ import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException; import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Passphrase;
@@ -74,8 +76,7 @@ import java.util.List;
public class SecurityTokenConnection { public class SecurityTokenConnection {
private static final int APDU_SW1_RESPONSE_AVAILABLE = 0x61; private static final int APDU_SW1_RESPONSE_AVAILABLE = 0x61;
// Fidesmo constants private static final String AID_PREFIX_FIDESMO = "A000000617";
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 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};
@@ -89,6 +90,7 @@ public class SecurityTokenConnection {
private final Passphrase mPin; private final Passphrase mPin;
private final OpenPgpCommandApduFactory commandFactory; private final OpenPgpCommandApduFactory commandFactory;
private TokenType tokenType;
private CardCapabilities mCardCapabilities; private CardCapabilities mCardCapabilities;
private OpenPgpCapabilities mOpenPgpCapabilities; private OpenPgpCapabilities mOpenPgpCapabilities;
private SecureMessaging mSecureMessaging; private SecureMessaging mSecureMessaging;
@@ -182,8 +184,8 @@ public class SecurityTokenConnection {
mTransport.connect(); mTransport.connect();
// Connect on smartcard layer determineTokenType();
// Command APDU (page 51) for SELECT FILE command (page 29)
CommandApdu select = commandFactory.createSelectFileOpenPgpCommand(); CommandApdu select = commandFactory.createSelectFileOpenPgpCommand();
ResponseApdu response = communicate(select); // activate connection ResponseApdu response = communicate(select); // activate connection
@@ -208,6 +210,31 @@ public class SecurityTokenConnection {
} }
} }
@VisibleForTesting
void determineTokenType() throws IOException {
tokenType = mTransport.getTokenTypeIfAvailable();
if (tokenType != null) {
return;
}
CommandApdu selectFidesmoApdu = commandFactory.createSelectFileCommand(AID_PREFIX_FIDESMO);
if (communicate(selectFidesmoApdu).isSuccess()) {
tokenType = TokenType.FIDESMO;
return;
}
/* We could determine if this is a yubikey here. The info isn't used at the moment, so we save the roundtrip
// AID from https://github.com/Yubico/ykneo-oath/blob/master/build.xml#L16
CommandApdu selectYubicoApdu = commandFactory.createSelectFileCommand("A000000527200101");
if (communicate(selectYubicoApdu).isSuccess()) {
tokenType = TokenType.YUBIKEY_UNKNOWN;
return;
}
*/
tokenType = TokenType.UNKNOWN;
}
@VisibleForTesting @VisibleForTesting
void setConnectionCapabilities(OpenPgpCapabilities openPgpCapabilities) throws IOException { void setConnectionCapabilities(OpenPgpCapabilities openPgpCapabilities) throws IOException {
this.mOpenPgpCapabilities = openPgpCapabilities; this.mOpenPgpCapabilities = openPgpCapabilities;
@@ -795,20 +822,6 @@ public class SecurityTokenConnection {
return lastResponse; return lastResponse;
} }
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
CommandApdu apdu = commandFactory.createSelectFileCommand(FIDESMO_APPS_AID_PREFIX);
return communicate(apdu).isSuccess();
} 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), * 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. * this command also has the effect of resetting the digital signature counter.
@@ -917,6 +930,10 @@ public class SecurityTokenConnection {
return mTransport.isConnected(); return mTransport.isConnected();
} }
public TokenType getTokenType() {
return tokenType;
}
public void clearSecureMessaging() { public void clearSecureMessaging() {
if (mSecureMessaging != null) { if (mSecureMessaging != null) {
mSecureMessaging.clearSession(); mSecureMessaging.clearSession();
@@ -929,10 +946,6 @@ public class SecurityTokenConnection {
mSecureMessaging = sm; mSecureMessaging = sm;
} }
OpenPgpCapabilities getOpenPgpCapabilities() {
return mOpenPgpCapabilities;
}
public SecurityTokenInfo getTokenInfo() throws IOException { public SecurityTokenInfo getTokenInfo() throws IOException {
byte[] rawFingerprints = getFingerprints(); byte[] rawFingerprints = getFingerprints();
@@ -948,7 +961,16 @@ public class SecurityTokenConnection {
String url = getUrl(); String url = getUrl();
byte[] pwInfo = getPwStatusBytes(); byte[] pwInfo = getPwStatusBytes();
return SecurityTokenInfo.create(fingerprints, aid, userId, url, pwInfo[4], pwInfo[6]); TransportType transportType = mTransport.getTransportType();
SecurityTokenInfo info = SecurityTokenInfo
.create(transportType, tokenType, fingerprints, aid, userId, url, pwInfo[4], pwInfo[6]);
if (! info.isSecurityTokenSupported()) {
throw new UnsupportedSecurityTokenException();
}
return info;
} }
public static double parseOpenPgpVersion(final byte[] aid) { public static double parseOpenPgpVersion(final byte[] aid) {

View File

@@ -3,6 +3,7 @@ package org.sufficientlysecure.keychain.securitytoken;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import android.os.Parcelable; import android.os.Parcelable;
@@ -18,6 +19,9 @@ import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
public abstract class SecurityTokenInfo implements Parcelable { public abstract class SecurityTokenInfo implements Parcelable {
private static final byte[] EMPTY_ARRAY = new byte[20]; private static final byte[] EMPTY_ARRAY = new byte[20];
public abstract TransportType getTransportType();
public abstract TokenType getTokenType();
public abstract List<byte[]> getFingerprints(); public abstract List<byte[]> getFingerprints();
@Nullable @Nullable
public abstract byte[] getAid(); public abstract byte[] getAid();
@@ -32,7 +36,8 @@ public abstract class SecurityTokenInfo implements Parcelable {
return getFingerprints().isEmpty(); return getFingerprints().isEmpty();
} }
public static SecurityTokenInfo create(byte[][] fingerprints, byte[] aid, String userId, String url, public static SecurityTokenInfo create(TransportType transportType, TokenType tokenType, byte[][] fingerprints,
byte[] aid, String userId, String url,
int verifyRetries, int verifyAdminRetries) { int verifyRetries, int verifyAdminRetries) {
ArrayList<byte[]> fingerprintList = new ArrayList<>(fingerprints.length); ArrayList<byte[]> fingerprintList = new ArrayList<>(fingerprints.length);
for (byte[] fingerprint : fingerprints) { for (byte[] fingerprint : fingerprints) {
@@ -40,14 +45,15 @@ public abstract class SecurityTokenInfo implements Parcelable {
fingerprintList.add(fingerprint); fingerprintList.add(fingerprint);
} }
} }
return new AutoValue_SecurityTokenInfo(fingerprintList, aid, userId, url, verifyRetries, verifyAdminRetries); return new AutoValue_SecurityTokenInfo(
transportType, tokenType, fingerprintList, aid, userId, url, verifyRetries, verifyAdminRetries);
} }
public static SecurityTokenInfo newInstanceDebugKeyserver() { public static SecurityTokenInfo newInstanceDebugKeyserver() {
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
throw new UnsupportedOperationException("This operation is only available in debug builds!"); throw new UnsupportedOperationException("This operation is only available in debug builds!");
} }
return SecurityTokenInfo.create( return SecurityTokenInfo.create(TransportType.NFC, TokenType.UNKNOWN,
new byte[][] { KeyFormattingUtils.convertFingerprintHexFingerprint("1efdb4845ca242ca6977fddb1f788094fd3b430a") }, new byte[][] { KeyFormattingUtils.convertFingerprintHexFingerprint("1efdb4845ca242ca6977fddb1f788094fd3b430a") },
Hex.decode("010203040506"), "yubinu2@mugenguild.com", null, 3, 3); Hex.decode("010203040506"), "yubinu2@mugenguild.com", null, 3, 3);
} }
@@ -56,7 +62,7 @@ public abstract class SecurityTokenInfo implements Parcelable {
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
throw new UnsupportedOperationException("This operation is only available in debug builds!"); throw new UnsupportedOperationException("This operation is only available in debug builds!");
} }
return SecurityTokenInfo.create( return SecurityTokenInfo.create(TransportType.NFC, TokenType.UNKNOWN,
new byte[][] { KeyFormattingUtils.convertFingerprintHexFingerprint("4700BA1AC417ABEF3CC7765AD686905837779C3E") }, new byte[][] { KeyFormattingUtils.convertFingerprintHexFingerprint("4700BA1AC417ABEF3CC7765AD686905837779C3E") },
Hex.decode("010203040506"), "yubinu2@mugenguild.com", "http://valodim.stratum0.net/mryubinu2.asc", 3, 3); Hex.decode("010203040506"), "yubinu2@mugenguild.com", "http://valodim.stratum0.net/mryubinu2.asc", 3, 3);
} }
@@ -65,7 +71,7 @@ public abstract class SecurityTokenInfo implements Parcelable {
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
throw new UnsupportedOperationException("This operation is only available in debug builds!"); throw new UnsupportedOperationException("This operation is only available in debug builds!");
} }
return SecurityTokenInfo.create( return SecurityTokenInfo.create(TransportType.NFC, TokenType.UNKNOWN,
new byte[][] { KeyFormattingUtils.convertFingerprintHexFingerprint("4700BA1AC417ABEF3CC7765AD686905837779C3E") }, new byte[][] { KeyFormattingUtils.convertFingerprintHexFingerprint("4700BA1AC417ABEF3CC7765AD686905837779C3E") },
Hex.decode("010203040506"), "yubinu2@mugenguild.com", "http://valodim.stratum0.net/mryubinu2.asc", 0, 3); Hex.decode("010203040506"), "yubinu2@mugenguild.com", "http://valodim.stratum0.net/mryubinu2.asc", 0, 3);
} }
@@ -74,9 +80,43 @@ public abstract class SecurityTokenInfo implements Parcelable {
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
throw new UnsupportedOperationException("This operation is only available in debug builds!"); throw new UnsupportedOperationException("This operation is only available in debug builds!");
} }
return SecurityTokenInfo.create( return SecurityTokenInfo.create(TransportType.NFC, TokenType.UNKNOWN,
new byte[][] { KeyFormattingUtils.convertFingerprintHexFingerprint("4700BA1AC417ABEF3CC7765AD686905837779C3E") }, new byte[][] { KeyFormattingUtils.convertFingerprintHexFingerprint("4700BA1AC417ABEF3CC7765AD686905837779C3E") },
Hex.decode("010203040506"), "yubinu2@mugenguild.com", "http://valodim.stratum0.net/mryubinu2.asc", 0, 0); Hex.decode("010203040506"), "yubinu2@mugenguild.com", "http://valodim.stratum0.net/mryubinu2.asc", 0, 0);
} }
public enum TransportType {
NFC, USB
}
public enum TokenType {
YUBIKEY_NEO, YUBIKEY_4, FIDESMO, NITROKEY_PRO, NITROKEY_STORAGE, NITROKEY_START, GNUK, LEDGER_NANO_S, UNKNOWN
}
private static final HashSet<TokenType> SUPPORTED_USB_TOKENS = new HashSet<>(Arrays.asList(
TokenType.YUBIKEY_NEO,
TokenType.YUBIKEY_4,
TokenType.NITROKEY_PRO,
TokenType.NITROKEY_STORAGE
));
private static final HashSet<TokenType> SUPPORTED_USB_PUT_KEY = new HashSet<>(Arrays.asList(
TokenType.YUBIKEY_NEO,
TokenType.YUBIKEY_4 // Not clear, will be tested: https://github.com/open-keychain/open-keychain/issues/2069
));
public boolean isSecurityTokenSupported() {
boolean isKnownSupported = SUPPORTED_USB_TOKENS.contains(getTokenType());
boolean isNfcTransport = getTransportType() == TransportType.NFC;
return isKnownSupported || isNfcTransport;
}
public boolean isPutKeySupported() {
boolean isKnownSupported = SUPPORTED_USB_PUT_KEY.contains(getTokenType());
boolean isNfcTransport = getTransportType() == TransportType.NFC;
return isKnownSupported || isNfcTransport;
}
} }

View File

@@ -17,8 +17,14 @@
package org.sufficientlysecure.keychain.securitytoken; package org.sufficientlysecure.keychain.securitytoken;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType;
import java.io.IOException; import java.io.IOException;
import android.support.annotation.Nullable;
/** /**
* Abstraction for transmitting APDU commands * Abstraction for transmitting APDU commands
*/ */
@@ -55,4 +61,8 @@ public interface Transport {
* @throws IOException * @throws IOException
*/ */
void connect() throws IOException; void connect() throws IOException;
TransportType getTransportType();
@Nullable
TokenType getTokenTypeIfAvailable();
} }

View File

@@ -0,0 +1,15 @@
package org.sufficientlysecure.keychain.securitytoken;
import java.io.IOException;
public class UnsupportedSecurityTokenException extends IOException {
UnsupportedSecurityTokenException() {
super();
}
UnsupportedSecurityTokenException(String detailMessage) {
super(detailMessage);
}
}

View File

@@ -28,6 +28,8 @@ import android.support.annotation.Nullable;
import android.util.Pair; import android.util.Pair;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType;
import org.sufficientlysecure.keychain.securitytoken.Transport; import org.sufficientlysecure.keychain.securitytoken.Transport;
import org.sufficientlysecure.keychain.securitytoken.CommandApdu; import org.sufficientlysecure.keychain.securitytoken.CommandApdu;
import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; import org.sufficientlysecure.keychain.securitytoken.ResponseApdu;
@@ -54,6 +56,25 @@ public class UsbTransport implements Transport {
private static final int MASK_SHORT_APDU = 0x20000; private static final int MASK_SHORT_APDU = 0x20000;
private static final int MASK_EXTENDED_APDU = 0x40000; private static final int MASK_EXTENDED_APDU = 0x40000;
// https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h
private static final int VENDOR_YUBICO = 4176;
private static final int PRODUCT_YUBIKEY_NEO_OTP_CCID = 273;
private static final int PRODUCT_YUBIKEY_NEO_CCID = 274;
private static final int PRODUCT_YUBIKEY_NEO_U2F_CCID = 277;
private static final int PRODUCT_YUBIKEY_NEO_OTP_U2F_CCID = 278;
private static final int PRODUCT_YUBIKEY_4_CCID = 1028;
private static final int PRODUCT_YUBIKEY_4_OTP_CCID = 1029;
private static final int PRODUCT_YUBIKEY_4_U2F_CCID = 1030;
private static final int PRODUCT_YUBIKEY_4_OTP_U2F_CCID = 1031;
// https://www.nitrokey.com/de/documentation/installation#p:nitrokey-pro&os:linux
private static final int VENDOR_NITROKEY = 8352;
private static final int PRODUCT_NITROKEY_PRO = 16648;
private static final int PRODUCT_NITROKEY_START = 16913;
private static final int PRODUCT_NITROKEY_STORAGE = 16649;
private static final int VENDOR_FSIJ = 9035;
private static final int VENDOR_LEDGER = 11415;
private final UsbDevice usbDevice; private final UsbDevice usbDevice;
private final UsbManager usbManager; private final UsbManager usbManager;
@@ -80,6 +101,7 @@ public class UsbTransport implements Transport {
/** /**
* Check if device is was connected to and still is connected * Check if device is was connected to and still is connected
*
* @return true if device is connected * @return true if device is connected
*/ */
@Override @Override
@@ -91,6 +113,7 @@ public class UsbTransport implements Transport {
/** /**
* Check if Transport supports persistent connections e.g connections which can * Check if Transport supports persistent connections e.g connections which can
* handle multiple operations in one session * handle multiple operations in one session
*
* @return true if transport supports persistent connections * @return true if transport supports persistent connections
*/ */
@Override @Override
@@ -105,8 +128,7 @@ public class UsbTransport implements Transport {
public void connect() throws IOException { public void connect() throws IOException {
usbInterface = getSmartCardInterface(usbDevice); usbInterface = getSmartCardInterface(usbDevice);
if (usbInterface == null) { if (usbInterface == null) {
// Shouldn't happen as we whitelist only class 11 devices throw new UsbTransportException("USB error: CCID mode must be enabled (no class 11 interface)");
throw new UsbTransportException("USB error - device doesn't have class 11 interface");
} }
final Pair<UsbEndpoint, UsbEndpoint> ioEndpoints = getIoEndpoints(usbInterface); final Pair<UsbEndpoint, UsbEndpoint> ioEndpoints = getIoEndpoints(usbInterface);
@@ -114,16 +136,16 @@ public class UsbTransport implements Transport {
UsbEndpoint usbBulkOut = ioEndpoints.second; UsbEndpoint usbBulkOut = ioEndpoints.second;
if (usbBulkIn == null || usbBulkOut == null) { if (usbBulkIn == null || usbBulkOut == null) {
throw new UsbTransportException("USB error - invalid class 11 interface"); throw new UsbTransportException("USB error: invalid class 11 interface");
} }
usbConnection = usbManager.openDevice(usbDevice); usbConnection = usbManager.openDevice(usbDevice);
if (usbConnection == null) { if (usbConnection == null) {
throw new UsbTransportException("USB error - failed to connect to device"); throw new UsbTransportException("USB error: failed to connect to device");
} }
if (!usbConnection.claimInterface(usbInterface, true)) { if (!usbConnection.claimInterface(usbInterface, true)) {
throw new UsbTransportException("USB error - failed to claim interface"); throw new UsbTransportException("USB error: failed to claim interface");
} }
byte[] rawDescriptors = usbConnection.getRawDescriptors(); byte[] rawDescriptors = usbConnection.getRawDescriptors();
@@ -179,6 +201,7 @@ public class UsbTransport implements Transport {
/** /**
* Transmit and receive data * Transmit and receive data
*
* @param data data to transmit * @param data data to transmit
* @return received data * @return received data
*/ */
@@ -202,6 +225,53 @@ public class UsbTransport implements Transport {
return usbDevice != null ? usbDevice.hashCode() : 0; return usbDevice != null ? usbDevice.hashCode() : 0;
} }
@Override
public TransportType getTransportType() {
return TransportType.USB;
}
@Nullable
@Override
public TokenType getTokenTypeIfAvailable() {
switch (usbDevice.getVendorId()) {
case VENDOR_YUBICO: {
switch (usbDevice.getProductId()) {
case PRODUCT_YUBIKEY_NEO_OTP_CCID:
case PRODUCT_YUBIKEY_NEO_CCID:
case PRODUCT_YUBIKEY_NEO_U2F_CCID:
case PRODUCT_YUBIKEY_NEO_OTP_U2F_CCID:
return TokenType.YUBIKEY_NEO;
case PRODUCT_YUBIKEY_4_CCID:
case PRODUCT_YUBIKEY_4_OTP_CCID:
case PRODUCT_YUBIKEY_4_U2F_CCID:
case PRODUCT_YUBIKEY_4_OTP_U2F_CCID:
return TokenType.YUBIKEY_4;
}
break;
}
case VENDOR_NITROKEY: {
switch (usbDevice.getProductId()) {
case PRODUCT_NITROKEY_PRO:
return TokenType.NITROKEY_PRO;
case PRODUCT_NITROKEY_START:
return TokenType.NITROKEY_START;
case PRODUCT_NITROKEY_STORAGE:
return TokenType.NITROKEY_STORAGE;
}
break;
}
case VENDOR_FSIJ: {
return TokenType.GNUK;
}
case VENDOR_LEDGER: {
return TokenType.LEDGER_NANO_S;
}
}
Log.d(Constants.TAG, "Unknown USB token. Vendor ID: " + usbDevice.getVendorId() + ", Product ID: " +
usbDevice.getProductId());
return null;
}
/** /**
* Get first class 11 (Chip/Smartcard) interface of the device * Get first class 11 (Chip/Smartcard) interface of the device

View File

@@ -45,9 +45,12 @@ import org.sufficientlysecure.keychain.securitytoken.CardException;
import org.sufficientlysecure.keychain.securitytoken.NfcTransport; import org.sufficientlysecure.keychain.securitytoken.NfcTransport;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.securitytoken.Transport; import org.sufficientlysecure.keychain.securitytoken.Transport;
import org.sufficientlysecure.keychain.securitytoken.UnsupportedSecurityTokenException;
import org.sufficientlysecure.keychain.securitytoken.UsbConnectionDispatcher; import org.sufficientlysecure.keychain.securitytoken.UsbConnectionDispatcher;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransport; import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransport;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
@@ -136,8 +139,9 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity
public void securityTokenDiscovered(final Transport transport) { public void securityTokenDiscovered(final Transport transport) {
// Actual Security Token operations are executed in doInBackground to not block the UI thread // Actual Security Token operations are executed in doInBackground to not block the UI thread
if (!mTagHandlingEnabled) if (!mTagHandlingEnabled) {
return; return;
}
final SecurityTokenConnection stConnection = final SecurityTokenConnection stConnection =
SecurityTokenConnection.getInstanceForTransport(transport, mCachedPin); SecurityTokenConnection.getInstanceForTransport(transport, mCachedPin);
@@ -230,151 +234,158 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity
} }
private void handleSecurityTokenError(SecurityTokenConnection stConnection, IOException e) { private void handleSecurityTokenError(SecurityTokenConnection stConnection, IOException e) {
Log.d(Constants.TAG, "Exception in handleSecurityTokenError", e);
if (e instanceof TagLostException) { if (e instanceof TagLostException) {
onSecurityTokenError(getString(R.string.security_token_error_tag_lost)); onSecurityTokenError(getString(R.string.security_token_error_tag_lost));
return; return;
} }
if (e instanceof IsoDepNotSupportedException) { if (e instanceof NfcTransport.IsoDepNotSupportedException) {
onSecurityTokenError(getString(R.string.security_token_error_iso_dep_not_supported)); onSecurityTokenError(getString(R.string.security_token_error_iso_dep_not_supported));
return; return;
} }
short status; if (e instanceof UsbTransportException) {
if (e instanceof CardException) { onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
status = ((CardException) e).getResponseCode();
} else {
status = -1;
}
// Wrong PIN, a status of 63CX indicates X attempts remaining.
// NOTE: Used in ykneo-openpgp version < 1.0.10, changed to 0x6982 in 1.0.11
// https://github.com/Yubico/ykneo-openpgp/commit/90c2b91e86fb0e43ee234dd258834e75e3416410
if ((status & (short) 0xFFF0) == 0x63C0) {
int tries = status & 0x000F;
SecurityTokenInfo tokeninfo = null;
try {
tokeninfo = stConnection.getTokenInfo();
} catch (IOException e2) {
// don't care
}
// hook to do something different when PIN is wrong
onSecurityTokenPinError(
getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries), tokeninfo);
return; return;
} }
Log.d(Constants.TAG, "security token exception", e); if (e instanceof UnsupportedSecurityTokenException) {
onSecurityTokenError(getString(R.string.security_token_not_supported));
return;
}
// Otherwise, all status codes are fixed values. if (e instanceof CardException) {
switch (status) { short status = ((CardException) e).getResponseCode();
// These error conditions are likely to be experienced by an end user. // Wrong PIN, a status of 63CX indicates X attempts remaining.
// NOTE: Used in ykneo-openpgp version < 1.0.10, changed to 0x6982 in 1.0.11
// https://github.com/Yubico/ykneo-openpgp/commit/90c2b91e86fb0e43ee234dd258834e75e3416410
if ((status & (short) 0xFFF0) == 0x63C0) {
int tries = status & 0x000F;
/* OpenPGP Card Spec: Security status not satisfied, PW wrong,
PW not checked (command not allowed), Secure messaging incorrect (checksum and/or cryptogram) */
// NOTE: Used in ykneo-openpgp >= 1.0.11 for wrong PIN
case 0x6982: {
SecurityTokenInfo tokeninfo = null; SecurityTokenInfo tokeninfo = null;
try { try {
tokeninfo = stConnection.getTokenInfo(); tokeninfo = stConnection.getTokenInfo();
} catch (IOException e2) { } catch (IOException e2) {
// don't care // don't care
} }
// hook to do something different when PIN is wrong
onSecurityTokenPinError(
getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries), tokeninfo);
return;
}
// hook to do something different when PIN is wrong // Otherwise, all status codes are fixed values.
onSecurityTokenPinError(getString(R.string.security_token_error_security_not_satisfied), tokeninfo); switch (status) {
break; /* OpenPGP Card Spec: Security status not satisfied, PW wrong,
} PW not checked (command not allowed), Secure messaging incorrect (checksum and/or cryptogram) */
/* OpenPGP Card Spec: Selected file in termination state */ // NOTE: Used in ykneo-openpgp >= 1.0.11 for wrong PIN
case 0x6285: { case 0x6982: {
onSecurityTokenError(getString(R.string.security_token_error_terminated)); SecurityTokenInfo tokeninfo = null;
break; try {
} tokeninfo = stConnection.getTokenInfo();
/* OpenPGP Card Spec: Wrong length (Lc and/or Le) */ } catch (IOException e2) {
// NOTE: Used in ykneo-openpgp < 1.0.10 for too short PIN, changed in 1.0.11 to 0x6A80 for too short PIN // don't care
// https://github.com/Yubico/ykneo-openpgp/commit/b49ce8241917e7c087a4dab7b2c755420ff4500f
case 0x6700: {
// hook to do something different when PIN is wrong
onSecurityTokenPinError(getString(R.string.security_token_error_wrong_length), null);
break;
}
/* OpenPGP Card Spec: Incorrect parameters in the data field */
// NOTE: Used in ykneo-openpgp >= 1.0.11 for too short PIN
case 0x6A80: {
// hook to do something different when PIN is wrong
onSecurityTokenPinError(getString(R.string.security_token_error_bad_data), null);
break;
}
/* OpenPGP Card Spec: Authentication method blocked, PW blocked (error counter zero) */
case 0x6983: {
onSecurityTokenError(getString(R.string.security_token_error_authentication_blocked));
break;
}
/* OpenPGP Card Spec: Condition of use not satisfied */
case 0x6985: {
onSecurityTokenError(getString(R.string.security_token_error_conditions_not_satisfied));
break;
}
/* OpenPGP Card Spec: SM data objects incorrect (e.g. wrong TLV-structure in command data) */
// NOTE: 6A88 is "Not Found" in the spec, but ykneo-openpgp also returns 6A83 for this in some cases.
case 0x6A88:
case 0x6A83: {
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: {
onSecurityTokenError(getString(R.string.security_token_error_unknown));
break;
}
// 6A82 app not installed on security token!
case 0x6A82: {
if (stConnection.isFidesmoToken()) {
// Check if the Fidesmo app is installed
if (isAndroidAppInstalled(FIDESMO_APP_PACKAGE)) {
promptFidesmoPgpInstall();
} else {
promptFidesmoAppInstall();
} }
} else { // Other (possibly) compatible hardware
onSecurityTokenError(getString(R.string.security_token_error_pgp_app_not_installed)); // hook to do something different when PIN is wrong
onSecurityTokenPinError(getString(R.string.security_token_error_security_not_satisfied), tokeninfo);
break;
}
/* OpenPGP Card Spec: Selected file in termination state */
case 0x6285: {
onSecurityTokenError(getString(R.string.security_token_error_terminated));
break;
}
/* OpenPGP Card Spec: Wrong length (Lc and/or Le) */
// NOTE: Used in ykneo-openpgp < 1.0.10 for too short PIN, changed in 1.0.11 to 0x6A80 for too short PIN
// https://github.com/Yubico/ykneo-openpgp/commit/b49ce8241917e7c087a4dab7b2c755420ff4500f
case 0x6700: {
// hook to do something different when PIN is wrong
onSecurityTokenPinError(getString(R.string.security_token_error_wrong_length), null);
break;
}
/* OpenPGP Card Spec: Incorrect parameters in the data field */
// NOTE: Used in ykneo-openpgp >= 1.0.11 for too short PIN
case 0x6A80: {
// hook to do something different when PIN is wrong
onSecurityTokenPinError(getString(R.string.security_token_error_bad_data), null);
break;
}
/* OpenPGP Card Spec: Authentication method blocked, PW blocked (error counter zero) */
case 0x6983: {
onSecurityTokenError(getString(R.string.security_token_error_authentication_blocked));
break;
}
/* OpenPGP Card Spec: Condition of use not satisfied */
case 0x6985: {
onSecurityTokenError(getString(R.string.security_token_error_conditions_not_satisfied));
break;
}
/* OpenPGP Card Spec: SM data objects incorrect (e.g. wrong TLV-structure in command data) */
// NOTE: 6A88 is "Not Found" in the spec, but ykneo-openpgp also returns 6A83 for this in some cases.
case 0x6A88:
case 0x6A83: {
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: {
onSecurityTokenError(getString(R.string.security_token_error_unknown));
break;
}
// 6A82 app not installed on security token!
case 0x6A82: {
if (stConnection.getTokenType() == TokenType.FIDESMO) {
// Check if the Fidesmo app is installed
if (isAndroidAppInstalled(FIDESMO_APP_PACKAGE)) {
promptFidesmoPgpInstall();
} else {
promptFidesmoAppInstall();
}
}
break;
} }
break;
}
// These errors should not occur in everyday use; if they are returned, it means we // 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. // made a mistake sending data to the token, or the token is misbehaving.
/* OpenPGP Card Spec: Last command of the chain expected */ /* OpenPGP Card Spec: Last command of the chain expected */
case 0x6883: { case 0x6883: {
onSecurityTokenError(getString(R.string.security_token_error_chaining_error)); onSecurityTokenError(getString(R.string.security_token_error_chaining_error));
break; break;
} }
/* OpenPGP Card Spec: Wrong parameters P1-P2 */ /* OpenPGP Card Spec: Wrong parameters P1-P2 */
case 0x6B00: { case 0x6B00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "P1/P2")); onSecurityTokenError(getString(R.string.security_token_error_header, "P1/P2"));
break; break;
} }
/* OpenPGP Card Spec: Instruction (INS) not supported */ /* OpenPGP Card Spec: Instruction (INS) not supported */
case 0x6D00: { case 0x6D00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "INS")); onSecurityTokenError(getString(R.string.security_token_error_header, "INS"));
break; break;
} }
/* OpenPGP Card Spec: Class (CLA) not supported */ /* OpenPGP Card Spec: Class (CLA) not supported */
case 0x6E00: { case 0x6E00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "CLA")); onSecurityTokenError(getString(R.string.security_token_error_header, "CLA"));
break; break;
} }
default: { default: {
onSecurityTokenError(getString(R.string.security_token_error, e.getMessage())); onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
break; break;
}
} }
} }
// fallback for generic IOException
if (e.getMessage() != null) {
onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
} else {
onSecurityTokenError(getString(R.string.security_token_error_generic));
}
} }
/** /**
@@ -440,14 +451,6 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity
doSecurityTokenInBackground(stConnection); doSecurityTokenInBackground(stConnection);
} }
public static class IsoDepNotSupportedException extends IOException {
public IsoDepNotSupportedException(String detailMessage) {
super(detailMessage);
}
}
/** /**
* Ask user if she wants to install PGP onto her Fidesmo token * Ask user if she wants to install PGP onto her Fidesmo token
*/ */

View File

@@ -79,6 +79,7 @@ class ManageSecurityTokenContract {
void showActionRetryOrFromFile(); void showActionRetryOrFromFile();
void showActionLocked(int unlockAttempts); void showActionLocked(int unlockAttempts);
void showActionEmptyToken(); void showActionEmptyToken();
void showActionUnsupportedToken();
void hideAction(); void hideAction();
void operationImportKey(byte[] importKeyData); void operationImportKey(byte[] importKeyData);

View File

@@ -263,6 +263,11 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur
actionAnimator.setDisplayedChildId(R.id.token_layout_empty); actionAnimator.setDisplayedChildId(R.id.token_layout_empty);
} }
@Override
public void showActionUnsupportedToken() {
actionAnimator.setDisplayedChildId(R.id.token_layout_unsupported);
}
@Override @Override
public void hideAction() { public void hideAction() {
actionAnimator.setDisplayedChild(0); actionAnimator.setDisplayedChild(0);

View File

@@ -164,6 +164,14 @@ class ManageSecurityTokenPresenter implements ManageSecurityTokenMvpPresenter {
private void performKeyCheck() { private void performKeyCheck() {
boolean keyIsEmpty = tokenInfo.isEmpty(); boolean keyIsEmpty = tokenInfo.isEmpty();
boolean putKeyIsSupported = tokenInfo.isPutKeySupported();
if (keyIsEmpty && !putKeyIsSupported) {
view.statusLineOk();
view.showActionUnsupportedToken();
return;
}
if (keyIsEmpty) { if (keyIsEmpty) {
boolean tokenIsAdminLocked = tokenInfo.getVerifyAdminRetries() == 0; boolean tokenIsAdminLocked = tokenInfo.getVerifyAdminRetries() == 0;
if (tokenIsAdminLocked) { if (tokenIsAdminLocked) {

View File

@@ -370,6 +370,31 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/token_layout_unsupported">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeight"
android:gravity="center_vertical"
android:text="@string/token_result_empty"
style="?android:textAppearanceLarge"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:text="@string/token_put_key_unsupported"
style="?android:textAppearanceMedium"/>
</LinearLayout>
</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator> </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
</LinearLayout> </LinearLayout>

View File

@@ -1580,6 +1580,8 @@
<string name="security_token_error_header">"Security Token reported invalid %s byte."</string> <string name="security_token_error_header">"Security Token reported invalid %s byte."</string>
<string name="security_token_error_tag_lost">"Security Token has been taken off too early. Keep the Security Token at the back until the operation finishes."</string> <string name="security_token_error_tag_lost">"Security Token has been taken off too early. Keep the Security Token at the back until the operation finishes."</string>
<string name="security_token_error_iso_dep_not_supported">"Security Token does not support the required communication standard (ISO-DEP, ISO 14443-4)"</string> <string name="security_token_error_iso_dep_not_supported">"Security Token does not support the required communication standard (ISO-DEP, ISO 14443-4)"</string>
<string name="security_token_error_generic">"Generic Error: Most probably, the Security Token has been taken off too early."</string>
<string name="security_token_not_supported">"This Security Token is not supported by OpenKeychain."</string>
<string name="security_token_error_try_again">"Try again"</string> <string name="security_token_error_try_again">"Try again"</string>
<string name="btn_delete_original">Delete original file</string> <string name="btn_delete_original">Delete original file</string>
@@ -1895,6 +1897,7 @@
<string name="token_result_key_found">Key found!</string> <string name="token_result_key_found">Key found!</string>
<string name="token_result_token_ok">Ready for use!</string> <string name="token_result_token_ok">Ready for use!</string>
<string name="token_result_empty">Token is empty</string> <string name="token_result_empty">Token is empty</string>
<string name="token_put_key_unsupported">"Please set up this Security Token using GnuPG. OpenKeychain currently only supports singing/decrypting with this Security Token."</string>
<string name="token_action_retry">Retry Search</string> <string name="token_action_retry">Retry Search</string>
<string name="token_action_load_from_file">Load from File</string> <string name="token_action_load_from_file">Load from File</string>
<string name="token_action_reset">Reset Security Token</string> <string name="token_action_reset">Reset Security Token</string>

View File

@@ -1,42 +1,33 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- <!--
Based on Route all smart card devices (class 11) to OpenKeychain.
https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h Internally, we check against a whitelist of working devices.
https://www.nitrokey.com/de/documentation/installation#p:nitrokey-pro&os:linux -->
<usb-device class="11" />
Note that values are decimal. <!--
--> Route some HID devices (class 3) to OpenKeychain.
<resources xmlns:android="http://schemas.android.com/apk/res/android"> These tokens are generally supported but need to have CCID enabled.
<!-- Yubikey NEO OTP + CCID --> Thus, we show a notification inside OpenKeychain to inform the user.
<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"/>
<!-- Nitrokey Pro --> Based on
<usb-device class="11" vendor-id="8352" product-id="16648"/> https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h
Note that values are decimal.
-->
<!-- Yubikey NEO - OTP only -->
<usb-device class="3" vendor-id="4176" product-id="272"/>
<!-- Yubikey NEO - U2F only -->
<usb-device class="3" vendor-id="4176" product-id="275"/>
<!-- Yubikey NEO - OTP and U2F -->
<usb-device class="3" vendor-id="4176" product-id="276"/>
<!-- Yubikey 4 CCID --> <!-- Yubikey 4 - OTP only -->
<usb-device class="11" vendor-id="4176" product-id="1028"/> <usb-device class="3" vendor-id="4176" product-id="1025"/>
<!-- Yubikey 4 OTP + CCID --> <!-- Yubikey 4 - U2F only -->
<usb-device class="11" vendor-id="4176" product-id="1029"/> <usb-device class="3" vendor-id="4176" product-id="1026"/>
<!-- Yubikey 4 U2F + CCID --> <!-- Yubikey 4 - OTP and U2F -->
<usb-device class="11" vendor-id="4176" product-id="1030"/> <usb-device class="3" vendor-id="4176" product-id="1027"/>
<!-- Yubikey 4 OTP + U2F + CCID -->
<usb-device class="11" vendor-id="4176" product-id="1031"/>
<!-- Nitrokey Storage -->
<!--<usb-device class="11" vendor-id="8352" product-id="16649"/>-->
<!-- Nitrokey Start -->
<!--<usb-device class="11" vendor-id="8352" product-id="16913"/>-->
<!-- Default GNUK vid/pid -->
<!--<usb-device class="11" vendor-id="9035" product-id="0"/>-->
<!-- Ledger Nano S -->
<!--<usb-device class="11" vendor-id="11415" product-id="1"/>-->
</resources> </resources>

View File

@@ -8,9 +8,12 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.mockito.internal.verification.VerificationModeFactory;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowLog; import org.robolectric.shadows.ShadowLog;
import org.sufficientlysecure.keychain.KeychainTestRunner; import org.sufficientlysecure.keychain.KeychainTestRunner;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType;
import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Passphrase;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
@@ -23,19 +26,25 @@ import static org.mockito.Mockito.when;
@RunWith(KeychainTestRunner.class) @RunWith(KeychainTestRunner.class)
public class SecurityTokenConnectionTest { public class SecurityTokenConnectionTest {
private Transport transport;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
ShadowLog.stream = System.out; ShadowLog.stream = System.out;
transport = mock(Transport.class);
when(transport.getTransportType()).thenReturn(TransportType.USB);
when(transport.getTokenTypeIfAvailable()).thenReturn(TokenType.YUBIKEY_NEO);
} }
@Test @Test
public void test_connectToDevice() throws Exception { public void test_connectToDevice() throws Exception {
Transport transport = mock(Transport.class);
SecurityTokenConnection securityTokenConnection = SecurityTokenConnection securityTokenConnection =
new SecurityTokenConnection(transport, new Passphrase("123456"), new OpenPgpCommandApduFactory()); new SecurityTokenConnection(transport, new Passphrase("123456"), new OpenPgpCommandApduFactory());
String[] dialog = {
String[] dialog = { "00a4040006d27600012401", "9000", "00a4040006d27600012401", // select openpgp applet
"00ca006e00", "9000",
"00ca006e00", // get application related data
"6e81de4f10d27600012401020000060364311500005f520f0073000080000000000000000000007381b7c00af" + "6e81de4f10d27600012401020000060364311500005f520f0073000080000000000000000000007381b7c00af" +
"00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f03030" + "00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f03030" +
"3c53c4ec5fee25c4e89654d58cad8492510a89d3c3d8468da7b24e15bfc624c6a792794f15b7599915f7" + "3c53c4ec5fee25c4e89654d58cad8492510a89d3c3d8468da7b24e15bfc624c6a792794f15b7599915f7" +
@@ -55,7 +64,6 @@ public class SecurityTokenConnectionTest {
@Test @Test
public void test_getTokenInfo() throws Exception { public void test_getTokenInfo() throws Exception {
Transport transport = mock(Transport.class);
SecurityTokenConnection securityTokenConnection = SecurityTokenConnection securityTokenConnection =
new SecurityTokenConnection(transport, new Passphrase("123456"), new OpenPgpCommandApduFactory()); new SecurityTokenConnection(transport, new Passphrase("123456"), new OpenPgpCommandApduFactory());
OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities( OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities(
@@ -68,6 +76,7 @@ public class SecurityTokenConnectionTest {
"000000000cd0c59cd0f2a59cd0af059cd0c95" "000000000cd0c59cd0f2a59cd0af059cd0c95"
)); ));
securityTokenConnection.setConnectionCapabilities(openPgpCapabilities); securityTokenConnection.setConnectionCapabilities(openPgpCapabilities);
securityTokenConnection.determineTokenType();
String[] dialog = { String[] dialog = {
"00ca006500", "00ca006500",
@@ -98,6 +107,5 @@ public class SecurityTokenConnectionTest {
CommandApdu command = CommandApdu.fromBytes(Hex.decode(dialog[i])); CommandApdu command = CommandApdu.fromBytes(Hex.decode(dialog[i]));
inOrder.verify(transport).transceive(eq(command)); inOrder.verify(transport).transceive(eq(command));
} }
inOrder.verifyNoMoreInteractions();
} }
} }