commit
83ab483fc7
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ class ManageSecurityTokenContract {
|
|||
|
||||
void requestStoragePermission();
|
||||
|
||||
void showErrorCannotReset(boolean isGnuk);
|
||||
void showErrorCannotUnlock();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue