diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CommandAPDUFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CommandAPDUFactory.java new file mode 100644 index 000000000..ce78d7e41 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CommandAPDUFactory.java @@ -0,0 +1,140 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +import android.support.annotation.NonNull; + +import javax.smartcardio.CommandAPDU; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; + + +class CommandAPDUFactory { + 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; + + final int CLA = 0x00; + + final int MASK_CLA_CHAINING = 1 << 4; + + @NonNull + CommandAPDU createPutKeyCommand(byte[] keyBytes) { + return new CommandAPDU(CLA, 0xDB, 0x3F, 0xFF, keyBytes); + } + + @NonNull + CommandAPDU createComputeDigitalSignatureCommand(byte[] data) { + return new CommandAPDU(CLA, 0x2A, 0x9E, 0x9A, data, MAX_APDU_NE_EXT); + } + + + @NonNull + CommandAPDU createPutDataCommand(int dataObject, byte[] data) { + return new CommandAPDU(CLA, 0xDA, (dataObject & 0xFF00) >> 8, dataObject & 0xFF, data); + } + + @NonNull + CommandAPDU createDecipherCommand(byte[] data) { + return new CommandAPDU(CLA, 0x2A, 0x80, 0x86, data, MAX_APDU_NE_EXT); + } + + @NonNull + CommandAPDU createChangeReferenceDataCommand(int pw, byte[] newPin, byte[] pin) { + return new CommandAPDU(CLA, 0x24, 0x00, pw, Arrays.concatenate(pin, newPin)); + } + + @NonNull + CommandAPDU createResetRetryCounter(byte[] newPin) { + return new CommandAPDU(CLA, 0x2C, 0x02, 0x81, newPin); + } + + @NonNull + CommandAPDU createGetDataCommand(int p1, int p2) { + return new CommandAPDU(CLA, 0xCA, p1, p2, MAX_APDU_NE_EXT); + } + + @NonNull + CommandAPDU createGenerateKeyCommand(int slot) { + return new CommandAPDU(CLA, 0x47, 0x80, 0x00, new byte[] { (byte) slot, 0x00 }, MAX_APDU_NE_EXT); + } + + @NonNull + CommandAPDU createGetResponseCommand(int lastResponseSw2) { + return new CommandAPDU(CLA, 0xC0, 0x00, 0x00, lastResponseSw2); + } + + + @NonNull + CommandAPDU createVerifyCommand(int mode, byte[] pin) { + return new CommandAPDU(CLA, 0x20, 0x00, mode, pin); + } + + @NonNull + CommandAPDU createSelectFileCommand(String fileAid) { + return new CommandAPDU(CLA, 0xA4, 0x04, 0x00, Hex.decode(fileAid)); + } + + @NonNull + CommandAPDU createReactivate2Command() { + return new CommandAPDU(CLA, 0x44, 0x00, 0x00); + } + + @NonNull + CommandAPDU createReactivate1Command() { + return new CommandAPDU(CLA, 0xE6, 0x00, 0x00); + } + + @NonNull + CommandAPDU createInternalAuthenticateCommand(ByteArrayOutputStream pkout) { + return new CommandAPDU(0, 0x88, 0x01, 0x0, pkout.toByteArray(), MAX_APDU_NE_EXT); + } + + @NonNull + CommandAPDU createRetrievePublicKeyCommand(byte[] msgCert) { + return new CommandAPDU(0, 0x47, 0x81, 0x00, msgCert, MAX_APDU_NE_EXT); + } + + @NonNull + CommandAPDU createRetrieveCertificateCommand() { + return new CommandAPDU(0, 0xA5, 0x03, 0x04, + new byte[]{0x60, 0x04, 0x5C, 0x02, 0x7F, 0x21}); + } + + @NonNull + CommandAPDU createShortApdu(CommandAPDU apdu) { + int ne = Math.min(apdu.getNe(), MAX_APDU_NE); + return new CommandAPDU(apdu.getCLA(), apdu.getINS(), apdu.getP1(), apdu.getP2(), apdu.getData(), ne); + } + + @NonNull + List createChainedApdus(CommandAPDU apdu) { + ArrayList result = new ArrayList<>(); + + 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); + + CommandAPDU cmd = + new CommandAPDU(cla, apdu.getINS(), apdu.getP1(), apdu.getP2(), data, offset, curLen, ne); + result.add(cmd); + + offset += curLen; + } + + return result; + } + + boolean isSuitableForShortApdu(CommandAPDU apdu) { + return apdu.getData().length <= MAX_APDU_NC; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SCP11bSecureMessaging.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SCP11bSecureMessaging.java index fd619ccd0..0f0aa9521 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SCP11bSecureMessaging.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SCP11bSecureMessaging.java @@ -275,7 +275,7 @@ class SCP11bSecureMessaging implements SecureMessaging { } - public static void establish(final SecurityTokenConnection t, final Context ctx) + public static void establish(final SecurityTokenConnection t, final Context ctx, CommandAPDUFactory commandFactory) throws SecureMessagingException, IOException { CommandAPDU cmd; @@ -285,8 +285,7 @@ class SCP11bSecureMessaging implements SecureMessaging { t.clearSecureMessaging(); // retrieving key algorithm - cmd = new CommandAPDU(0, (byte)0xCA, (byte)0x00, - OPENPGP_SECURE_MESSAGING_KEY_ATTRIBUTES_TAG, SecurityTokenConnection.MAX_APDU_NE_EXT); + cmd = commandFactory.createGetDataCommand(0x00, OPENPGP_SECURE_MESSAGING_KEY_ATTRIBUTES_TAG); resp = t.communicate(cmd); if (resp.getSW() != SecurityTokenConnection.APDU_SW_SUCCESS) { throw new SecureMessagingException("failed to retrieve secure messaging key attributes"); @@ -317,13 +316,12 @@ class SCP11bSecureMessaging implements SecureMessaging { if (prefs != null && prefs.getExperimentalSmartPGPAuthoritiesEnable()) { // retrieving certificate - cmd = new CommandAPDU(0, (byte) 0xA5, (byte) 0x03, (byte) 0x04, - new byte[]{(byte) 0x60, (byte) 0x04, (byte) 0x5C, (byte) 0x02, (byte) 0x7F, (byte) 0x21}); + cmd = commandFactory.createRetrieveCertificateCommand(); resp = t.communicate(cmd); if (resp.getSW() != SecurityTokenConnection.APDU_SW_SUCCESS) { throw new SecureMessagingException("failed to select secure messaging certificate"); } - cmd = new CommandAPDU(0, (byte) 0xCA, (byte) 0x7F, (byte) 0x21, SecurityTokenConnection.MAX_APDU_NE_EXT); + cmd = commandFactory.createGetDataCommand(0x7F, 0x21); resp = t.communicate(cmd); if (resp.getSW() != SecurityTokenConnection.APDU_SW_SUCCESS) { throw new SecureMessagingException("failed to retrieve secure messaging certificate"); @@ -333,8 +331,7 @@ class SCP11bSecureMessaging implements SecureMessaging { } else { // retrieving public key - cmd = new CommandAPDU(0, (byte) 0x47, (byte) 0x81, (byte) 0x00, - OPENPGP_SECURE_MESSAGING_KEY_CRT, SecurityTokenConnection.MAX_APDU_NE_EXT); + cmd = commandFactory.createRetrievePublicKeyCommand(OPENPGP_SECURE_MESSAGING_KEY_CRT); resp = t.communicate(cmd); if (resp.getSW() != SecurityTokenConnection.APDU_SW_SUCCESS) { throw new SecureMessagingException("failed to retrieve secure messaging public key"); @@ -395,8 +392,7 @@ class SCP11bSecureMessaging implements SecureMessaging { pkout = bout; // internal authenticate - cmd = new CommandAPDU(0, (byte)0x88, (byte)0x01, (byte)0x0, pkout.toByteArray(), - SecurityTokenConnection.MAX_APDU_NE_EXT); + cmd = commandFactory.createInternalAuthenticateCommand(pkout); resp = t.communicate(cmd); if (resp.getSW() != SecurityTokenConnection.APDU_SW_SUCCESS) { throw new SecureMessagingException("failed to initiate internal authenticate"); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java index f26534e5b..1d95c2e9f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -64,6 +64,8 @@ import java.security.NoSuchAlgorithmException; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateCrtKey; +import java.util.List; + /** * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant @@ -71,17 +73,9 @@ import java.security.interfaces.RSAPrivateCrtKey; * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf */ public class SecurityTokenConnection { - 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; - static final int MAX_APDU_NE_EXT = 65536; - 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"; @@ -95,6 +89,7 @@ public class SecurityTokenConnection { private final Transport mTransport; @NonNull private final Passphrase mPin; + private final CommandAPDUFactory commandFactory; private CardCapabilities mCardCapabilities; private OpenPgpCapabilities mOpenPgpCapabilities; @@ -115,6 +110,8 @@ public class SecurityTokenConnection { private SecurityTokenConnection(@NonNull Transport transport, @NonNull Passphrase pin) { this.mTransport = transport; this.mPin = pin; + + commandFactory = new CommandAPDUFactory(); } private String getHolderName(byte[] name) { @@ -186,7 +183,7 @@ public class SecurityTokenConnection { // Connect on smartcard layer // Command APDU (page 51) for SELECT FILE command (page 29) - CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode("D27600012401")); + CommandAPDU select = commandFactory.createSelectFileCommand("D27600012401"); ResponseAPDU response = communicate(select); // activate connection if (response.getSW() != APDU_SW_SUCCESS) { @@ -202,7 +199,7 @@ public class SecurityTokenConnection { if (mOpenPgpCapabilities.isHasSCP11bSM()) { try { - SCP11bSecureMessaging.establish(this, context); + SCP11bSecureMessaging.establish(this, context, commandFactory); } catch (SecureMessagingException e) { mSecureMessaging = null; Log.e(Constants.TAG, "failed to establish secure messaging", e); @@ -225,7 +222,7 @@ public class SecurityTokenConnection { } // Command APDU for RESET RETRY COUNTER command (page 33) - CommandAPDU changePin = new CommandAPDU(0x00, 0x2C, 0x02, 0x81, newPin); + CommandAPDU changePin = commandFactory.createResetRetryCounter(newPin); ResponseAPDU response = communicate(changePin); if (response.getSW() != APDU_SW_SUCCESS) { @@ -269,7 +266,7 @@ public class SecurityTokenConnection { } // Command APDU for CHANGE REFERENCE DATA command (page 32) - CommandAPDU changePin = new CommandAPDU(0x00, 0x24, 0x00, pw, Arrays.concatenate(pin, newPin)); + CommandAPDU changePin = commandFactory.createChangeReferenceDataCommand(pw, newPin, pin); ResponseAPDU response = communicate(changePin); if (response.getSW() != APDU_SW_SUCCESS) { @@ -349,7 +346,7 @@ public class SecurityTokenConnection { throw new CardException("Unknown encryption key type!"); } - CommandAPDU command = new CommandAPDU(0x00, 0x2A, 0x80, 0x86, data, MAX_APDU_NE_EXT); + CommandAPDU command = commandFactory.createDecipherCommand(data); ResponseAPDU response = communicate(command); if (response.getSW() != APDU_SW_SUCCESS) { @@ -466,7 +463,7 @@ public class SecurityTokenConnection { verifyAdminPin(adminPin); } - CommandAPDU command = new CommandAPDU(0x00, 0xDA, (dataObject & 0xFF00) >> 8, dataObject & 0xFF, data); + CommandAPDU command = commandFactory.createPutDataCommand(dataObject, data); ResponseAPDU response = communicate(command); // put data if (response.getSW() != APDU_SW_SUCCESS) { @@ -474,7 +471,6 @@ public class SecurityTokenConnection { } } - private void setKeyAttributes(Passphrase adminPin, final KeyType slot, final CanonicalizedSecretKey secretKey) throws IOException { @@ -566,7 +562,7 @@ public class SecurityTokenConnection { throw new IOException(e.getMessage()); } - CommandAPDU apdu = new CommandAPDU(0x00, 0xDB, 0x3F, 0xFF, keyBytes); + CommandAPDU apdu = commandFactory.createPutKeyCommand(keyBytes); ResponseAPDU response = communicate(apdu); if (response.getSW() != APDU_SW_SUCCESS) { @@ -581,7 +577,7 @@ public class SecurityTokenConnection { * @return The fingerprints of all subkeys in a contiguous byte array. */ public byte[] getFingerprints() throws IOException { - CommandAPDU apdu = new CommandAPDU(0x00, 0xCA, 0x00, 0x6E, MAX_APDU_NE_EXT); + CommandAPDU apdu = commandFactory.createGetDataCommand(0x00, 0x6E); ResponseAPDU response = communicate(apdu); if (response.getSW() != APDU_SW_SUCCESS) { @@ -629,7 +625,7 @@ public class SecurityTokenConnection { } private byte[] getData(int p1, int p2) throws IOException { - ResponseAPDU response = communicate(new CommandAPDU(0x00, 0xCA, p1, p2, MAX_APDU_NE_EXT)); + ResponseAPDU response = communicate(commandFactory.createGetDataCommand(p1, p2)); if (response.getSW() != APDU_SW_SUCCESS) { throw new CardException("Failed to get pw status bytes", response.getSW()); } @@ -711,7 +707,7 @@ public class SecurityTokenConnection { } // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37) - CommandAPDU command = new CommandAPDU(0x00, 0x2A, 0x9E, 0x9A, data, MAX_APDU_NE_EXT); + CommandAPDU command = commandFactory.createComputeDigitalSignatureCommand(data); ResponseAPDU response = communicate(command); if (response.getSW() != APDU_SW_SUCCESS) { @@ -756,7 +752,6 @@ public class SecurityTokenConnection { return signature; } - /** * Transceives APDU * Splits extended APDU into short APDUs and chains them if necessary @@ -776,45 +771,36 @@ public class SecurityTokenConnection { } } - 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); + } else if (commandFactory.isSuitableForShortApdu(apdu)) { + CommandAPDU shortApdu = commandFactory.createShortApdu(apdu); + lastResponse = mTransport.transceive(shortApdu); + } else if (mCardCapabilities.hasChaining()) { + List chainedApdus = commandFactory.createChainedApdus(apdu); + for (int i = 0, totalCommands = chainedApdus.size(); i < totalCommands; i++) { + CommandAPDU chainedApdu = chainedApdus.get(i); + lastResponse = mTransport.transceive(chainedApdu); - lastResponse = mTransport.transceive(new CommandAPDU(cla, apdu.getINS(), apdu.getP1(), - apdu.getP2(), data, offset, curLen, ne)); - - if (!last && lastResponse.getSW() != APDU_SW_SUCCESS) { + boolean isLastCommand = i < totalCommands - 1; + if (isLastCommand && lastResponse.getSW() != APDU_SW_SUCCESS) { throw new UsbTransportException("Failed to chain apdu (last SW: " + lastResponse.getSW() + ")"); } - - offset += curLen; } } if (lastResponse == null) { throw new UsbTransportException("Can't transmit command"); } + ByteArrayOutputStream result = new ByteArrayOutputStream(); 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()); + CommandAPDU getResponse = commandFactory.createGetResponseCommand(lastResponse.getSW2()); lastResponse = mTransport.transceive(getResponse); result.write(lastResponse.getData()); } @@ -841,7 +827,7 @@ public class SecurityTokenConnection { try { // By trying to select any apps that have the Fidesmo AID prefix we can // see if it is a Fidesmo device or not - CommandAPDU apdu = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, Hex.decode(FIDESMO_APPS_AID_PREFIX)); + CommandAPDU apdu = commandFactory.createSelectFileCommand(FIDESMO_APPS_AID_PREFIX); return communicate(apdu).getSW() == APDU_SW_SUCCESS; } catch (IOException e) { Log.e(Constants.TAG, "Card communication failed!", e); @@ -873,7 +859,7 @@ public class SecurityTokenConnection { verifyAdminPin(adminPin); } - CommandAPDU apdu = new CommandAPDU(0x00, 0x47, 0x80, 0x00, new byte[]{(byte) slot, 0x00}, MAX_APDU_NE_EXT); + CommandAPDU apdu = commandFactory.createGenerateKeyCommand(slot); ResponseAPDU response = communicate(apdu); if (response.getSW() != APDU_SW_SUCCESS) { @@ -885,7 +871,7 @@ public class SecurityTokenConnection { private ResponseAPDU tryPin(int mode, byte[] pin) throws IOException { // Command APDU for VERIFY command (page 32) - return communicate(new CommandAPDU(0x00, 0x20, 0x00, mode, pin)); + return communicate(commandFactory.createVerifyCommand(mode, pin)); } /** @@ -918,8 +904,8 @@ public class SecurityTokenConnection { // reactivate token! // 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); + CommandAPDU reactivate1 = commandFactory.createReactivate1Command(); + CommandAPDU reactivate2 = commandFactory.createReactivate2Command(); ResponseAPDU response1 = communicate(reactivate1); ResponseAPDU response2 = communicate(reactivate2); if (response1.getSW() != APDU_SW_SUCCESS) {