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;
import android.nfc.Tag;
import android.support.annotation.Nullable;
import android.util.Log;
import org.bouncycastle.util.encoders.Hex;
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 java.io.IOException;
@@ -35,6 +38,14 @@ public class NfcTransport implements Transport {
private final Tag mTag;
private IsoCard mIsoCard;
public static class IsoDepNotSupportedException extends IOException {
IsoDepNotSupportedException(String detailMessage) {
super(detailMessage);
}
}
public NfcTransport(Tag tag) {
this.mTag = tag;
}
@@ -96,13 +107,25 @@ public class NfcTransport implements Transport {
public void connect() throws IOException {
mIsoCard = AndroidCard.get(mTag);
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.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
public boolean equals(final Object o) {
if (this == o) return true;

View File

@@ -48,6 +48,8 @@ import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
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.util.Log;
import org.sufficientlysecure.keychain.util.Passphrase;
@@ -74,8 +76,7 @@ import java.util.List;
public class SecurityTokenConnection {
private static final int APDU_SW1_RESPONSE_AVAILABLE = 0x61;
// Fidesmo constants
private static final String FIDESMO_APPS_AID_PREFIX = "A000000617";
private static final String AID_PREFIX_FIDESMO = "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};
@@ -89,6 +90,7 @@ public class SecurityTokenConnection {
private final Passphrase mPin;
private final OpenPgpCommandApduFactory commandFactory;
private TokenType tokenType;
private CardCapabilities mCardCapabilities;
private OpenPgpCapabilities mOpenPgpCapabilities;
private SecureMessaging mSecureMessaging;
@@ -182,8 +184,8 @@ public class SecurityTokenConnection {
mTransport.connect();
// Connect on smartcard layer
// Command APDU (page 51) for SELECT FILE command (page 29)
determineTokenType();
CommandApdu select = commandFactory.createSelectFileOpenPgpCommand();
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
void setConnectionCapabilities(OpenPgpCapabilities openPgpCapabilities) throws IOException {
this.mOpenPgpCapabilities = openPgpCapabilities;
@@ -795,20 +822,6 @@ public class SecurityTokenConnection {
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),
* this command also has the effect of resetting the digital signature counter.
@@ -917,6 +930,10 @@ public class SecurityTokenConnection {
return mTransport.isConnected();
}
public TokenType getTokenType() {
return tokenType;
}
public void clearSecureMessaging() {
if (mSecureMessaging != null) {
mSecureMessaging.clearSession();
@@ -929,10 +946,6 @@ public class SecurityTokenConnection {
mSecureMessaging = sm;
}
OpenPgpCapabilities getOpenPgpCapabilities() {
return mOpenPgpCapabilities;
}
public SecurityTokenInfo getTokenInfo() throws IOException {
byte[] rawFingerprints = getFingerprints();
@@ -948,7 +961,16 @@ public class SecurityTokenConnection {
String url = getUrl();
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) {

View File

@@ -3,6 +3,7 @@ package org.sufficientlysecure.keychain.securitytoken;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import android.os.Parcelable;
@@ -18,6 +19,9 @@ import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
public abstract class SecurityTokenInfo implements Parcelable {
private static final byte[] EMPTY_ARRAY = new byte[20];
public abstract TransportType getTransportType();
public abstract TokenType getTokenType();
public abstract List<byte[]> getFingerprints();
@Nullable
public abstract byte[] getAid();
@@ -32,7 +36,8 @@ public abstract class SecurityTokenInfo implements Parcelable {
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) {
ArrayList<byte[]> fingerprintList = new ArrayList<>(fingerprints.length);
for (byte[] fingerprint : fingerprints) {
@@ -40,14 +45,15 @@ public abstract class SecurityTokenInfo implements Parcelable {
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() {
if (!BuildConfig.DEBUG) {
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") },
Hex.decode("010203040506"), "yubinu2@mugenguild.com", null, 3, 3);
}
@@ -56,7 +62,7 @@ public abstract class SecurityTokenInfo implements Parcelable {
if (!BuildConfig.DEBUG) {
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") },
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) {
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") },
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) {
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") },
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;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType;
import java.io.IOException;
import android.support.annotation.Nullable;
/**
* Abstraction for transmitting APDU commands
*/
@@ -55,4 +61,8 @@ public interface Transport {
* @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 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.CommandApdu;
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_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 UsbManager usbManager;
@@ -80,6 +101,7 @@ public class UsbTransport implements Transport {
/**
* Check if device is was connected to and still is connected
*
* @return true if device is connected
*/
@Override
@@ -91,6 +113,7 @@ public class UsbTransport implements Transport {
/**
* 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
@@ -105,8 +128,7 @@ public class UsbTransport implements Transport {
public void connect() throws IOException {
usbInterface = getSmartCardInterface(usbDevice);
if (usbInterface == null) {
// Shouldn't happen as we whitelist only class 11 devices
throw new UsbTransportException("USB error - device doesn't have class 11 interface");
throw new UsbTransportException("USB error: CCID mode must be enabled (no class 11 interface)");
}
final Pair<UsbEndpoint, UsbEndpoint> ioEndpoints = getIoEndpoints(usbInterface);
@@ -114,16 +136,16 @@ public class UsbTransport implements Transport {
UsbEndpoint usbBulkOut = ioEndpoints.second;
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);
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)) {
throw new UsbTransportException("USB error - failed to claim interface");
throw new UsbTransportException("USB error: failed to claim interface");
}
byte[] rawDescriptors = usbConnection.getRawDescriptors();
@@ -179,6 +201,7 @@ public class UsbTransport implements Transport {
/**
* Transmit and receive data
*
* @param data data to transmit
* @return received data
*/
@@ -202,6 +225,53 @@ public class UsbTransport implements Transport {
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

View File

@@ -45,9 +45,12 @@ import org.sufficientlysecure.keychain.securitytoken.CardException;
import org.sufficientlysecure.keychain.securitytoken.NfcTransport;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.securitytoken.Transport;
import org.sufficientlysecure.keychain.securitytoken.UnsupportedSecurityTokenException;
import org.sufficientlysecure.keychain.securitytoken.UsbConnectionDispatcher;
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.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
@@ -136,8 +139,9 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity
public void securityTokenDiscovered(final Transport transport) {
// Actual Security Token operations are executed in doInBackground to not block the UI thread
if (!mTagHandlingEnabled)
if (!mTagHandlingEnabled) {
return;
}
final SecurityTokenConnection stConnection =
SecurityTokenConnection.getInstanceForTransport(transport, mCachedPin);
@@ -230,151 +234,158 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity
}
private void handleSecurityTokenError(SecurityTokenConnection stConnection, IOException e) {
Log.d(Constants.TAG, "Exception in handleSecurityTokenError", e);
if (e instanceof TagLostException) {
onSecurityTokenError(getString(R.string.security_token_error_tag_lost));
return;
}
if (e instanceof IsoDepNotSupportedException) {
if (e instanceof NfcTransport.IsoDepNotSupportedException) {
onSecurityTokenError(getString(R.string.security_token_error_iso_dep_not_supported));
return;
}
short status;
if (e instanceof CardException) {
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);
if (e instanceof UsbTransportException) {
onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
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.
switch (status) {
if (e instanceof CardException) {
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;
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;
}
// 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.isFidesmoToken()) {
// Check if the Fidesmo app is installed
if (isAndroidAppInstalled(FIDESMO_APP_PACKAGE)) {
promptFidesmoPgpInstall();
} else {
promptFidesmoAppInstall();
// Otherwise, all status codes are fixed values.
switch (status) {
/* 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;
try {
tokeninfo = stConnection.getTokenInfo();
} catch (IOException e2) {
// don't care
}
} 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
// made a mistake sending data to the token, or the token is misbehaving.
// 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.
/* OpenPGP Card Spec: Last command of the chain expected */
case 0x6883: {
onSecurityTokenError(getString(R.string.security_token_error_chaining_error));
break;
}
/* OpenPGP Card Spec: Wrong parameters P1-P2 */
case 0x6B00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "P1/P2"));
break;
}
/* OpenPGP Card Spec: Instruction (INS) not supported */
case 0x6D00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "INS"));
break;
}
/* OpenPGP Card Spec: Class (CLA) not supported */
case 0x6E00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "CLA"));
break;
}
default: {
onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
break;
/* OpenPGP Card Spec: Last command of the chain expected */
case 0x6883: {
onSecurityTokenError(getString(R.string.security_token_error_chaining_error));
break;
}
/* OpenPGP Card Spec: Wrong parameters P1-P2 */
case 0x6B00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "P1/P2"));
break;
}
/* OpenPGP Card Spec: Instruction (INS) not supported */
case 0x6D00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "INS"));
break;
}
/* OpenPGP Card Spec: Class (CLA) not supported */
case 0x6E00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "CLA"));
break;
}
default: {
onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
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);
}
public static class IsoDepNotSupportedException extends IOException {
public IsoDepNotSupportedException(String detailMessage) {
super(detailMessage);
}
}
/**
* Ask user if she wants to install PGP onto her Fidesmo token
*/

View File

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

View File

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

View File

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

View File

@@ -370,6 +370,31 @@
</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>
</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_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_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="btn_delete_original">Delete original file</string>
@@ -1895,6 +1897,7 @@
<string name="token_result_key_found">Key found!</string>
<string name="token_result_token_ok">Ready for use!</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_load_from_file">Load from File</string>
<string name="token_action_reset">Reset Security Token</string>

View File

@@ -1,42 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Based on
https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h
https://www.nitrokey.com/de/documentation/installation#p:nitrokey-pro&os:linux
<!--
Route all smart card devices (class 11) to OpenKeychain.
Internally, we check against a whitelist of working devices.
-->
<usb-device class="11" />
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"/>
<!--
Route some HID devices (class 3) to OpenKeychain.
These tokens are generally supported but need to have CCID enabled.
Thus, we show a notification inside OpenKeychain to inform the user.
<!-- Nitrokey Pro -->
<usb-device class="11" vendor-id="8352" product-id="16648"/>
Based on
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 -->
<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"/>
<!-- Yubikey 4 - OTP only -->
<usb-device class="3" vendor-id="4176" product-id="1025"/>
<!-- Yubikey 4 - U2F only -->
<usb-device class="3" vendor-id="4176" product-id="1026"/>
<!-- Yubikey 4 - OTP and U2F -->
<usb-device class="3" vendor-id="4176" product-id="1027"/>
<!-- 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>

View File

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