Merge pull request #1868 from nmikhailov/nitrokey

WIP: Security token USB handling improvements
This commit is contained in:
Dominik Schürmann
2016-08-15 07:46:27 +02:00
committed by GitHub
27 changed files with 2499 additions and 407 deletions

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
import java.nio.ByteBuffer;
public class CardCapabilities {
private static final int MASK_CHAINING = 1 << 7;
private static final int MASK_EXTENDED = 1 << 6;
private byte[] mCapabilityBytes;
public CardCapabilities(byte[] historicalBytes) throws UsbTransportException {
if (historicalBytes[0] != 0x00) {
throw new UsbTransportException("Invalid historical bytes category indicator byte");
}
mCapabilityBytes = getCapabilitiesBytes(historicalBytes);
}
public CardCapabilities() {
mCapabilityBytes = null;
}
private static byte[] getCapabilitiesBytes(byte[] historicalBytes) {
// Compact TLV
ByteBuffer byteBuffer = ByteBuffer.wrap(historicalBytes, 1, historicalBytes.length - 2);
while (byteBuffer.hasRemaining()) {
byte tl = byteBuffer.get();
if (tl == 0x73) { // Capabilities TL
byte[] val = new byte[3];
byteBuffer.get(val);
return val;
}
byteBuffer.position(byteBuffer.position() + (tl & 0xF));
}
return null;
}
public boolean hasChaining() {
return mCapabilityBytes != null && (mCapabilityBytes[2] & MASK_CHAINING) != 0;
}
public boolean hasExtended() {
return mCapabilityBytes != null && (mCapabilityBytes[2] & MASK_EXTENDED) != 0;
}
}

View File

