SecurityToken: use CommandAPDU instead of raw strings

This commit is contained in:
Nikita Mikhailov
2016-05-14 15:35:36 +06:00
parent 131bf94b9b
commit c2ca440c88
5 changed files with 135 additions and 228 deletions

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

@@ -19,6 +19,8 @@ package org.sufficientlysecure.keychain.securitytoken;
import android.nfc.Tag;
import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU;
import org.sufficientlysecure.keychain.securitytoken.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

@@ -30,6 +30,8 @@ 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 org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU;
import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU;
import org.sufficientlysecure.keychain.util.Iso7816TLV;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Passphrase;
@@ -47,7 +49,14 @@ import nordpol.Apdu;
* 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 * 500;
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;
// Fidesmo constants
private static final String FIDESMO_APPS_AID_PREFIX = "A000000617";
@@ -72,16 +81,9 @@ public class SecurityTokenHelper {
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
@@ -153,23 +155,12 @@ public class SecurityTokenHelper {
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();
@@ -179,24 +170,6 @@ public class SecurityTokenHelper {
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 +203,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,43 +222,20 @@ public class SecurityTokenHelper {
verifyPin(0x82); // (Verify PW1 with mode 82 for decryption)
}
int offset = 2; // Skip first byte TODO: why?
String response = "", status = "";
boolean shouldPad = true;
// Transmit
while (offset < encryptedSessionKey.length) {
boolean isLastCommand = MAX_APDU_DATAFIELD_SIZE >= encryptedSessionKey.length - offset;
String cla = isLastCommand ? "00" : "10";
int len = Math.min(MAX_APDU_DATAFIELD_SIZE, encryptedSessionKey.length - offset + (shouldPad ? 1 : 0));
String command = cla + "2a8086"
+ Hex.toHexString(new byte[]{(byte) ((len >> 16) & 0xFF), (byte) ((len >> 8) & 0xFF), (byte) (len & 0xFF)})
+ (shouldPad ? "00": "")
+ Hex.toHexString(encryptedSessionKey, offset, len - (shouldPad ? 1 : 0)) + "0000";
shouldPad = false;
response = communicate(command);
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();
}
/**
@@ -309,12 +254,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) {
@@ -347,16 +289,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());
}
}
@@ -442,34 +379,18 @@ public class SecurityTokenHelper {
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;
}
if (!response.endsWith("9000")) {
throw new CardException("Key export to Security Token failed", parseCardStatus(response));
}
}
CommandAPDU apdu = new CommandAPDU(0x00, 0xDB, 0x3F, 0xFF, dataToSend);
// Clear array with secret data before we return.
Arrays.fill(dataToSend, (byte) 0);
ResponseAPDU response = communicate(apdu);
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Key export to Security Token failed", response.getSW());
}
}
/**
@@ -479,25 +400,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);
ResponseAPDU response = communicate(apdu);
Iso7816TLV[] tlvs = Iso7816TLV.readList(buf, true);
Iso7816TLV fptlv = null;
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Failed to get fingerprints", response.getSW());
}
for (int i = 0; i < tlvs.length; i++) {
Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlvs[i].prettyPrint());
Iso7816TLV[] tlvList = Iso7816TLV.readList(response.getData(), true);
Iso7816TLV fingerPrintTlv = null;
Iso7816TLV tlv = Iso7816TLV.findRecursive(tlvs[i], 0xc5);
if (tlv != null) {
fptlv = tlv;
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 (fptlv == null) {
if (fingerPrintTlv == null) {
return null;
}
return fptlv.mV;
return fingerPrintTlv.mV;
}
/**
@@ -506,18 +431,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));
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Failed to get pw status bytes", response.getSW());
}
return response.getData();
}
/**
@@ -532,7 +462,7 @@ public class SecurityTokenHelper {
}
// dsi, including Lc
String dsi;
byte[] dsi;
Log.i(Constants.TAG, "Hash: " + hashAlgo);
switch (hashAlgo) {
@@ -540,86 +470,65 @@ 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
+ "0000"
+ dsi // digital signature input
+ "0000"; // 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);
if (response.getSW() != APDU_SW_SUCCESS) {
throw new CardException("Failed to sign", response.getSW());
}
// 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);
}
Log.d(Constants.TAG, "final response:" + status);
if (!mPw1ValidForMultipleSignatures) {
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);
@@ -628,8 +537,8 @@ public class SecurityTokenHelper {
/**
* Transceive data via NFC encoded as Hex
*/
private String communicate(String apdu) throws IOException {
return getHex(mTransport.transceive(Hex.decode(apdu)));
private ResponseAPDU communicate(CommandAPDU apdu) throws IOException {
return mTransport.transceive(apdu);
}
public Transport getTransport() {
@@ -645,9 +554,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);
}
@@ -678,38 +586,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));
}
/**
@@ -723,30 +612,33 @@ public class SecurityTokenHelper {
// 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));
}
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 org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU;
import org.sufficientlysecure.keychain.securitytoken.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

@@ -29,8 +29,9 @@ import android.util.Pair;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.securitytoken.Transport;
import org.sufficientlysecure.keychain.securitytoken.smartcardio.CommandAPDU;
import org.sufficientlysecure.keychain.securitytoken.smartcardio.ResponseAPDU;
import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1TpduProtocol;
import org.sufficientlysecure.keychain.util.Iso7816TLV;
import org.sufficientlysecure.keychain.util.Log;
import java.io.IOException;
@@ -230,8 +231,8 @@ public class UsbTransport implements Transport {
* @throws UsbTransportException
*/
@Override
public byte[] transceive(byte[] data) throws UsbTransportException {
return mProtocol.transceive(data);
public ResponseAPDU transceive(CommandAPDU data) throws UsbTransportException {
return new ResponseAPDU(mProtocol.transceive(data.getBytes()));
}
@Override