Merge pull request #2201 from open-keychain/gnuk-new

Gnuk
This commit is contained in:
Dominik Schürmann 2017-11-01 14:45:29 +01:00 committed by GitHub
commit 83ab483fc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 678 additions and 142 deletions

View file

@ -20,20 +20,22 @@ 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),;
SIGN(0, 0xB6, 0xCE, 0xC7, 0xC1),
ENCRYPT(1, 0xB8, 0xCF, 0xC8, 0xC2),
AUTH(2, 0xA4, 0xD0, 0xC9, 0xC3);
private final int mIdx;
private final int mSlot;
private final int mTimestampObjectId;
private final int mFingerprintObjectId;
private final int mAlgoAttributeSlot;
KeyType(final int idx, final int slot, final int timestampObjectId, final int fingerprintObjectId) {
KeyType(int idx, int slot, int timestampObjectId, int fingerprintObjectId, int algoAttributeSlot) {
this.mIdx = idx;
this.mSlot = slot;
this.mTimestampObjectId = timestampObjectId;
this.mFingerprintObjectId = fingerprintObjectId;
this.mAlgoAttributeSlot = algoAttributeSlot;
}
public static KeyType from(final CanonicalizedSecretKey key) {
@ -62,4 +64,8 @@ public enum KeyType {
public int getFingerprintObjectId() {
return mFingerprintObjectId;
}
public int getAlgoAttributeSlot() {
return mAlgoAttributeSlot;
}
}

View file

@ -193,8 +193,7 @@ public class SecurityTokenConnection {
throw new CardException("Initialization failed!", response.getSw());
}
OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities(getData(0x00, 0x6E));
setConnectionCapabilities(openPgpCapabilities);
refreshConnectionCapabilities();
mPw1ValidatedForSignature = false;
mPw1ValidatedForDecrypt = false;
@ -235,6 +234,13 @@ public class SecurityTokenConnection {
tokenType = TokenType.UNKNOWN;
}
private void refreshConnectionCapabilities() throws IOException {
byte[] rawOpenPgpCapabilities = getData(0x00, 0x6E);
OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities(rawOpenPgpCapabilities);
setConnectionCapabilities(openPgpCapabilities);
}
@VisibleForTesting
void setConnectionCapabilities(OpenPgpCapabilities openPgpCapabilities) throws IOException {
this.mOpenPgpCapabilities = openPgpCapabilities;
@ -493,33 +499,13 @@ public class SecurityTokenConnection {
}
}
private void setKeyAttributes(Passphrase adminPin, final KeyType slot, final CanonicalizedSecretKey secretKey)
throws IOException {
if (mOpenPgpCapabilities.isAttributesChangable()) {
int tag;
if (slot == KeyType.SIGN) {
tag = 0xC1;
} else if (slot == KeyType.ENCRYPT) {
tag = 0xC2;
} else if (slot == KeyType.AUTH) {
tag = 0xC3;
} else {
throw new IOException("Unknown key for card.");
}
try {
putData(adminPin, tag, SecurityTokenUtils.attributesFromSecretKey(slot, secretKey));
mOpenPgpCapabilities.updateWithData(getData(0x00, tag));
} catch (PgpGeneralException e) {
throw new IOException("Key algorithm not supported by the security token.");
}
private void setKeyAttributes(Passphrase adminPin, KeyType keyType, byte[] data) throws IOException {
if (!mOpenPgpCapabilities.isAttributesChangable()) {
return;
}
putData(adminPin, keyType.getAlgoAttributeSlot(), data);
refreshConnectionCapabilities();
}
/**
@ -546,9 +532,11 @@ public class SecurityTokenConnection {
try {
secretKey.unlock(passphrase);
setKeyAttributes(adminPin, slot, secretKey);
setKeyAttributes(adminPin, slot, SecurityTokenUtils.attributesFromSecretKey(slot, secretKey,
mOpenPgpCapabilities.getFormatForKeyType(slot)));
switch (mOpenPgpCapabilities.getFormatForKeyType(slot).keyFormatType()) {
KeyFormat formatForKeyType = mOpenPgpCapabilities.getFormatForKeyType(slot);
switch (formatForKeyType.keyFormatType()) {
case RSAKeyFormatType:
if (!secretKey.isRSA()) {
throw new IOException("Security Token not configured for RSA key.");
@ -561,7 +549,7 @@ public class SecurityTokenConnection {
}
keyBytes = SecurityTokenUtils.createRSAPrivKeyTemplate(crtSecretKey, slot,
(RSAKeyFormat) (mOpenPgpCapabilities.getFormatForKeyType(slot)));
(RSAKeyFormat) formatForKeyType);
break;
case ECKeyFormatType:
@ -574,7 +562,7 @@ public class SecurityTokenConnection {
ecPublicKey = secretKey.getSecurityTokenECPublicKey();
keyBytes = SecurityTokenUtils.createECPrivKeyTemplate(ecSecretKey, ecPublicKey, slot,
(ECKeyFormat) (mOpenPgpCapabilities.getFormatForKeyType(slot)));
(ECKeyFormat) formatForKeyType);
break;
default:

View file

@ -5,6 +5,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.os.Parcelable;
import android.support.annotation.Nullable;
@ -18,6 +20,7 @@ import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
@AutoValue
public abstract class SecurityTokenInfo implements Parcelable {
private static final byte[] EMPTY_ARRAY = new byte[20];
private static final Pattern GNUK_VERSION_PATTERN = Pattern.compile("FSIJ-(\\d\\.\\d\\.\\d)-.+");
public abstract TransportType getTransportType();
public abstract TokenType getTokenType();
@ -90,19 +93,31 @@ public abstract class SecurityTokenInfo implements Parcelable {
}
public enum TokenType {
YUBIKEY_NEO, YUBIKEY_4, FIDESMO, NITROKEY_PRO, NITROKEY_STORAGE, NITROKEY_START, GNUK, LEDGER_NANO_S, UNKNOWN
YUBIKEY_NEO, YUBIKEY_4, FIDESMO, NITROKEY_PRO, NITROKEY_STORAGE, NITROKEY_START,
GNUK_OLD, GNUK_UNKNOWN, GNUK_NEWER_1_25, 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
TokenType.NITROKEY_STORAGE,
TokenType.GNUK_OLD,
TokenType.GNUK_UNKNOWN,
TokenType.GNUK_NEWER_1_25
));
private static final HashSet<TokenType> SUPPORTED_USB_RESET = new HashSet<>(Arrays.asList(
TokenType.YUBIKEY_NEO,
TokenType.YUBIKEY_4,
TokenType.NITROKEY_PRO,
TokenType.GNUK_NEWER_1_25
));
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
TokenType.YUBIKEY_4, // Not clear, will be tested: https://github.com/open-keychain/open-keychain/issues/2069
TokenType.NITROKEY_PRO
));
public boolean isSecurityTokenSupported() {
@ -119,4 +134,22 @@ public abstract class SecurityTokenInfo implements Parcelable {
return isKnownSupported || isNfcTransport;
}
public boolean isResetSupported() {
boolean isKnownSupported = SUPPORTED_USB_RESET.contains(getTokenType());
boolean isNfcTransport = getTransportType() == TransportType.NFC;
return isKnownSupported || isNfcTransport;
}
public static String parseGnukVersionString(String serialNo) {
if (serialNo == null) {
return null;
}
Matcher matcher = GNUK_VERSION_PATTERN.matcher(serialNo);
if (!matcher.matches()) {
return null;
}
return matcher.group(1);
}
}

View file

@ -22,9 +22,7 @@ import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.securitytoken.ECKeyFormat;
import org.sufficientlysecure.keychain.securitytoken.RSAKeyFormat;
import org.sufficientlysecure.keychain.securitytoken.KeyType;
import org.sufficientlysecure.keychain.securitytoken.RSAKeyFormat.RSAAlgorithmFormat;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -33,25 +31,15 @@ import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
class SecurityTokenUtils {
static byte[] attributesFromSecretKey(final KeyType slot, final CanonicalizedSecretKey secretKey) throws IOException, PgpGeneralException {
static byte[] attributesFromSecretKey(KeyType slot, CanonicalizedSecretKey secretKey, KeyFormat formatForKeyType)
throws IOException {
if (secretKey.isRSA()) {
final int mModulusLength = secretKey.getBitStrength();
final int mExponentLength = secretKey.getSecurityTokenRSASecretKey().getPublicExponent().bitLength();
final byte[] attrs = new byte[6];
int i = 0;
attrs[i++] = (byte) 0x01;
attrs[i++] = (byte) ((mModulusLength >> 8) & 0xff);
attrs[i++] = (byte) (mModulusLength & 0xff);
attrs[i++] = (byte) ((mExponentLength >> 8) & 0xff);
attrs[i++] = (byte) (mExponentLength & 0xff);
attrs[i] = RSAKeyFormat.RSAAlgorithmFormat.CRT_WITH_MODULUS.getValue();
return attrs;
return attributesForRsaKey(secretKey.getBitStrength(), (RSAKeyFormat) formatForKeyType);
} else if (secretKey.isEC()) {
final byte[] oid = new ASN1ObjectIdentifier(secretKey.getCurveOid()).getEncoded();
final byte[] attrs = new byte[1 + (oid.length - 2) + 1];
byte[] oid = new ASN1ObjectIdentifier(secretKey.getCurveOid()).getEncoded();
byte[] attrs = new byte[1 + (oid.length - 2) + 1];
if (slot.equals(KeyType.SIGN))
attrs[0] = ECKeyFormat.ECAlgorithmFormat.ECDSA_WITH_PUBKEY.getValue();
@ -69,6 +57,21 @@ class SecurityTokenUtils {
}
}
private static byte[] attributesForRsaKey(int modulusLength, RSAKeyFormat formatForKeyType) {
RSAAlgorithmFormat algorithmFormat = formatForKeyType.getAlgorithmFormat();
int exponentLength = formatForKeyType.getExponentLength();
int i = 0;
byte[] attrs = new byte[6];
attrs[i++] = (byte) 0x01;
attrs[i++] = (byte) ((modulusLength >> 8) & 0xff);
attrs[i++] = (byte) (modulusLength & 0xff);
attrs[i++] = (byte) ((exponentLength >> 8) & 0xff);
attrs[i++] = (byte) (exponentLength & 0xff);
attrs[i] = algorithmFormat.getValue();
return attrs;
}
static byte[] createRSAPrivKeyTemplate(RSAPrivateCrtKey secretKey, KeyType slot,
RSAKeyFormat format) throws IOException {

View file

@ -0,0 +1,149 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
* Copyright (C) 2017 Vincent Breitmoser <v.breitmoser@mugenguild.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.usb;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import com.google.auto.value.AutoValue;
import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1ShortApduProtocol;
import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1TpduProtocol;
@AutoValue
abstract class CcidDescription {
private static final int DESCRIPTOR_LENGTH = 0x36;
private static final int DESCRIPTOR_TYPE = 0x21;
// dwFeatures Masks
private static final int FEATURE_AUTOMATIC_VOLTAGE = 0x00008;
private static final int FEATURE_EXCHANGE_LEVEL_TPDU = 0x10000;
private static final int FEATURE_EXCHAGE_LEVEL_SHORT_APDU = 0x20000;
private static final int FEATURE_EXCHAGE_LEVEL_EXTENDED_APDU = 0x40000;
// bVoltageSupport Masks
private static final byte VOLTAGE_5V = 1;
private static final byte VOLTAGE_3V = 2;
private static final byte VOLTAGE_1_8V = 4;
private static final int SLOT_OFFSET = 4;
private static final int FEATURES_OFFSET = 40;
private static final short MASK_T1_PROTO = 2;
public abstract byte getMaxSlotIndex();
public abstract byte getVoltageSupport();
public abstract int getProtocols();
public abstract int getFeatures();
@VisibleForTesting
static CcidDescription fromValues(byte maxSlotIndex, byte voltageSupport, int protocols, int features) {
return new AutoValue_CcidDescription(maxSlotIndex, voltageSupport, protocols, features);
}
@NonNull
static CcidDescription fromRawDescriptors(byte[] desc) throws UsbTransportException {
int dwProtocols = 0, dwFeatures = 0;
byte bMaxSlotIndex = 0, bVoltageSupport = 0;
boolean hasCcidDescriptor = false;
ByteBuffer byteBuffer = ByteBuffer.wrap(desc).order(ByteOrder.LITTLE_ENDIAN);
while (byteBuffer.hasRemaining()) {
byteBuffer.mark();
byte len = byteBuffer.get(), type = byteBuffer.get();
if (type == DESCRIPTOR_TYPE && len == DESCRIPTOR_LENGTH) {
byteBuffer.reset();
byteBuffer.position(byteBuffer.position() + SLOT_OFFSET);
bMaxSlotIndex = byteBuffer.get();
bVoltageSupport = byteBuffer.get();
dwProtocols = byteBuffer.getInt();
byteBuffer.reset();
byteBuffer.position(byteBuffer.position() + FEATURES_OFFSET);
dwFeatures = byteBuffer.getInt();
hasCcidDescriptor = true;
break;
} else {
byteBuffer.position(byteBuffer.position() + len - 2);
}
}
if (!hasCcidDescriptor) {
throw new UsbTransportException("CCID descriptor not found");
}
return new AutoValue_CcidDescription(bMaxSlotIndex, bVoltageSupport, dwProtocols, dwFeatures);
}
Voltage[] getVoltages() {
ArrayList<Voltage> voltages = new ArrayList<>();
if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) {
voltages.add(Voltage.AUTO);
} else {
for (Voltage v : Voltage.values()) {
if ((v.mask & getVoltageSupport()) != 0) {
voltages.add(v);
}
}
}
return voltages.toArray(new Voltage[voltages.size()]);
}
CcidTransportProtocol getSuitableTransportProtocol() throws UsbTransportException {
boolean hasT1Protocol = (getProtocols() & MASK_T1_PROTO) != 0;
if (!hasT1Protocol) {
throw new UsbTransportException("T=0 protocol is not supported!");
}
if (hasFeature(CcidDescription.FEATURE_EXCHANGE_LEVEL_TPDU)) {
return new T1TpduProtocol();
} else if (hasFeature(CcidDescription.FEATURE_EXCHAGE_LEVEL_SHORT_APDU) ||
hasFeature(CcidDescription.FEATURE_EXCHAGE_LEVEL_EXTENDED_APDU)) {
return new T1ShortApduProtocol();
} else {
throw new UsbTransportException("Character level exchange is not supported");
}
}
private boolean hasFeature(int feature) {
return (getFeatures() & feature) != 0;
}
enum Voltage {
AUTO(0, 0), _5V(1, VOLTAGE_5V), _3V(2, VOLTAGE_3V), _1_8V(3, VOLTAGE_1_8V);
final byte mask;
final byte powerOnValue;
Voltage(int powerOnValue, int mask) {
this.powerOnValue = (byte) powerOnValue;
this.mask = (byte) mask;
}
}
}

View file

@ -33,12 +33,15 @@ import com.google.auto.value.AutoValue;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException.UsbCcidErrorException;
public class CcidTransceiver {
private static final int CCID_HEADER_LENGTH = 10;
private static final int MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK = 0x80;
private static final int MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_ON = 0x62;
private static final int MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_OFF = 0x63;
private static final int MESSAGE_TYPE_PC_TO_RDR_XFR_BLOCK = 0x6f;
private static final int COMMAND_STATUS_SUCCESS = 0;
@ -55,15 +58,18 @@ public class CcidTransceiver {
private final UsbDeviceConnection usbConnection;
private final UsbEndpoint usbBulkIn;
private final UsbEndpoint usbBulkOut;
private final CcidDescription usbCcidDescription;
private final byte[] inputBuffer;
private byte currentSequenceNumber;
CcidTransceiver(UsbDeviceConnection connection, UsbEndpoint bulkIn, UsbEndpoint bulkOut) {
CcidTransceiver(UsbDeviceConnection connection, UsbEndpoint bulkIn, UsbEndpoint bulkOut,
CcidDescription ccidDescription) {
usbConnection = connection;
usbBulkIn = bulkIn;
usbBulkOut = bulkOut;
usbCcidDescription = ccidDescription;
inputBuffer = new byte[usbBulkIn.getMaxPacketSize()];
}
@ -79,25 +85,63 @@ public class CcidTransceiver {
skipAvailableInput();
CcidDataBlock response = null;
for (CcidDescription.Voltage v : usbCcidDescription.getVoltages()) {
Log.v(Constants.TAG, "CCID: attempting to power on with voltage " + v.toString());
try {
response = iccPowerOnVoltage(v.powerOnValue);
} catch (UsbCcidErrorException e) {
if (e.getErrorResponse().getError() == 7) { // Power select error
Log.v(Constants.TAG, "CCID: failed to power on with voltage " + v.toString());
iccPowerOff();
Log.v(Constants.TAG, "CCID: powered off");
continue;
}
throw e;
}
break;
}
if (response == null) {
throw new UsbTransportException("Couldn't power up ICC2");
}
long elapsedTime = SystemClock.elapsedRealtime() - startTime;
Log.d(Constants.TAG, "Usb transport connected, took " + elapsedTime + "ms, ATR=" +
Hex.toHexString(response.getData()));
return response;
}
private CcidDataBlock iccPowerOnVoltage(byte voltage) throws UsbTransportException {
byte sequenceNumber = currentSequenceNumber++;
final byte[] iccPowerCommand = {
MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_ON,
0x00, 0x00, 0x00, 0x00,
SLOT_NUMBER,
sequenceNumber,
0x00, // voltage select = auto
voltage,
0x00, 0x00 // reserved for future use
};
sendRaw(iccPowerCommand, 0, iccPowerCommand.length);
CcidDataBlock response = receiveDataBlock(sequenceNumber);
long elapsedTime = SystemClock.elapsedRealtime() - startTime;
return receiveDataBlock(sequenceNumber);
}
Log.d(Constants.TAG, "Usb transport connected T1/TPDU, took " + elapsedTime + "ms, ATR=" +
Hex.toHexString(response.getData()));
private void iccPowerOff() throws UsbTransportException {
byte sequenceNumber = currentSequenceNumber++;
final byte[] iccPowerCommand = {
MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_OFF,
0x00, 0x00, 0x00, 0x00,
0x00,
sequenceNumber,
0x00
};
return response;
sendRaw(iccPowerCommand, 0, iccPowerCommand.length);
}
/**
@ -124,7 +168,7 @@ public class CcidTransceiver {
int sentBytes = 0;
while (sentBytes < data.length) {
int bytesToSend = Math.min(usbBulkIn.getMaxPacketSize(), data.length - sentBytes);
int bytesToSend = Math.min(usbBulkOut.getMaxPacketSize(), data.length - sentBytes);
sendRaw(data, sentBytes, bytesToSend);
sentBytes += bytesToSend;
}
@ -156,7 +200,7 @@ public class CcidTransceiver {
} while (response.isStatusTimeoutExtensionRequest());
if (!response.isStatusSuccess()) {
throw new UsbTransportException("USB-CCID error: " + response);
throw new UsbCcidErrorException("USB-CCID error!", response);
}
return response;
@ -176,7 +220,6 @@ public class CcidTransceiver {
throw new UsbTransportException("USB-CCID error - bad CCID header type " + inputBuffer[0]);
}
CcidDataBlock result = CcidDataBlock.parseHeaderFromBytes(inputBuffer);
if (expectedSequenceNumber != result.getSeq()) {
@ -198,6 +241,7 @@ public class CcidTransceiver {
}
result = result.withData(dataBuffer);
return result;
}

View file

@ -17,6 +17,9 @@
package org.sufficientlysecure.keychain.securitytoken.usb;
import java.io.IOException;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
@ -27,35 +30,22 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Pair;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.securitytoken.CommandApdu;
import org.sufficientlysecure.keychain.securitytoken.ResponseApdu;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo;
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;
import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1ShortApduProtocol;
import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1TpduProtocol;
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 PROTOCOLS_OFFSET = 6;
private static final int FEATURES_OFFSET = 40;
private static final short MASK_T1_PROTO = 2;
// dwFeatures Masks
private static final int MASK_TPDU = 0x10000;
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;
@ -148,57 +138,14 @@ public class UsbTransport implements Transport {
throw new UsbTransportException("USB error: failed to claim interface");
}
byte[] rawDescriptors = usbConnection.getRawDescriptors();
ccidTransportProtocol = getCcidTransportProtocolForRawDescriptors(rawDescriptors);
CcidDescription ccidDescription = CcidDescription.fromRawDescriptors(usbConnection.getRawDescriptors());
Log.d(Constants.TAG, "CCID Description: " + ccidDescription);
CcidTransceiver transceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, ccidDescription);
CcidTransceiver transceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut);
ccidTransportProtocol = ccidDescription.getSuitableTransportProtocol();
ccidTransportProtocol.connect(transceiver);
}
private CcidTransportProtocol getCcidTransportProtocolForRawDescriptors(byte[] desc) throws UsbTransportException {
int dwProtocols = 0, dwFeatures = 0;
boolean hasCcidDescriptor = false;
ByteBuffer byteBuffer = ByteBuffer.wrap(desc).order(ByteOrder.LITTLE_ENDIAN);
while (byteBuffer.hasRemaining()) {
byteBuffer.mark();
byte len = byteBuffer.get(), type = byteBuffer.get();
if (type == 0x21 && len == 0x36) {
byteBuffer.reset();
byteBuffer.position(byteBuffer.position() + PROTOCOLS_OFFSET);
dwProtocols = byteBuffer.getInt();
byteBuffer.reset();
byteBuffer.position(byteBuffer.position() + FEATURES_OFFSET);
dwFeatures = byteBuffer.getInt();
hasCcidDescriptor = true;
break;
} else {
byteBuffer.position(byteBuffer.position() + len - 2);
}
}
if (!hasCcidDescriptor) {
throw new UsbTransportException("CCID descriptor not found");
}
if ((dwProtocols & MASK_T1_PROTO) == 0) {
throw new UsbTransportException("T=0 protocol is not supported");
}
if ((dwFeatures & MASK_TPDU) != 0) {
return new T1TpduProtocol();
} else if (((dwFeatures & MASK_SHORT_APDU) != 0) || ((dwFeatures & MASK_EXTENDED_APDU) != 0)) {
return new T1ShortApduProtocol();
} else {
throw new UsbTransportException("Character level exchange is not supported");
}
}
/**
* Transmit and receive data
*
@ -207,7 +154,17 @@ public class UsbTransport implements Transport {
*/
@Override
public ResponseApdu transceive(CommandApdu data) throws UsbTransportException {
return ResponseApdu.fromBytes(ccidTransportProtocol.transceive(data.toBytes()));
byte[] rawCommand = data.toBytes();
if (Constants.DEBUG) {
Log.d(Constants.TAG, "USB >> " + Hex.toHexString(rawCommand));
}
byte[] rawResponse = ccidTransportProtocol.transceive(rawCommand);
if (Constants.DEBUG) {
Log.d(Constants.TAG, "USB << " + Hex.toHexString(rawResponse));
}
return ResponseApdu.fromBytes(rawResponse);
}
@Override
@ -261,7 +218,10 @@ public class UsbTransport implements Transport {
break;
}
case VENDOR_FSIJ: {
return TokenType.GNUK;
String serialNo = usbConnection.getSerial();
String gnukVersion = SecurityTokenInfo.parseGnukVersionString(serialNo);
boolean versionBigger125 = gnukVersion != null && "1.2.5".compareTo(gnukVersion) < 0;
return versionBigger125 ? TokenType.GNUK_NEWER_1_25 : TokenType.GNUK_OLD;
}
case VENDOR_LEDGER: {
return TokenType.LEDGER_NANO_S;

View file

@ -19,19 +19,32 @@ package org.sufficientlysecure.keychain.securitytoken.usb;
import java.io.IOException;
public class UsbTransportException extends IOException {
public UsbTransportException() {
}
import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransceiver.CcidDataBlock;
public UsbTransportException(final String detailMessage) {
public class UsbTransportException extends IOException {
public UsbTransportException(String detailMessage) {
super(detailMessage);
}
public UsbTransportException(final String message, final Throwable cause) {
public UsbTransportException(String message, final Throwable cause) {
super(message, cause);
}
public UsbTransportException(final Throwable cause) {
public UsbTransportException(Throwable cause) {
super(cause);
}
static class UsbCcidErrorException extends UsbTransportException {
private CcidDataBlock errorResponse;
UsbCcidErrorException(String detailMessage, CcidDataBlock errorResponse) {
super(detailMessage + " " + errorResponse);
this.errorResponse = errorResponse;
}
CcidDataBlock getErrorResponse() {
return errorResponse;
}
}
}

View file

@ -28,9 +28,14 @@ import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransportProtocol;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
import org.sufficientlysecure.keychain.util.Log;
/* T=1 Protocol, see http://www.icedev.se/proxmark3/docs/ISO-7816.pdf, Part 11 */
public class T1TpduProtocol implements CcidTransportProtocol {
private final static int MAX_FRAME_LEN = 254;
private static final byte PPS_PPPSS = (byte) 0xFF;
private static final byte PPS_PPS0_T1 = 1;
@SuppressWarnings("PointlessBitwiseExpression") // constructed per spec
private static final byte PPS_PCK = (byte) (PPS_PPPSS ^ PPS_PPS0_T1);
private CcidTransceiver ccidTransceiver;
private T1TpduBlockFactory blockFactory;
@ -53,7 +58,8 @@ public class T1TpduProtocol implements CcidTransportProtocol {
}
private void performPpsExchange() throws UsbTransportException {
byte[] pps = { (byte) 0xFF, 1, (byte) (0xFF ^ 1) };
// Perform PPS, see ISO-7816, Part 9
byte[] pps = { PPS_PPPSS, PPS_PPS0_T1, PPS_PCK };
CcidDataBlock response = ccidTransceiver.sendXfrBlock(pps);

View file

@ -98,6 +98,7 @@ class ManageSecurityTokenContract {
void requestStoragePermission();
void showErrorCannotReset(boolean isGnuk);
void showErrorCannotUnlock();
}
}

View file

@ -352,6 +352,15 @@ public class ManageSecurityTokenFragment extends Fragment implements ManageSecur
Notify.create(getActivity(), R.string.token_error_locked_indefinitely, Style.ERROR).show();
}
@Override
public void showErrorCannotReset(boolean isGnuk) {
if (isGnuk) {
Notify.create(getActivity(), R.string.token_error_cannot_reset_gnuk_old, Style.ERROR).show();
} else {
Notify.create(getActivity(), R.string.token_error_cannot_reset, Style.ERROR).show();
}
}
@Override
public void showDisplayLogActivity(OperationResult result) {
Intent intent = new Intent(getActivity(), LogDisplayActivity.class);

View file

@ -30,6 +30,7 @@ import org.sufficientlysecure.keychain.operations.results.GenericOperationResult
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType;
import org.sufficientlysecure.keychain.ui.token.ManageSecurityTokenContract.ManageSecurityTokenMvpPresenter;
import org.sufficientlysecure.keychain.ui.token.ManageSecurityTokenContract.ManageSecurityTokenMvpView;
import org.sufficientlysecure.keychain.ui.token.ManageSecurityTokenFragment.StatusLine;
@ -362,6 +363,14 @@ class ManageSecurityTokenPresenter implements ManageSecurityTokenMvpPresenter {
@Override
public void onClickResetToken() {
if (!tokenInfo.isResetSupported()) {
TokenType tokenType = tokenInfo.getTokenType();
boolean isGnuk = tokenType == TokenType.GNUK_OLD || tokenType == TokenType.GNUK_UNKNOWN;
view.showErrorCannotReset(isGnuk);
return;
}
view.showConfirmResetDialog();
}

View file

@ -1910,7 +1910,9 @@
<item quantity="one">"1 attempt left"</item>
<item quantity="other">"%d attempts left"</item>
</plurals>
<string name="token_error_locked_indefinitely">Too many reset attempts. Token is locked irrecoverably!</string>
<string name="token_error_locked_indefinitely">Too many reset attempts. Token cannot be unlocked!</string>
<string name="token_error_cannot_reset_gnuk_old">"The Gnuk Token does not support reset until version 1.2.5"</string>
<string name="token_error_cannot_reset">"This Security Token does not support reset"</string>
<string name="token_menu_change_pin">Change PIN</string>
<string name="token_menu_view_log">View Log</string>

View file

@ -0,0 +1,313 @@
package org.sufficientlysecure.keychain.securitytoken.usb;
import java.util.LinkedList;
import android.annotation.TargetApi;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.os.Build.VERSION_CODES;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.sufficientlysecure.keychain.KeychainTestRunner;
import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransceiver.CcidDataBlock;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException.UsbCcidErrorException;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SuppressWarnings("WeakerAccess")
@RunWith(KeychainTestRunner.class)
@TargetApi(VERSION_CODES.JELLY_BEAN_MR2)
public class CcidTransceiverTest {
static final String ATR = "3bda11ff81b1fe551f0300318473800180009000e4";
static final int MAX_PACKET_LENGTH_IN = 61;
static final int MAX_PACKET_LENGTH_OUT = 63;
UsbDeviceConnection usbConnection;
UsbEndpoint usbBulkIn;
UsbEndpoint usbBulkOut;
LinkedList<byte[]> expectReplies;
LinkedList<byte[]> expectRepliesVerify;
@Before
public void setUp() throws Exception {
usbConnection = mock(UsbDeviceConnection.class);
usbBulkIn = mock(UsbEndpoint.class);
when(usbBulkIn.getMaxPacketSize()).thenReturn(MAX_PACKET_LENGTH_IN);
usbBulkOut = mock(UsbEndpoint.class);
when(usbBulkOut.getMaxPacketSize()).thenReturn(MAX_PACKET_LENGTH_OUT);
expectReplies = new LinkedList<>();
expectRepliesVerify = new LinkedList<>();
when(usbConnection.bulkTransfer(same(usbBulkIn), any(byte[].class), any(Integer.class), any(Integer.class)))
.thenAnswer(
new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
byte[] reply = expectReplies.poll();
if (reply == null) {
return -1;
}
byte[] buf = invocation.getArgumentAt(1, byte[].class);
assertEquals(buf.length, MAX_PACKET_LENGTH_IN);
int len = Math.min(buf.length, reply.length);
System.arraycopy(reply, 0, buf, 0, len);
if (len < reply.length) {
byte[] rest = Arrays.copyOfRange(reply, len, reply.length);
expectReplies.addFirst(rest);
}
return len;
}
});
}
@Test
public void testAutoVoltageSelection() throws Exception {
CcidDescription description = CcidDescription.fromValues((byte) 0, (byte) 1, 2, 132218);
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, description);
byte[] iccPowerOnVoltageAutoCommand = Hex.decode("62000000000000000000");
byte[] iccPowerOnReply = Hex.decode("80150000000000000000" + ATR);
expectReadPreamble();
expect(iccPowerOnVoltageAutoCommand, iccPowerOnReply);
CcidDataBlock ccidDataBlock = ccidTransceiver.iccPowerOn();
verifyDialog();
assertArrayEquals(Hex.decode(ATR), ccidDataBlock.getData());
}
@Test
public void testManualVoltageSelection() throws Exception {
CcidDescription description = CcidDescription.fromValues((byte) 0, (byte) 1, 2, 132210);
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, description);
byte[] iccPowerOnVoltage5VCommand = Hex.decode("62000000000000010000");
byte[] iccPowerOnReply = Hex.decode("80150000000000000000" + ATR);
expectReadPreamble();
expect(iccPowerOnVoltage5VCommand, iccPowerOnReply);
CcidDataBlock ccidDataBlock = ccidTransceiver.iccPowerOn();
verifyDialog();
assertArrayEquals(Hex.decode(ATR), ccidDataBlock.getData());
}
@Test
public void testManualVoltageSelection_failFirst() throws Exception {
CcidDescription description = CcidDescription.fromValues((byte) 0, (byte) 3, 2, 132210);
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, description);
byte[] iccPowerOnVoltage5VCommand = Hex.decode("62000000000000010000");
byte[] iccPowerOnFailureReply = Hex.decode("80000000000000010700");
byte[] iccPowerOffCommand = Hex.decode("6300000000000100");
byte[] iccPowerOnVoltage3VCommand = Hex.decode("62000000000002020000");
byte[] iccPowerOnReply = Hex.decode("80150000000002000000" + ATR);
expectReadPreamble();
expect(iccPowerOnVoltage5VCommand, iccPowerOnFailureReply);
expect(iccPowerOffCommand, null);
expect(iccPowerOnVoltage3VCommand, iccPowerOnReply);
CcidDataBlock ccidDataBlock = ccidTransceiver.iccPowerOn();
verifyDialog();
assertArrayEquals(Hex.decode(ATR), ccidDataBlock.getData());
}
@Test
public void testXfer() throws Exception {
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null);
String commandData = "010203";
byte[] command = Hex.decode("6F030000000000000000" + commandData);
String responseData = "0304";
byte[] response = Hex.decode("80020000000000000000" + responseData);
expect(command, response);
CcidDataBlock ccidDataBlock = ccidTransceiver.sendXfrBlock(Hex.decode(commandData));
verifyDialog();
assertArrayEquals(Hex.decode(responseData), ccidDataBlock.getData());
}
@Test
public void testXfer_IncrementalSeqNums() throws Exception {
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null);
String commandData = "010203";
byte[] commandSeq1 = Hex.decode("6F030000000000000000" + commandData);
byte[] commandSeq2 = Hex.decode("6F030000000001000000" + commandData);
String responseData = "0304";
byte[] responseSeq1 = Hex.decode("80020000000000000000" + responseData);
byte[] responseSeq2 = Hex.decode("80020000000001000000" + responseData);
expect(commandSeq1, responseSeq1);
expect(commandSeq2, responseSeq2);
ccidTransceiver.sendXfrBlock(Hex.decode(commandData));
ccidTransceiver.sendXfrBlock(Hex.decode(commandData));
verifyDialog();
}
@Test(expected = UsbTransportException.class)
public void testXfer_badSeqNumberReply() throws Exception {
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null);
String commandData = "010203";
byte[] command = Hex.decode("6F030000000000000000" + commandData);
String responseData = "0304";
byte[] response = Hex.decode("800200000000AA000000" + responseData);
expect(command, response);
ccidTransceiver.sendXfrBlock(Hex.decode(commandData));
}
@Test
public void testXfer_errorReply() throws Exception {
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null);
String commandData = "010203";
byte[] command = Hex.decode("6F030000000000000000" + commandData);
byte[] response = Hex.decode("80000000000000012A00");
expect(command, response);
try {
ccidTransceiver.sendXfrBlock(Hex.decode(commandData));
} catch (UsbCcidErrorException e) {
assertEquals(0x01, e.getErrorResponse().getIccStatus());
assertEquals(0x2A, e.getErrorResponse().getError());
return;
}
fail();
}
@Test
public void testXfer_chainedCommand() throws Exception {
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null);
String commandData =
"0000000000000123456789000000000000000000000000000000000000000000" +
"0000000000000000000000012345678900000000000000000000000000000000" +
"00000000000001234567890000000000";
byte[] command = Hex.decode("6F500000000000000000" + commandData);
String responseData = "0304";
byte[] response = Hex.decode("80020000000000000000" + responseData);
expectChained(command, response);
CcidDataBlock ccidDataBlock = ccidTransceiver.sendXfrBlock(Hex.decode(commandData));
verifyDialog();
assertArrayEquals(Hex.decode(responseData), ccidDataBlock.getData());
}
@Test
public void testXfer_chainedReply() throws Exception {
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null);
String commandData = "010203";
byte[] command = Hex.decode("6F030000000000000000" + commandData);
String responseData =
"0000000000000000000000000000000000012345678900000000000000000000" +
"0000000000000000000000000001234567890000000000000000000000000000" +
"00000012345678900000000000000000";
byte[] response = Hex.decode("80500000000000000000" + responseData);
expect(command, response);
CcidDataBlock ccidDataBlock = ccidTransceiver.sendXfrBlock(Hex.decode(commandData));
verifyDialog();
assertArrayEquals(Hex.decode(responseData), ccidDataBlock.getData());
}
@Test
public void testXfer_timeoutExtensionReply() throws Exception {
CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null);
String commandData = "010203";
byte[] command = Hex.decode("6F030000000000000000" + commandData);
byte[] timeExtensionResponse = Hex.decode("80000000000000800000");
String responseData = "0304";
byte[] response = Hex.decode("80020000000000000000" + responseData);
expect(command, timeExtensionResponse);
expect(null, response);
CcidDataBlock ccidDataBlock = ccidTransceiver.sendXfrBlock(Hex.decode(commandData));
verifyDialog();
assertArrayEquals(Hex.decode(responseData), ccidDataBlock.getData());
}
private void verifyDialog() {
assertTrue(expectReplies.isEmpty());
assertFalse(expectRepliesVerify.isEmpty());
for (byte[] command : expectRepliesVerify) {
if (command == null) {
continue;
}
verify(usbConnection).bulkTransfer(same(usbBulkIn), aryEq(command), any(Integer.class), any(Integer.class));
}
expectRepliesVerify.clear();
}
private void expectReadPreamble() {
expectReplies.add(null);
expectRepliesVerify.add(null);
}
private void expectChained(byte[] command, byte[] reply) {
for (int i = 0; i < command.length; i+= MAX_PACKET_LENGTH_OUT) {
int len = Math.min(MAX_PACKET_LENGTH_OUT, command.length - i);
when(usbConnection.bulkTransfer(same(usbBulkOut), aryEq(command), eq(i), eq(len),
any(Integer.class))).thenReturn(len);
}
if (reply != null) {
expectReplies.add(reply);
expectRepliesVerify.add(null);
}
}
private void expect(byte[] command, byte[] reply) {
if (command != null) {
when(usbConnection.bulkTransfer(same(usbBulkOut), aryEq(command), eq(0), eq(command.length),
any(Integer.class))).thenReturn(command.length);
}
if (reply != null) {
expectReplies.add(reply);
expectRepliesVerify.add(null);
}
}
}