@@ -27,6 +27,15 @@ public class CardException extends IOException {
mResponseCode = responseCode;
}
public CardException(String detailMessage, int responseCode) {
super(detailMessage);
mResponseCode = (short) responseCode;
}
public CardException(String s) {
this(s, -1);
}
public short getResponseCode() {
return mResponseCode;
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken;
// 4.3.3.6 Algorithm Attributes
public class KeyFormat {
private int mAlgorithmId;
private int mModulusLength;
private int mExponentLength;
private AlgorithmFormat mAlgorithmFormat;
public KeyFormat(byte[] bytes) {
mAlgorithmId = bytes[0];
mModulusLength = bytes[1] << 8 | bytes[2];
mExponentLength = bytes[3] << 8 | bytes[4];
mAlgorithmFormat = AlgorithmFormat.from(bytes[5]);
if (mAlgorithmId != 1) { // RSA
throw new IllegalArgumentException("Unsupported Algorithm id " + mAlgorithmId);
}
}
public int getAlgorithmId() {
return mAlgorithmId;
}
public int getModulusLength() {
return mModulusLength;
}
public int getExponentLength() {
return mExponentLength;
}
public AlgorithmFormat getAlgorithmFormat() {
return mAlgorithmFormat;
}
public enum AlgorithmFormat {
STANDARD(0, false, false),
STANDARD_WITH_MODULUS(1, false, true),
CRT(2, true, false),
CRT_WITH_MODULUS(3, true, true);
private int mValue;
private boolean mIncludeModulus;
private boolean mIncludeCrt;
AlgorithmFormat(int value, boolean includeCrt, boolean includeModulus) {
mValue = value;
mIncludeModulus = includeModulus;
mIncludeCrt = includeCrt;
}
public static AlgorithmFormat from(byte b) {
for (AlgorithmFormat format : values()) {
if (format.mValue == b) {
return format;
}
}
return null;
}
public boolean isIncludeModulus() {
return mIncludeModulus;
}
public boolean isIncludeCrt() {
return mIncludeCrt;
}
}
}

View File

@@ -51,7 +51,7 @@ public enum KeyType {
return mIdx;
}
public int getmSlot() {
public int getSlot() {
return mSlot;
}
@@ -59,7 +59,7 @@ public enum KeyType {
return mTimestampObjectId;
}
public int getmFingerprintObjectId() {
public int getFingerprintObjectId() {
return mFingerprintObjectId;
}
}

View File

@@ -19,6 +19,8 @@ package org.sufficientlysecure.keychain.securitytoken;
import android.nfc.Tag;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import java.io.IOException;
@@ -43,8 +45,8 @@ public class NfcTransport implements Transport {
* @throws IOException
*/
@Override
public byte[] transceive(final byte[] data) throws IOException {
return mIsoCard.transceive(data);
public ResponseAPDU transceive(final CommandAPDU data) throws IOException {
return new ResponseAPDU(mIsoCard.transceive(data.getBytes()));
}
/**

View File

@@ -0,0 +1,154 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken;
import org.sufficientlysecure.keychain.util.Iso7816TLV;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class OpenPgpCapabilities {
private final static int MASK_SM = 1 << 7;
private final static int MASK_KEY_IMPORT = 1 << 5;
private final static int MASK_ATTRIBUTES_CHANGABLE = 1 << 2;
private boolean mPw1ValidForMultipleSignatures;
private byte[] mAid;
private byte[] mHistoricalBytes;
private boolean mHasSM;
private boolean mAttriburesChangable;
private boolean mHasKeyImport;
private byte mSMAlgo;
private int mMaxCmdLen;
private int mMaxRspLen;
private Map<KeyType, KeyFormat> mKeyFormats;
public OpenPgpCapabilities(byte[] data) throws IOException {
Iso7816TLV[] tlvs = Iso7816TLV.readList(data, true);
mKeyFormats = new HashMap<>();
if (tlvs.length == 1 && tlvs[0].mT == 0x6E) {
tlvs = ((Iso7816TLV.Iso7816CompositeTLV) tlvs[0]).mSubs;
}
for (Iso7816TLV tlv : tlvs) {
switch (tlv.mT) {
case 0x4F:
mAid = tlv.mV;
break;
case 0x5F52:
mHistoricalBytes = tlv.mV;
break;
case 0x73:
parseDdo((Iso7816TLV.Iso7816CompositeTLV) tlv);
break;
case 0xC0:
parseExtendedCaps(tlv.mV);
break;
case 0xC1:
mKeyFormats.put(KeyType.SIGN, new KeyFormat(tlv.mV));
break;
case 0xC2:
mKeyFormats.put(KeyType.ENCRYPT, new KeyFormat(tlv.mV));
break;
case 0xC3:
mKeyFormats.put(KeyType.AUTH, new KeyFormat(tlv.mV));
break;
case 0xC4:
mPw1ValidForMultipleSignatures = tlv.mV[0] == 1;
break;
}
}
}
private void parseDdo(Iso7816TLV.Iso7816CompositeTLV tlvs) {
for (Iso7816TLV tlv : tlvs.mSubs) {
switch (tlv.mT) {
case 0xC0:
parseExtendedCaps(tlv.mV);
break;
case 0xC1:
mKeyFormats.put(KeyType.SIGN, new KeyFormat(tlv.mV));
break;
case 0xC2:
mKeyFormats.put(KeyType.ENCRYPT, new KeyFormat(tlv.mV));
break;
case 0xC3:
mKeyFormats.put(KeyType.AUTH, new KeyFormat(tlv.mV));
break;
case 0xC4:
mPw1ValidForMultipleSignatures = tlv.mV[0] == 1;
break;
}
}
}
private void parseExtendedCaps(byte[] v) {
mHasSM = (v[0] & MASK_SM) != 0;
mHasKeyImport = (v[0] & MASK_KEY_IMPORT) != 0;
mAttriburesChangable =(v[0] & MASK_ATTRIBUTES_CHANGABLE) != 0;
mSMAlgo = v[1];
mMaxCmdLen = (v[6] << 8) + v[7];
mMaxRspLen = (v[8] << 8) + v[9];
}
public boolean isPw1ValidForMultipleSignatures() {
return mPw1ValidForMultipleSignatures;
}
public byte[] getAid() {
return mAid;
}
public byte[] getHistoricalBytes() {
return mHistoricalBytes;
}
public boolean isHasSM() {
return mHasSM;
}
public boolean isAttriburesChangable() {
return mAttriburesChangable;
}
public boolean isHasKeyImport() {
return mHasKeyImport;
}
public byte getSMAlgo() {
return mSMAlgo;
}
public int getMaxCmdLen() {
return mMaxCmdLen;
}
public int getMaxRspLen() {
return mMaxRspLen;
}
public KeyFormat getFormatForKeyType(KeyType keyType) {
return mKeyFormats.get(keyType);
}
}

View File

@@ -19,7 +19,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken;
import android.support.annotation.NonNull;
@@ -30,33 +29,47 @@ import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
import org.sufficientlysecure.keychain.util.Iso7816TLV;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.SecurityTokenUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.interfaces.RSAPrivateCrtKey;
import nordpol.Apdu;
/**
* This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant
* devices.
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
*/
public class SecurityTokenHelper {
private static final int MAX_APDU_DATAFIELD_SIZE = 254;
private static final int MAX_APDU_NC = 255;
private static final int MAX_APDU_NC_EXT = 65535;
private static final int MAX_APDU_NE = 256;
private static final int MAX_APDU_NE_EXT = 65536;
private static final int APDU_SW_SUCCESS = 0x9000;
private static final int APDU_SW1_RESPONSE_AVAILABLE = 0x61;
private static final int MASK_CLA_CHAINING = 1 << 4;
// Fidesmo constants
private static final String FIDESMO_APPS_AID_PREFIX = "A000000617";
private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
private Transport mTransport;
private CardCapabilities mCardCapabilities;
private OpenPgpCapabilities mOpenPgpCapabilities;
private Passphrase mPin;
private Passphrase mAdminPin;
private boolean mPw1ValidForMultipleSignatures;
private boolean mPw1ValidatedForSignature;
private boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming?
private boolean mPw3Validated;
@@ -68,20 +81,9 @@ public class SecurityTokenHelper {
return LazyHolder.SECURITY_TOKEN_HELPER;
}
private static String getHex(byte[] raw) {
return new String(Hex.encode(raw));
}
private String getHolderName(String name) {
private String getHolderName(byte[] name) {
try {
String slength;
int ilength;
name = name.substring(6);
slength = name.substring(0, 2);
ilength = Integer.parseInt(slength, 16) * 2;
name = name.substring(2, ilength + 2);
name = (new String(Hex.decode(name))).replace('<', ' ');
return name;
return (new String(name, 4, name[3])).replace('<', ' ');
} catch (IndexOutOfBoundsException e) {
// try-catch for https://github.com/FluffyKaon/OpenPGP-Card
// Note: This should not happen, but happens with
@@ -126,8 +128,8 @@ public class SecurityTokenHelper {
keyType.toString()));
}
putKey(keyType.getmSlot(), secretKey, passphrase);
putData(keyType.getmFingerprintObjectId(), secretKey.getFingerprint());
putKey(keyType, secretKey, passphrase);
putData(keyType.getFingerprintObjectId(), secretKey.getFingerprint());
putData(keyType.getTimestampObjectId(), timestampBytes);
}
@@ -150,53 +152,27 @@ public class SecurityTokenHelper {
*/
public void connectToDevice() throws IOException {
// Connect on transport layer
mCardCapabilities = new CardCapabilities();
mTransport.connect();
// Connect on smartcard layer
// SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
// See specification, page 51
String accepted = "9000";
// Command APDU (page 51) for SELECT FILE command (page 29)
String opening =
"00" // CLA
+ "A4" // INS
+ "04" // P1
+ "00" // P2
+ "06" // Lc (number of bytes)
+ "D27600012401" // Data (6 bytes)
+ "00"; // Le
String response = communicate(opening); // activate connection
if (!response.endsWith(accepted)) {
throw new CardException("Initialization failed!", parseCardStatus(response));
CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode("D27600012401"));
ResponseAPDU response = communicate(select); // activate connection
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Initialization failed!", response.getSW());
}
byte[] pwStatusBytes = getPwStatusBytes();
mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1);
mOpenPgpCapabilities = new OpenPgpCapabilities(getData(0x00, 0x6E));
mCardCapabilities = new CardCapabilities(mOpenPgpCapabilities.getHistoricalBytes());
mPw1ValidatedForSignature = false;
mPw1ValidatedForDecrypt = false;
mPw3Validated = false;
}
/**
* Parses out the status word from a JavaCard response string.
*
* @param response A hex string with the response from the card
* @return A short indicating the SW1/SW2, or 0 if a status could not be determined.
*/
private short parseCardStatus(String response) {
if (response.length() < 4) {
return 0; // invalid input
}
try {
return Short.parseShort(response.substring(response.length() - 4), 16);
} catch (NumberFormatException e) {
return 0;
}
}
/**
* Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for
* conformance to the token's requirements for key length.
@@ -230,16 +206,11 @@ public class SecurityTokenHelper {
}
// Command APDU for CHANGE REFERENCE DATA command (page 32)
String changeReferenceDataApdu = "00" // CLA
+ "24" // INS
+ "00" // P1
+ String.format("%02x", pw) // P2
+ String.format("%02x", pin.length + newPin.length) // Lc
+ getHex(pin)
+ getHex(newPin);
String response = communicate(changeReferenceDataApdu); // change PIN
if (!response.equals("9000")) {
throw new CardException("Failed to change PIN", parseCardStatus(response));
CommandAPDU changePin = new CommandAPDU(0x00, 0x24, 0x00, pw, Arrays.concatenate(pin, newPin));
ResponseAPDU response = communicate(changePin);
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Failed to change PIN", response.getSW());
}
}
@@ -254,38 +225,20 @@ public class SecurityTokenHelper {
verifyPin(0x82); // (Verify PW1 with mode 82 for decryption)
}
int offset = 1; // Skip first byte
String response = "", status = "";
// Transmit
while (offset < encryptedSessionKey.length) {
boolean isLastCommand = offset + MAX_APDU_DATAFIELD_SIZE < encryptedSessionKey.length;
String cla = isLastCommand ? "10" : "00";
int len = Math.min(MAX_APDU_DATAFIELD_SIZE, encryptedSessionKey.length - offset);
response = communicate(cla + "2a8086" + Hex.toHexString(new byte[]{(byte) len})
+ Hex.toHexString(encryptedSessionKey, offset, len));
status = response.substring(response.length() - 4);
if (!isLastCommand && !response.endsWith("9000")) {
throw new CardException("Deciphering with Security token failed on transmit", parseCardStatus(response));
}
offset += MAX_APDU_DATAFIELD_SIZE;
byte[] data = Arrays.copyOfRange(encryptedSessionKey, 2, encryptedSessionKey.length);
if (data[0] != 0) {
data = Arrays.prepend(data, (byte) 0x00);
}
// Receive
String result = getDataField(response);
while (response.endsWith("61")) {
response = communicate("00C00000" + status.substring(2));
status = response.substring(response.length() - 4);
result += getDataField(response);
}
if (!status.equals("9000")) {
throw new CardException("Deciphering with Security token failed on receive", parseCardStatus(response));
CommandAPDU command = new CommandAPDU(0x00, 0x2A, 0x80, 0x86, data, MAX_APDU_NE_EXT);
ResponseAPDU response = communicate(command);
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Deciphering with Security token failed on receive", response.getSW());
}
return Hex.decode(result);
return response.getData();
}
/**
@@ -304,12 +257,9 @@ public class SecurityTokenHelper {
pin = mPin.toStringUnsafe().getBytes();
}
// SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
// See specification, page 51
String accepted = "9000";
String response = tryPin(mode, pin); // login
if (!response.equals(accepted)) {
throw new CardException("Bad PIN!", parseCardStatus(response));
ResponseAPDU response = tryPin(mode, pin);// login
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Bad PIN!", response.getSW());
}
if (mode == 0x81) {
@@ -342,16 +292,11 @@ public class SecurityTokenHelper {
verifyPin(0x83); // (Verify PW3)
}
String putDataApdu = "00" // CLA
+ "DA" // INS
+ String.format("%02x", (dataObject & 0xFF00) >> 8) // P1
+ String.format("%02x", dataObject & 0xFF) // P2
+ String.format("%02x", data.length) // Lc
+ getHex(data);
CommandAPDU command = new CommandAPDU(0x00, 0xDA, (dataObject & 0xFF00) >> 8, dataObject & 0xFF, data);
ResponseAPDU response = communicate(command); // put data
String response = communicate(putDataApdu); // put data
if (!response.equals("9000")) {
throw new CardException("Failed to put data.", parseCardStatus(response));
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Failed to put data.", response.getSW());
}
}
@@ -363,12 +308,8 @@ public class SecurityTokenHelper {
* 0xB8: Decipherment Key
* 0xA4: Authentication Key
*/
private void putKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
private void putKey(KeyType slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
throws IOException {
if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
throw new IOException("Invalid key slot");
}
RSAPrivateCrtKey crtSecretKey;
try {
secretKey.unlock(passphrase);
@@ -391,80 +332,17 @@ public class SecurityTokenHelper {
verifyPin(0x83); // (Verify PW3 with mode 83)
}
byte[] header = Hex.decode(
"4D82" + "03A2" // Extended header list 4D82, length of 930 bytes. (page 23)
+ String.format("%02x", slot) + "00" // CRT to indicate targeted key, no length
+ "7F48" + "15" // Private key template 0x7F48, length 21 (decimal, 0x15 hex)
+ "9103" // Public modulus, length 3
+ "928180" // Prime P, length 128
+ "938180" // Prime Q, length 128
+ "948180" // Coefficient (1/q mod p), length 128
+ "958180" // Prime exponent P (d mod (p - 1)), length 128
+ "968180" // Prime exponent Q (d mod (1 - 1)), length 128
+ "97820100" // Modulus, length 256, last item in private key template
+ "5F48" + "820383");// DO 5F48; 899 bytes of concatenated key data will follow
byte[] dataToSend = new byte[934];
byte[] currentKeyObject;
int offset = 0;
System.arraycopy(header, 0, dataToSend, offset, header.length);
offset += header.length;
currentKeyObject = crtSecretKey.getPublicExponent().toByteArray();
System.arraycopy(currentKeyObject, 0, dataToSend, offset, 3);
offset += 3;
// NOTE: For a 2048-bit key, these lengths are fixed. However, bigint includes a leading 0
// in the array to represent sign, so we take care to set the offset to 1 if necessary.
currentKeyObject = crtSecretKey.getPrimeP().toByteArray();
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
Arrays.fill(currentKeyObject, (byte) 0);
offset += 128;
currentKeyObject = crtSecretKey.getPrimeQ().toByteArray();
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
Arrays.fill(currentKeyObject, (byte) 0);
offset += 128;
currentKeyObject = crtSecretKey.getCrtCoefficient().toByteArray();
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
Arrays.fill(currentKeyObject, (byte) 0);
offset += 128;
currentKeyObject = crtSecretKey.getPrimeExponentP().toByteArray();
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
Arrays.fill(currentKeyObject, (byte) 0);
offset += 128;
currentKeyObject = crtSecretKey.getPrimeExponentQ().toByteArray();
System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
Arrays.fill(currentKeyObject, (byte) 0);
offset += 128;
currentKeyObject = crtSecretKey.getModulus().toByteArray();
System.arraycopy(currentKeyObject, currentKeyObject.length - 256, dataToSend, offset, 256);
String putKeyCommand = "10DB3FFF";
String lastPutKeyCommand = "00DB3FFF";
// Now we're ready to communicate with the token.
offset = 0;
String response;
while (offset < dataToSend.length) {
int dataRemaining = dataToSend.length - offset;
if (dataRemaining > 254) {
response = communicate(
putKeyCommand + "FE" + Hex.toHexString(dataToSend, offset, 254)
);
offset += 254;
} else {
int length = dataToSend.length - offset;
response = communicate(
lastPutKeyCommand + String.format("%02x", length)
+ Hex.toHexString(dataToSend, offset, length));
offset += length;
}
byte[] bytes = SecurityTokenUtils.createPrivKeyTemplate(crtSecretKey, slot,
mOpenPgpCapabilities.getFormatForKeyType(slot));
if (!response.endsWith("9000")) {
throw new CardException("Key export to Security Token failed", parseCardStatus(response));
}
CommandAPDU apdu = new CommandAPDU(0x00, 0xDB, 0x3F, 0xFF, bytes);
ResponseAPDU response = communicate(apdu);
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Key export to Security Token failed", response.getSW());
}
// Clear array with secret data before we return.
Arrays.fill(dataToSend, (byte) 0);
}
/**
@@ -474,17 +352,29 @@ public class SecurityTokenHelper {
* @return The fingerprints of all subkeys in a contiguous byte array.
*/
public byte[] getFingerprints() throws IOException {
String data = "00CA006E00";
byte[] buf = mTransport.transceive(Hex.decode(data));
CommandAPDU apdu = new CommandAPDU(0x00, 0xCA, 0x00, 0x6E, MAX_APDU_NE_EXT);
ResponseAPDU response = communicate(apdu);
Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true);
Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint());
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Failed to get fingerprints", response.getSW());
}
Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5);
if (fptlv == null) {
Iso7816TLV[] tlvList = Iso7816TLV.readList(response.getData(), true);
Iso7816TLV fingerPrintTlv = null;
for (Iso7816TLV tlv : tlvList) {
Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint());
Iso7816TLV matchingTlv = Iso7816TLV.findRecursive(tlv, 0xc5);
if (matchingTlv != null) {
fingerPrintTlv = matchingTlv;
}
}
if (fingerPrintTlv == null) {
return null;
}
return fptlv.mV;
return fingerPrintTlv.mV;
}
/**
@@ -493,18 +383,23 @@ public class SecurityTokenHelper {
* @return Seven bytes in fixed format, plus 0x9000 status word at the end.
*/
private byte[] getPwStatusBytes() throws IOException {
String data = "00CA00C400";
return mTransport.transceive(Hex.decode(data));
return getData(0x00, 0xC4);
}
public byte[] getAid() throws IOException {
String info = "00CA004F00";
return mTransport.transceive(Hex.decode(info));
return getData(0x00, 0x4F);
}
public String getUserId() throws IOException {
String info = "00CA006500";
return getHolderName(communicate(info));
return getHolderName(getData(0x00, 0x65));
}
private byte[] getData(int p1, int p2) throws IOException {
ResponseAPDU response = communicate(new CommandAPDU(0x00, 0xCA, p1, p2, MAX_APDU_NE_EXT));
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Failed to get pw status bytes", response.getSW());
}
return response.getData();
}
/**
@@ -518,8 +413,7 @@ public class SecurityTokenHelper {
verifyPin(0x81); // (Verify PW1 with mode 81 for signing)
}
// dsi, including Lc
String dsi;
byte[] dsi;
Log.i(Constants.TAG, "Hash: " + hashAlgo);
switch (hashAlgo) {
@@ -527,95 +421,127 @@ public class SecurityTokenHelper {
if (hash.length != 20) {
throw new IOException("Bad hash length (" + hash.length + ", expected 10!");
}
dsi = "23" // Lc
+ "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
dsi = Arrays.concatenate(Hex.decode(
"3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
+ "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
+ "0605" + "2B0E03021A" // OID of SHA1
+ "0500" // TLV coding of ZERO
+ "0414" + getHex(hash); // 0x14 are 20 hash bytes
+ "0414"), hash); // 0x14 are 20 hash bytes
break;
case HashAlgorithmTags.RIPEMD160:
if (hash.length != 20) {
throw new IOException("Bad hash length (" + hash.length + ", expected 20!");
}
dsi = "233021300906052B2403020105000414" + getHex(hash);
dsi = Arrays.concatenate(Hex.decode("3021300906052B2403020105000414"), hash);
break;
case HashAlgorithmTags.SHA224:
if (hash.length != 28) {
throw new IOException("Bad hash length (" + hash.length + ", expected 28!");
}
dsi = "2F302D300D06096086480165030402040500041C" + getHex(hash);
dsi = Arrays.concatenate(Hex.decode("302D300D06096086480165030402040500041C"), hash);
break;
case HashAlgorithmTags.SHA256:
if (hash.length != 32) {
throw new IOException("Bad hash length (" + hash.length + ", expected 32!");
}
dsi = "333031300D060960864801650304020105000420" + getHex(hash);
dsi = Arrays.concatenate(Hex.decode("3031300D060960864801650304020105000420"), hash);
break;
case HashAlgorithmTags.SHA384:
if (hash.length != 48) {
throw new IOException("Bad hash length (" + hash.length + ", expected 48!");
}
dsi = "433041300D060960864801650304020205000430" + getHex(hash);
dsi = Arrays.concatenate(Hex.decode("3041300D060960864801650304020205000430"), hash);
break;
case HashAlgorithmTags.SHA512:
if (hash.length != 64) {
throw new IOException("Bad hash length (" + hash.length + ", expected 64!");
}
dsi = "533051300D060960864801650304020305000440" + getHex(hash);
dsi = Arrays.concatenate(Hex.decode("3051300D060960864801650304020305000440"), hash);
break;
default:
throw new IOException("Not supported hash algo!");
}
// Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
String apdu =
"002A9E9A" // CLA, INS, P1, P2
+ dsi // digital signature input
+ "00"; // Le
CommandAPDU command = new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, dsi, MAX_APDU_NE_EXT);
ResponseAPDU response = communicate(command);
String response = communicate(apdu);
if (response.length() < 4) {
throw new CardException("Bad response", (short) 0);
}
// split up response into signature and status
String status = response.substring(response.length() - 4);
String signature = response.substring(0, response.length() - 4);
// while we are getting 0x61 status codes, retrieve more data
while (status.substring(0, 2).equals("61")) {
Log.d(Constants.TAG, "requesting more data, status " + status);
// Send GET RESPONSE command
response = communicate("00C00000" + status.substring(2));
status = response.substring(response.length() - 4);
signature += response.substring(0, response.length() - 4);
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Failed to sign", response.getSW());
}
Log.d(Constants.TAG, "final response:" + status);
if (!mPw1ValidForMultipleSignatures) {
if (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) {
mPw1ValidatedForSignature = false;
}
if (!"9000".equals(status)) {
throw new CardException("Bad NFC response code: " + status, parseCardStatus(response));
}
byte[] signature = response.getData();
// Make sure the signature we received is actually the expected number of bytes long!
if (signature.length() != 256 && signature.length() != 512
&& signature.length() != 768 && signature.length() != 1024) {
throw new IOException("Bad signature length! Expected 128/256/384/512 bytes, got " + signature.length() / 2);
if (signature.length != 128 && signature.length != 256
&& signature.length != 384 && signature.length != 512) {
throw new IOException("Bad signature length! Expected 128/256/384/512 bytes, got " + signature.length);
}
return Hex.decode(signature);
return signature;
}
/**
* Transceive data via NFC encoded as Hex
* Transceives APDU
* Splits extended APDU into short APDUs and chains them if necessary
* Performs GET RESPONSE command(ISO/IEC 7816-4 par.7.6.1) on retrieving if necessary
* @param apdu short or extended APDU to transceive
* @return response from the card
* @throws IOException
*/
private String communicate(String apdu) throws IOException {
return getHex(mTransport.transceive(Hex.decode(apdu)));
private ResponseAPDU communicate(CommandAPDU apdu) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
ResponseAPDU lastResponse = null;
// Transmit
if (mCardCapabilities.hasExtended()) {
lastResponse = mTransport.transceive(apdu);
} else if (apdu.getData().length <= MAX_APDU_NC) {
int ne = Math.min(apdu.getNe(), MAX_APDU_NE);
lastResponse = mTransport.transceive(new CommandAPDU(apdu.getCLA(), apdu.getINS(),
apdu.getP1(), apdu.getP2(), apdu.getData(), ne));
} else if (apdu.getData().length > MAX_APDU_NC && mCardCapabilities.hasChaining()) {
int offset = 0;
byte[] data = apdu.getData();
int ne = Math.min(apdu.getNe(), MAX_APDU_NE);
while (offset < data.length) {
int curLen = Math.min(MAX_APDU_NC, data.length - offset);
boolean last = offset + curLen >= data.length;
int cla = apdu.getCLA() + (last ? 0 : MASK_CLA_CHAINING);
lastResponse = mTransport.transceive(new CommandAPDU(cla, apdu.getINS(), apdu.getP1(),
apdu.getP2(), Arrays.copyOfRange(data, offset, offset + curLen), ne));
if (!last && lastResponse.getSW() != APDU_SW_SUCCESS) {
throw new UsbTransportException("Failed to chain apdu");
}
offset += curLen;
}
}
if (lastResponse == null) {
throw new UsbTransportException("Can't transmit command");
}
result.write(lastResponse.getData());
// Receive
while (lastResponse.getSW1() == APDU_SW1_RESPONSE_AVAILABLE) {
// GET RESPONSE ISO/IEC 7816-4 par.7.6.1
CommandAPDU getResponse = new CommandAPDU(0x00, 0xC0, 0x00, 0x00, lastResponse.getSW2());
lastResponse = mTransport.transceive(getResponse);
result.write(lastResponse.getData());
}
result.write(lastResponse.getSW1());
result.write(lastResponse.getSW2());
return new ResponseAPDU(result.toByteArray());
}
public Transport getTransport() {
@@ -631,9 +557,8 @@ public class SecurityTokenHelper {
try {
// By trying to select any apps that have the Fidesmo AID prefix we can
// see if it is a Fidesmo device or not
byte[] mSelectResponse = mTransport.transceive(Apdu.select(FIDESMO_APPS_AID_PREFIX));
// Compare the status returned by our select with the OK status code
return Apdu.hasStatus(mSelectResponse, Apdu.OK_APDU);
CommandAPDU apdu = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode(FIDESMO_APPS_AID_PREFIX));
return communicate(apdu).getSW() == APDU_SW_SUCCESS;
} catch (IOException e) {
Log.e(Constants.TAG, "Card communication failed!", e);
}
@@ -664,38 +589,19 @@ public class SecurityTokenHelper {
verifyPin(0x83); // (Verify PW3 with mode 83)
}
String generateKeyApdu = "0047800002" + String.format("%02x", slot) + "0000";
String getResponseApdu = "00C00000";
CommandAPDU apdu = new CommandAPDU(0x00, 0x47, 0x80, 0x00, new byte[]{(byte) slot, 0x00}, MAX_APDU_NE_EXT);
ResponseAPDU response = communicate(apdu);
String first = communicate(generateKeyApdu);
String second = communicate(getResponseApdu);
if (!second.endsWith("9000")) {
if (response.getSW() != APDU_SW_SUCCESS) {
throw new IOException("On-card key generation failed");
}
String publicKeyData = getDataField(first) + getDataField(second);
Log.d(Constants.TAG, "Public Key Data Objects: " + publicKeyData);
return Hex.decode(publicKeyData);
return response.getData();
}
private String getDataField(String output) {
return output.substring(0, output.length() - 4);
}
private String tryPin(int mode, byte[] pin) throws IOException {
private ResponseAPDU tryPin(int mode, byte[] pin) throws IOException {
// Command APDU for VERIFY command (page 32)
String login =
"00" // CLA
+ "20" // INS
+ "00" // P1
+ String.format("%02x", mode) // P2
+ String.format("%02x", pin.length) // Lc
+ Hex.toHexString(pin);
return communicate(login);
return communicate(new CommandAPDU(0x00, 0x20, 0x00, mode, pin));
}
/**
@@ -704,35 +610,37 @@ public class SecurityTokenHelper {
* Afterwards, the token is reactivated.
*/
public void resetAndWipeToken() throws IOException {
String accepted = "9000";
// try wrong PIN 4 times until counter goes to C0
byte[] pin = "XXXXXX".getBytes();
for (int i = 0; i <= 4; i++) {
String response = tryPin(0x81, pin);
if (response.equals(accepted)) { // Should NOT accept!
throw new CardException("Should never happen, XXXXXX has been accepted!", parseCardStatus(response));
ResponseAPDU response = tryPin(0x81, pin);
if (response.getSW() == APDU_SW_SUCCESS) { // Should NOT accept!
throw new CardException("Should never happen, XXXXXX has been accepted!", response.getSW());
}
}
// try wrong Admin PIN 4 times until counter goes to C0
byte[] adminPin = "XXXXXXXX".getBytes();
for (int i = 0; i <= 4; i++) {
String response = tryPin(0x83, adminPin);
if (response.equals(accepted)) { // Should NOT accept!
throw new CardException("Should never happen, XXXXXXXX has been accepted", parseCardStatus(response));
ResponseAPDU response = tryPin(0x83, adminPin);
if (response.getSW() == APDU_SW_SUCCESS) { // Should NOT accept!
throw new CardException("Should never happen, XXXXXXXX has been accepted", response.getSW());
}
}
// reactivate token!
String reactivate1 = "00" + "e6" + "00" + "00";
String reactivate2 = "00" + "44" + "00" + "00";
String response1 = communicate(reactivate1);
String response2 = communicate(reactivate2);
if (!response1.equals(accepted) || !response2.equals(accepted)) {
throw new CardException("Reactivating failed!", parseCardStatus(response1));
// NOTE: keep the order here! First execute _both_ reactivate commands. Before checking _both_ responses
// If a token is in a bad state and reactivate1 fails, it could still be reactivated with reactivate2
CommandAPDU reactivate1 = new CommandAPDU(0x00, 0xE6, 0x00, 0x00);
CommandAPDU reactivate2 = new CommandAPDU(0x00, 0x44, 0x00, 0x00);
ResponseAPDU response1 = communicate(reactivate1);
ResponseAPDU response2 = communicate(reactivate2);
if (response1.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Reactivating failed!", response1.getSW());
}
if (response2.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Reactivating failed!", response2.getSW());
}
}
/**

View File

@@ -17,6 +17,9 @@
package org.sufficientlysecure.keychain.securitytoken;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import java.io.IOException;
/**
@@ -29,7 +32,7 @@ public interface Transport {
* @return received data
* @throws IOException
*/
byte[] transceive(byte[] data) throws IOException;
ResponseAPDU transceive(CommandAPDU data) throws IOException;
/**
* Disconnect and release connection

View File

@@ -0,0 +1,169 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class CcidTransceiver {
private static final int TIMEOUT = 20 * 1000; // 20s
private byte mCounter;
private UsbDeviceConnection mConnection;
private UsbEndpoint mBulkIn;
private UsbEndpoint mBulkOut;
public CcidTransceiver(final UsbDeviceConnection connection, final UsbEndpoint bulkIn,
final UsbEndpoint bulkOut) {
mConnection = connection;
mBulkIn = bulkIn;
mBulkOut = bulkOut;
}
public byte[] receiveRaw() throws UsbTransportException {
byte[] bytes;
do {
bytes = receive();
} while (isDataBlockNotReady(bytes));
checkDataBlockResponse(bytes);
return Arrays.copyOfRange(bytes, 10, bytes.length);
}
/**
* Power of ICC
* Spec: 6.1.1 PC_to_RDR_IccPowerOn
*
* @throws UsbTransportException
*/
@NonNull
public byte[] iccPowerOn() throws UsbTransportException {
final byte[] iccPowerCommand = {
0x62,
0x00, 0x00, 0x00, 0x00,
0x00,
mCounter++,
0x00,
0x00, 0x00
};
sendRaw(iccPowerCommand);
long startTime = System.currentTimeMillis();
byte[] atr = null;
while (true) {
try {
atr = receiveRaw();
break;
} catch (Exception e) {
// Try more startTime
if (System.currentTimeMillis() - startTime > TIMEOUT) {
break;
}
}
SystemClock.sleep(100);
}
if (atr == null) {
throw new UsbTransportException("Couldn't power up Security Token");
}
return atr;
}
/**
* Transmits XfrBlock
* 6.1.4 PC_to_RDR_XfrBlock
* @param payload payload to transmit
* @throws UsbTransportException
*/
public void sendXfrBlock(byte[] payload) throws UsbTransportException {
int l = payload.length;
byte[] data = Arrays.concatenate(new byte[]{
0x6f,
(byte) l, (byte) (l >> 8), (byte) (l >> 16), (byte) (l >> 24),
0x00,
mCounter++,
0x00,
0x00, 0x00},
payload);
int send = 0;
while (send < data.length) {
final int len = Math.min(mBulkIn.getMaxPacketSize(), data.length - send);
sendRaw(Arrays.copyOfRange(data, send, send + len));
send += len;
}
}
public byte[] receive() throws UsbTransportException {
byte[] buffer = new byte[mBulkIn.getMaxPacketSize()];
byte[] result = null;
int readBytes = 0, totalBytes = 0;
do {
int res = mConnection.bulkTransfer(mBulkIn, buffer, buffer.length, TIMEOUT);
if (res < 0) {
throw new UsbTransportException("USB error - failed to receive response " + res);
}
if (result == null) {
if (res < 10) {
throw new UsbTransportException("USB-CCID error - failed to receive CCID header");
}
totalBytes = ByteBuffer.wrap(buffer, 1, 4).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get() + 10;
result = new byte[totalBytes];
}
System.arraycopy(buffer, 0, result, readBytes, res);
readBytes += res;
} while (readBytes < totalBytes);
return result;
}
private void sendRaw(final byte[] data) throws UsbTransportException {
final int tr1 = mConnection.bulkTransfer(mBulkOut, data, data.length, TIMEOUT);
if (tr1 != data.length) {
throw new UsbTransportException("USB error - failed to transmit data " + tr1);
}
}
private static byte getStatus(byte[] bytes) {
return (byte) ((bytes[7] >> 6) & 0x03);
}
private void checkDataBlockResponse(byte[] bytes) throws UsbTransportException {
final byte status = getStatus(bytes);
if (status != 0) {
throw new UsbTransportException("USB-CCID error - status " + status + " error code: " + Hex.toHexString(bytes, 8, 1));
}
}
private static boolean isDataBlockNotReady(byte[] bytes) {
return getStatus(bytes) == 2;
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb;
import android.support.annotation.NonNull;
public interface CcidTransportProtocol {
byte[] transceive(@NonNull byte[] apdu) throws UsbTransportException;
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb;
import android.support.annotation.NonNull;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.Log;
public class T1ShortApduProtocol implements CcidTransportProtocol {
private CcidTransceiver mTransceiver;
public T1ShortApduProtocol(CcidTransceiver transceiver) throws UsbTransportException {
mTransceiver = transceiver;
byte[] atr = mTransceiver.iccPowerOn();
Log.d(Constants.TAG, "Usb transport connected T1/Short APDU, ATR=" + Hex.toHexString(atr));
}
@Override
public byte[] transceive(@NonNull final byte[] apdu) throws UsbTransportException {
mTransceiver.sendXfrBlock(apdu);
return mTransceiver.receiveRaw();
}
}

View File

@@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken;
package org.sufficientlysecure.keychain.securitytoken.usb;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
@@ -27,9 +27,11 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Pair;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.securitytoken.Transport;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1TpduProtocol;
import org.sufficientlysecure.keychain.util.Log;
import java.io.IOException;
@@ -42,8 +44,15 @@ import java.nio.ByteOrder;
* Implements small subset of these features
*/
public class UsbTransport implements Transport {
private static final int USB_CLASS_SMARTCARD = 11;
private static final int TIMEOUT = 20 * 1000; // 20s
private 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;
private final UsbManager mUsbManager;
private final UsbDevice mUsbDevice;
@@ -51,39 +60,14 @@ public class UsbTransport implements Transport {
private UsbEndpoint mBulkIn;
private UsbEndpoint mBulkOut;
private UsbDeviceConnection mConnection;
private byte mCounter;
private CcidTransceiver mTransceiver;
private CcidTransportProtocol mProtocol;
public UsbTransport(UsbDevice usbDevice, UsbManager usbManager) {
mUsbDevice = usbDevice;
mUsbManager = usbManager;
}
/**
* Manage ICC power, Yubikey requires to power on ICC
* Spec: 6.1.1 PC_to_RDR_IccPowerOn; 6.1.2 PC_to_RDR_IccPowerOff
*
* @param on true to turn ICC on, false to turn it off
* @throws UsbTransportException
*/
private void setIccPower(boolean on) throws UsbTransportException {
final byte[] iccPowerCommand = {
(byte) (on ? 0x62 : 0x63),
0x00, 0x00, 0x00, 0x00,
0x00,
mCounter++,
0x00,
0x00, 0x00
};
sendRaw(iccPowerCommand);
byte[] bytes;
do {
bytes = receive();
} while (isDataBlockNotReady(bytes));
checkDataBlockResponse(bytes);
}
/**
* Get first class 11 (Chip/Smartcard) interface of the device
*
@@ -94,7 +78,7 @@ public class UsbTransport implements Transport {
private static UsbInterface getSmartCardInterface(UsbDevice device) {
for (int i = 0; i < device.getInterfaceCount(); i++) {
UsbInterface anInterface = device.getInterface(i);
if (anInterface.getInterfaceClass() == USB_CLASS_SMARTCARD) {
if (anInterface.getInterfaceClass() == UsbConstants.USB_CLASS_CSCID) {
return anInterface;
}
}
@@ -165,7 +149,6 @@ public class UsbTransport implements Transport {
*/
@Override
public void connect() throws IOException {
mCounter = 0;
mUsbInterface = getSmartCardInterface(mUsbDevice);
if (mUsbInterface == null) {
// Shouldn't happen as we whitelist only class 11 devices
@@ -189,8 +172,56 @@ public class UsbTransport implements Transport {
throw new UsbTransportException("USB error - failed to claim interface");
}
setIccPower(true);
Log.d(Constants.TAG, "Usb transport connected");
mTransceiver = new CcidTransceiver(mConnection, mBulkIn, mBulkOut);
configureProtocol();
}
private void configureProtocol() throws UsbTransportException {
byte[] desc = mConnection.getRawDescriptors();
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) {
mProtocol = new T1TpduProtocol(mTransceiver);
} else if (((dwFeatures & MASK_SHORT_APDU) != 0) || ((dwFeatures & MASK_EXTENDED_APDU) != 0)) {
mProtocol = new T1ShortApduProtocol(mTransceiver);
} else {
throw new UsbTransportException("Character level exchange is not supported");
}
}
/**
@@ -200,87 +231,8 @@ public class UsbTransport implements Transport {
* @throws UsbTransportException
*/
@Override
public byte[] transceive(byte[] data) throws UsbTransportException {
sendXfrBlock(data);
byte[] bytes;
do {
bytes = receive();
} while (isDataBlockNotReady(bytes));
checkDataBlockResponse(bytes);
// Discard header
return Arrays.copyOfRange(bytes, 10, bytes.length);
}
/**
* Transmits XfrBlock
* 6.1.4 PC_to_RDR_XfrBlock
* @param payload payload to transmit
* @throws UsbTransportException
*/
private void sendXfrBlock(byte[] payload) throws UsbTransportException {
int l = payload.length;
byte[] data = Arrays.concatenate(new byte[]{
0x6f,
(byte) l, (byte) (l >> 8), (byte) (l >> 16), (byte) (l >> 24),
0x00,
mCounter++,
0x00,
0x00, 0x00},
payload);
int send = 0;
while (send < data.length) {
final int len = Math.min(mBulkIn.getMaxPacketSize(), data.length - send);
sendRaw(Arrays.copyOfRange(data, send, send + len));
send += len;
}
}
private byte[] receive() throws UsbTransportException {
byte[] buffer = new byte[mBulkIn.getMaxPacketSize()];
byte[] result = null;
int readBytes = 0, totalBytes = 0;
do {
int res = mConnection.bulkTransfer(mBulkIn, buffer, buffer.length, TIMEOUT);
if (res < 0) {
throw new UsbTransportException("USB error - failed to receive response " + res);
}
if (result == null) {
if (res < 10) {
throw new UsbTransportException("USB-CCID error - failed to receive CCID header");
}
totalBytes = ByteBuffer.wrap(buffer, 1, 4).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get() + 10;
result = new byte[totalBytes];
}
System.arraycopy(buffer, 0, result, readBytes, res);
readBytes += res;
} while (readBytes < totalBytes);
return result;
}
private void sendRaw(final byte[] data) throws UsbTransportException {
final int tr1 = mConnection.bulkTransfer(mBulkOut, data, data.length, TIMEOUT);
if (tr1 != data.length) {
throw new UsbTransportException("USB error - failed to transmit data " + tr1);
}
}
private byte getStatus(byte[] bytes) {
return (byte) ((bytes[7] >> 6) & 0x03);
}
private void checkDataBlockResponse(byte[] bytes) throws UsbTransportException {
final byte status = getStatus(bytes);
if (status != 0) {
throw new UsbTransportException("USB-CCID error - status " + status + " error code: " + Hex.toHexString(bytes, 8, 1));
}
}
private boolean isDataBlockNotReady(byte[] bytes) {
return getStatus(bytes) == 2;
public ResponseAPDU transceive(CommandAPDU data) throws UsbTransportException {
return new ResponseAPDU(mProtocol.transceive(data.getBytes()));
}
@Override

View File

@@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken;
package org.sufficientlysecure.keychain.securitytoken.usb;
import java.io.IOException;

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb.tpdu;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
public class Block {
protected static final int MAX_PAYLOAD_LEN = 254;
protected static final int OFFSET_NAD = 0;
protected static final int OFFSET_PCB = 1;
protected static final int OFFSET_LEN = 2;
protected static final int OFFSET_DATA = 3;
protected byte[] mData;
protected BlockChecksumType mChecksumType;
public Block(BlockChecksumType checksumType, byte[] data) throws UsbTransportException {
this.mChecksumType = checksumType;
this.mData = data;
int checksumOffset = this.mData.length - mChecksumType.getLength();
byte[] checksum = mChecksumType.computeChecksum(data, 0, checksumOffset);
if (!Arrays.areEqual(checksum, getEdc())) {
throw new UsbTransportException("TPDU CRC doesn't match");
}
}
protected Block(BlockChecksumType checksumType, byte nad, byte pcb, byte[] apdu)
throws UsbTransportException {
this.mChecksumType = checksumType;
if (apdu.length > MAX_PAYLOAD_LEN) {
throw new UsbTransportException("APDU is too long; should be split");
}
this.mData = Arrays.concatenate(
new byte[]{nad, pcb, (byte) apdu.length},
apdu,
new byte[mChecksumType.getLength()]);
int checksumOffset = this.mData.length - mChecksumType.getLength();
byte[] checksum = mChecksumType.computeChecksum(this.mData, 0, checksumOffset);
System.arraycopy(checksum, 0, this.mData, checksumOffset, mChecksumType.getLength());
}
protected Block(Block baseBlock) {
this.mChecksumType = baseBlock.getChecksumType();
this.mData = baseBlock.getRawData();
}
public byte getNad() {
return mData[OFFSET_NAD];
}
public byte getPcb() {
return mData[OFFSET_PCB];
}
public byte getLen() {
return mData[OFFSET_LEN];
}
public byte[] getEdc() {
return Arrays.copyOfRange(mData, mData.length - mChecksumType.getLength(), mData.length);
}
public BlockChecksumType getChecksumType() {
return mChecksumType;
}
public byte[] getApdu() {
return Arrays.copyOfRange(mData, OFFSET_DATA, mData.length - mChecksumType.getLength());
}
public byte[] getRawData() {
return mData;
}
@Override
public String toString() {
return Hex.toHexString(mData);
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb.tpdu;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
public enum BlockChecksumType {
LRC(1), CRC(2);
private int mLength;
BlockChecksumType(int length) {
mLength = length;
}
public byte[] computeChecksum(byte[] data, int offset, int len) throws UsbTransportException {
if (this == LRC) {
byte res = 0;
for (int i = offset; i < len; i++) {
res ^= data[i];
}
return new byte[]{res};
} else {
throw new UsbTransportException("CRC checksum is not implemented");
}
}
public int getLength() {
return mLength;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb.tpdu;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
public enum FrameType {
I_BLOCK(0b00000000, 0b10000000, 6, true), // Information
R_BLOCK(0b10000000, 0b11000000, 4, false), // Receipt ack
S_BLOCK(0b11000000, 0b11000000, -1, false); // System
private byte mValue;
private byte mMask;
private int mSequenceBit;
private boolean mChainingSupported;
FrameType(int value, int mask, int sequenceBit, boolean chaining) {
// Accept ints just to avoid cast in creation
this.mValue = (byte) value;
this.mMask = (byte) mask;
this.mSequenceBit = sequenceBit;
this.mChainingSupported = chaining;
}
public static FrameType fromPCB(byte pcb) throws UsbTransportException {
for (final FrameType frameType : values()) {
if ((frameType.mMask & pcb) == frameType.mValue) {
return frameType;
}
}
throw new UsbTransportException("Invalid PCB byte");
}
public int getSequenceBit() {
return mSequenceBit;
}
public boolean isChainingSupported() {
return mChainingSupported;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb.tpdu;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
public class IBlock extends Block {
public static final byte MASK_RBLOCK = (byte) 0b10000000;
public static final byte MASK_VALUE_RBLOCK = (byte) 0b00000000;
private static final byte BIT_SEQUENCE = 6;
private static final byte BIT_CHAINING = 5;
public IBlock(final Block baseBlock) {
super(baseBlock);
}
public IBlock(BlockChecksumType checksumType, byte nad, byte sequence, boolean chaining,
byte[] apdu) throws UsbTransportException {
super(checksumType, nad,
(byte) (((sequence & 1) << BIT_SEQUENCE) | (chaining ? 1 << BIT_CHAINING : 0)),
apdu);
}
public byte getSequence() {
return (byte) ((getPcb() >> BIT_SEQUENCE) & 1);
}
public boolean getChaining() {
return ((getPcb() >> BIT_CHAINING) & 1) != 0;
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb.tpdu;
import android.support.annotation.NonNull;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
public class RBlock extends Block {
public static final byte MASK_RBLOCK = (byte) 0b11000000;
public static final byte MASK_VALUE_RBLOCK = (byte) 0b10000000;
private static final byte BIT_SEQUENCE = 4;
public RBlock(Block baseBlock) throws UsbTransportException {
super(baseBlock);
if (getApdu().length != 0) {
throw new UsbTransportException("Data in R-block");
}
}
public RBlock(BlockChecksumType checksumType, byte nad, byte sequence)
throws UsbTransportException {
super(checksumType, nad, (byte) (MASK_VALUE_RBLOCK | ((sequence & 1) << BIT_SEQUENCE)), new byte[0]);
}
public RError getError() throws UsbTransportException {
return RError.from(getPcb());
}
public enum RError {
NO_ERROR(0), EDC_ERROR(1), OTHER_ERROR(2);
private byte mLowBits;
RError(int lowBits) {
mLowBits = (byte) lowBits;
}
@NonNull
public static RError from(byte i) throws UsbTransportException {
for (final RError error : values()) {
if (error.mLowBits == (i & 0x3)) {
return error;
}
}
throw new UsbTransportException("Invalid R block error bits");
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb.tpdu;
public class SBlock extends Block {
public static final byte MASK_SBLOCK = (byte) 0b11000000;
public static final byte MASK_VALUE_SBLOCK = (byte) 0b11000000;
public SBlock(Block baseBlock) {
super(baseBlock);
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.securitytoken.usb.tpdu;
import android.support.annotation.NonNull;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransceiver;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException;
import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransportProtocol;
import org.sufficientlysecure.keychain.util.Log;
public class T1TpduProtocol implements CcidTransportProtocol {
private final static int MAX_FRAME_LEN = 254;
private byte mCounter = 0;
private CcidTransceiver mTransceiver;
private BlockChecksumType mChecksumType;
public T1TpduProtocol(final CcidTransceiver transceiver) throws UsbTransportException {
mTransceiver = transceiver;
// Connect
byte[] atr = mTransceiver.iccPowerOn();
Log.d(Constants.TAG, "Usb transport connected T1/TPDU, ATR=" + Hex.toHexString(atr));
// TODO: set checksum from atr
mChecksumType = BlockChecksumType.LRC;
// PPS all auto
pps();
}
protected void pps() throws UsbTransportException {
byte[] pps = new byte[]{(byte) 0xFF, 1, (byte) (0xFF ^ 1)};
mTransceiver.sendXfrBlock(pps);
byte[] ppsResponse = mTransceiver.receiveRaw();
Log.d(Constants.TAG, "PPS response " + Hex.toHexString(ppsResponse));
}
public byte[] transceive(@NonNull byte[] apdu) throws UsbTransportException {
int start = 0;
if (apdu.length == 0) {
throw new UsbTransportException("Cant transcive zero-length apdu(tpdu)");
}
Block responseBlock = null;
while (apdu.length - start > 0) {
boolean hasMore = start + MAX_FRAME_LEN < apdu.length;
int len = Math.min(MAX_FRAME_LEN, apdu.length - start);
// Send next frame
Block block = newIBlock(mCounter++, hasMore, Arrays.copyOfRange(apdu, start, start + len));
mTransceiver.sendXfrBlock(block.getRawData());
// Receive I or R block
responseBlock = getBlockFromResponse(mTransceiver.receiveRaw());
start += len;
if (responseBlock instanceof SBlock) {
Log.d(Constants.TAG, "S-Block received " + responseBlock.toString());
// just ignore
} else if (responseBlock instanceof RBlock) {
Log.d(Constants.TAG, "R-Block received " + responseBlock.toString());
if (((RBlock) responseBlock).getError() != RBlock.RError.NO_ERROR) {
throw new UsbTransportException("R-Block reports error "
+ ((RBlock) responseBlock).getError());
}
} else { // I block
if (start != apdu.length) {
throw new UsbTransportException("T1 frame response underflow");
}
break;
}
}
// Receive
if (responseBlock == null || !(responseBlock instanceof IBlock))
throw new UsbTransportException("Invalid tpdu sequence state");
byte[] responseApdu = responseBlock.getApdu();
while (((IBlock) responseBlock).getChaining()) {
Block ackBlock = newRBlock((byte) (((IBlock) responseBlock).getSequence() + 1));
mTransceiver.sendXfrBlock(ackBlock.getRawData());
responseBlock = getBlockFromResponse(mTransceiver.receiveRaw());
if (responseBlock instanceof IBlock) {
responseApdu = Arrays.concatenate(responseApdu, responseBlock.getApdu());
} else {
Log.d(Constants.TAG, "Response block received " + responseBlock.toString());
throw new UsbTransportException("Response: invalid state - invalid block received");
}
}
return responseApdu;
}
// Factory methods
public Block getBlockFromResponse(byte[] data) throws UsbTransportException {
final Block baseBlock = new Block(mChecksumType, data);
if ((baseBlock.getPcb() & IBlock.MASK_RBLOCK) == IBlock.MASK_VALUE_RBLOCK) {
return new IBlock(baseBlock);
} else if ((baseBlock.getPcb() & SBlock.MASK_SBLOCK) == SBlock.MASK_VALUE_SBLOCK) {
return new SBlock(baseBlock);
} else if ((baseBlock.getPcb() & RBlock.MASK_RBLOCK) == RBlock.MASK_VALUE_RBLOCK) {
return new RBlock(baseBlock);
}
throw new UsbTransportException("TPDU Unknown block type");
}
public IBlock newIBlock(byte sequence, boolean chaining, byte[] apdu) throws UsbTransportException {
return new IBlock(mChecksumType, (byte) 0, sequence, chaining, apdu);
}
public RBlock newRBlock(byte sequence) throws UsbTransportException {
return new RBlock(mChecksumType, (byte) 0, sequence);
}
}

View File

@@ -47,7 +47,7 @@ import org.sufficientlysecure.keychain.securitytoken.NfcTransport;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenHelper;
import org.sufficientlysecure.keychain.securitytoken.Transport;
import org.sufficientlysecure.keychain.util.UsbConnectionDispatcher;
import org.sufficientlysecure.keychain.securitytoken.UsbTransport;
import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransport;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity;
import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
import org.sufficientlysecure.keychain.ui.ViewKeyActivity;

View File

@@ -0,0 +1,154 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.util;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.securitytoken.KeyFormat;
import org.sufficientlysecure.keychain.securitytoken.KeyType;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.interfaces.RSAPrivateCrtKey;
public class SecurityTokenUtils {
public static byte[] createPrivKeyTemplate(RSAPrivateCrtKey secretKey, KeyType slot,
KeyFormat format) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream(),
template = new ByteArrayOutputStream(),
data = new ByteArrayOutputStream(),
res = new ByteArrayOutputStream();
int expLengthBytes = (format.getExponentLength() + 7) / 8;
// Public exponent
template.write(new byte[]{(byte) 0x91, (byte) expLengthBytes});
writeBits(data, secretKey.getPublicExponent(), expLengthBytes);
// Prime P, length 128
template.write(Hex.decode("928180"));
writeBits(data, secretKey.getPrimeP(), 128);
// Prime Q, length 128
template.write(Hex.decode("938180"));
writeBits(data, secretKey.getPrimeQ(), 128);
if (format.getAlgorithmFormat().isIncludeCrt()) {
// Coefficient (1/q mod p), length 128
template.write(Hex.decode("948180"));
writeBits(data, secretKey.getCrtCoefficient(), 128);
// Prime exponent P (d mod (p - 1)), length 128
template.write(Hex.decode("958180"));
writeBits(data, secretKey.getPrimeExponentP(), 128);
// Prime exponent Q (d mod (1 - 1)), length 128
template.write(Hex.decode("968180"));
writeBits(data, secretKey.getPrimeExponentQ(), 128);
}
if (format.getAlgorithmFormat().isIncludeModulus()) {
// Modulus, length 256, last item in private key template
template.write(Hex.decode("97820100"));
writeBits(data, secretKey.getModulus(), 256);
}
// Bundle up
// Ext header list data
// Control Reference Template to indicate the private key
stream.write(slot.getSlot());
stream.write(0);
// Cardholder private key template
stream.write(Hex.decode("7F48"));
stream.write(encodeLength(template.size()));
stream.write(template.toByteArray());
// Concatenation of key data as defined in DO 7F48
stream.write(Hex.decode("5F48"));
stream.write(encodeLength(data.size()));
stream.write(data.toByteArray());
// Result tlv
res.write(Hex.decode("4D"));
res.write(encodeLength(stream.size()));
res.write(stream.toByteArray());
return res.toByteArray();
}
public static byte[] encodeLength(int len) {
if (len < 0) {
throw new IllegalArgumentException("length is negative");
} else if (len >= 16777216) {
throw new IllegalArgumentException("length is too big: " + len);
}
byte[] res;
if (len < 128) {
res = new byte[1];
res[0] = (byte) len;
} else if (len < 256) {
res = new byte[2];
res[0] = -127;
res[1] = (byte) len;
} else if (len < 65536) {
res = new byte[3];
res[0] = -126;
res[1] = (byte) (len / 256);
res[2] = (byte) (len % 256);
} else {
res = new byte[4];
res[0] = -125;
res[1] = (byte) (len / 65536);
res[2] = (byte) (len / 256);
res[3] = (byte) (len % 256);
}
return res;
}
public static void writeBits(ByteArrayOutputStream stream, BigInteger value, int width) {
if (value.signum() == -1) {
throw new IllegalArgumentException("value is negative");
} else if (width <= 0) {
throw new IllegalArgumentException("width <= 0");
}
byte[] prime = value.toByteArray();
int stripIdx = 0;
while (prime[stripIdx] == 0 && stripIdx + 1 < prime.length) {
stripIdx++;
}
if (prime.length - stripIdx > width) {
throw new IllegalArgumentException("not enough width to fit value: "
+ prime.length + "/" + width);
}
byte[] res = new byte[width];
int empty = width - (prime.length - stripIdx);
System.arraycopy(prime, stripIdx, res, Math.max(0, empty), Math.min(prime.length, width));
stream.write(res, 0, width);
Arrays.fill(res, (byte) 0);
Arrays.fill(prime, (byte) 0);
}
}