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 b8ebf3ef8..ce882a272 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SCP11bSecureMessaging.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SCP11bSecureMessaging.java @@ -55,6 +55,7 @@ import java.security.spec.InvalidParameterSpecException; import java.util.ArrayList; import android.content.Context; +import android.support.annotation.CheckResult; import android.support.annotation.NonNull; import javax.crypto.BadPaddingException; @@ -272,7 +273,9 @@ class SCP11bSecureMessaging implements SecureMessaging { } - static void establish(final SecurityTokenConnection t, final Context ctx, OpenPgpCommandApduFactory commandFactory) + @CheckResult + static SecureMessaging establish(final SecurityTokenConnection t, final Context ctx, + OpenPgpCommandApduFactory commandFactory) throws SecureMessagingException, IOException { CommandApdu cmd; @@ -478,7 +481,7 @@ class SCP11bSecureMessaging implements SecureMessaging { final SCP11bSecureMessaging sm = new SCP11bSecureMessaging(); sm.setKeys(sEnc, sMac, sRmac, receipt); - t.setSecureMessaging(sm); + return sm; } catch (InvalidKeySpecException e) { throw new SecureMessagingException("invalid key specification : " + e.getMessage()); 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 26a68469b..78a5aa883 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -88,26 +88,7 @@ public class SecurityTokenConnection { this.commandFactory = commandFactory; } - OpenPgpCapabilities getOpenPgpCapabilities() { - return openPgpCapabilities; - } - - OpenPgpCommandApduFactory getCommandFactory() { - return commandFactory; - } - - private String getHolderName(byte[] name) { - try { - 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 - // https://github.com/FluffyKaon/OpenPGP-Card, thus return an empty string for now! - - Log.e(Constants.TAG, "Couldn't get holder name, returning empty string!", e); - return ""; - } - } + // region connection management public void connectIfNecessary(Context context) throws IOException { if (isConnected()) { @@ -143,14 +124,7 @@ public class SecurityTokenConnection { isPw1ValidatedForOther = false; isPw3Validated = false; - if (openPgpCapabilities.isHasSCP11bSM()) { - try { - SCP11bSecureMessaging.establish(this, context, commandFactory); - } catch (SecureMessagingException e) { - secureMessaging = null; - Log.e(Constants.TAG, "failed to establish secure messaging", e); - } - } + smEstablishIfAvailable(context); } @VisibleForTesting @@ -191,6 +165,137 @@ public class SecurityTokenConnection { this.cardCapabilities = new CardCapabilities(openPgpCapabilities.getHistoricalBytes()); } + // endregion + + // region communication + + /** + * 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 commandApdu short or extended APDU to transceive + * @return response from the card + */ + ResponseApdu communicate(CommandApdu commandApdu) throws IOException { + commandApdu = smEncryptIfAvailable(commandApdu); + + ResponseApdu lastResponse; + + lastResponse = transceiveWithChaining(commandApdu); + lastResponse = readChainedResponseIfAvailable(lastResponse); + + lastResponse = smDecryptIfAvailable(lastResponse); + + return lastResponse; + } + + @NonNull + private ResponseApdu transceiveWithChaining(CommandApdu commandApdu) throws IOException { + if (cardCapabilities.hasExtended()) { + return transport.transceive(commandApdu); + } else if (commandFactory.isSuitableForShortApdu(commandApdu)) { + CommandApdu shortApdu = commandFactory.createShortApdu(commandApdu); + return transport.transceive(shortApdu); + } else if (cardCapabilities.hasChaining()) { + ResponseApdu lastResponse = null; + + List chainedApdus = commandFactory.createChainedApdus(commandApdu); + for (int i = 0, totalCommands = chainedApdus.size(); i < totalCommands; i++) { + CommandApdu chainedApdu = chainedApdus.get(i); + lastResponse = transport.transceive(chainedApdu); + + boolean isLastCommand = (i == totalCommands - 1); + if (!isLastCommand && !lastResponse.isSuccess()) { + throw new IOException("Failed to chain apdu " + + "(" + i + "/" + (totalCommands-1) + ", last SW: " + lastResponse.getSw() + ")"); + } + } + + if (lastResponse == null) { + throw new IllegalStateException(); + } + + return lastResponse; + } else { + throw new IOException("Command too long, and chaining unavailable"); + } + } + + @NonNull + private ResponseApdu readChainedResponseIfAvailable(ResponseApdu lastResponse) throws IOException { + if (lastResponse.getSw1() != APDU_SW1_RESPONSE_AVAILABLE) { + return lastResponse; + } + + ByteArrayOutputStream result = new ByteArrayOutputStream(); + result.write(lastResponse.getData()); + + do { + // GET RESPONSE ISO/IEC 7816-4 par.7.6.1 + CommandApdu getResponse = commandFactory.createGetResponseCommand(lastResponse.getSw2()); + lastResponse = transport.transceive(getResponse); + result.write(lastResponse.getData()); + } while (lastResponse.getSw1() == APDU_SW1_RESPONSE_AVAILABLE); + + result.write(lastResponse.getSw1()); + result.write(lastResponse.getSw2()); + + return ResponseApdu.fromBytes(result.toByteArray()); + } + + // endregion + + // region secure messaging + + private void smEstablishIfAvailable(Context context) throws IOException { + if (!openPgpCapabilities.isHasSCP11bSM()) { + return; + } + + try { + secureMessaging = SCP11bSecureMessaging.establish(this, context, commandFactory); + } catch (SecureMessagingException e) { + secureMessaging = null; + Log.e(Constants.TAG, "failed to establish secure messaging", e); + } + } + + private CommandApdu smEncryptIfAvailable(CommandApdu apdu) throws IOException { + if (secureMessaging == null || !secureMessaging.isEstablished()) { + return apdu; + } + try { + return secureMessaging.encryptAndSign(apdu); + } catch (SecureMessagingException e) { + clearSecureMessaging(); + throw new IOException("secure messaging encrypt/sign failure : " + e.getMessage()); + } + } + + private ResponseApdu smDecryptIfAvailable(ResponseApdu response) throws IOException { + if (secureMessaging == null || !secureMessaging.isEstablished()) { + return response; + } + try { + return secureMessaging.verifyAndDecrypt(response); + } catch (SecureMessagingException e) { + clearSecureMessaging(); + throw new IOException("secure messaging verify/decrypt failure : " + e.getMessage()); + } + } + + public void clearSecureMessaging() { + if (secureMessaging != null) { + secureMessaging.clearSession(); + } + secureMessaging = null; + } + + // endregion + + // region pin management + void verifyPinForSignature() throws IOException { if (isPw1ValidatedForSignature) { return; @@ -253,37 +358,7 @@ public class SecurityTokenConnection { isPw3Validated = false; } - /** - * Return fingerprints of all keys from application specific data stored - * on tag, or null if data not available. - * - * @return The fingerprints of all subkeys in a contiguous byte array. - */ - public byte[] getFingerprints() throws IOException { - return openPgpCapabilities.getFingerprints(); - } - - /** - * Return the PW Status Bytes from the token. This is a simple DO; no TLV decoding needed. - * - * @return Seven bytes in fixed format, plus 0x9000 status word at the end. - */ - byte[] getPwStatusBytes() throws IOException { - return openPgpCapabilities.getPwStatusBytes(); - } - - public byte[] getAid() throws IOException { - return openPgpCapabilities.getAid(); - } - - public String getUrl() throws IOException { - byte[] data = getData(0x5F, 0x50); - return new String(data).trim(); - } - - public String getUserId() throws IOException { - return getHolderName(getData(0x00, 0x65)); - } + // endregion private byte[] getData(int p1, int p2) throws IOException { ResponseApdu response = communicate(commandFactory.createGetDataCommand(p1, p2)); @@ -293,99 +368,40 @@ public class SecurityTokenConnection { return response.getData(); } - /** - * 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 - */ - ResponseApdu communicate(CommandApdu apdu) throws IOException { - if ((secureMessaging != null) && secureMessaging.isEstablished()) { - try { - apdu = secureMessaging.encryptAndSign(apdu); - } catch (SecureMessagingException e) { - clearSecureMessaging(); - throw new IOException("secure messaging encrypt/sign failure : " + e.getMessage()); - } - } - ResponseApdu lastResponse = null; - // Transmit - if (cardCapabilities.hasExtended()) { - lastResponse = transport.transceive(apdu); - } else if (commandFactory.isSuitableForShortApdu(apdu)) { - CommandApdu shortApdu = commandFactory.createShortApdu(apdu); - lastResponse = transport.transceive(shortApdu); - } else if (cardCapabilities.hasChaining()) { - List chainedApdus = commandFactory.createChainedApdus(apdu); - for (int i = 0, totalCommands = chainedApdus.size(); i < totalCommands; i++) { - CommandApdu chainedApdu = chainedApdus.get(i); - lastResponse = transport.transceive(chainedApdu); - - boolean isLastCommand = (i == totalCommands - 1); - if (!isLastCommand && !lastResponse.isSuccess()) { - throw new IOException("Failed to chain apdu " + - "(" + i + "/" + (totalCommands-1) + ", last SW: " + lastResponse.getSw() + ")"); - } - } - } - if (lastResponse == null) { - throw new IOException("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 = commandFactory.createGetResponseCommand(lastResponse.getSw2()); - lastResponse = transport.transceive(getResponse); - result.write(lastResponse.getData()); - } - - result.write(lastResponse.getSw1()); - result.write(lastResponse.getSw2()); - - lastResponse = ResponseApdu.fromBytes(result.toByteArray()); - - if ((secureMessaging != null) && secureMessaging.isEstablished()) { - try { - lastResponse = secureMessaging.verifyAndDecrypt(lastResponse); - } catch (SecureMessagingException e) { - clearSecureMessaging(); - throw new IOException("secure messaging verify/decrypt failure : " + e.getMessage()); - } - } - - return lastResponse; + public String getUrl() throws IOException { + byte[] data = getData(0x5F, 0x50); + return new String(data).trim(); } - /** - * Return the fingerprint from application specific data stored on tag, or - * null if it doesn't exist. - * - * @param keyType key type - * @return The fingerprint of the requested key, or null if not found. - */ - public byte[] getKeyFingerprint(@NonNull KeyType keyType) throws IOException { - byte[] data = getFingerprints(); - if (data == null) { - return null; + public byte[] getUserId() throws IOException { + return getData(0x00, 0x65); + } + + public SecurityTokenInfo getTokenInfo() throws IOException { + byte[] rawFingerprints = openPgpCapabilities.getFingerprints(); + + byte[][] fingerprints = new byte[rawFingerprints.length / 20][]; + ByteBuffer buf = ByteBuffer.wrap(rawFingerprints); + for (int i = 0; i < rawFingerprints.length / 20; i++) { + fingerprints[i] = new byte[20]; + buf.get(fingerprints[i]); } - // return the master key fingerprint - ByteBuffer fpbuf = ByteBuffer.wrap(data); - byte[] fp = new byte[20]; - fpbuf.position(keyType.getIdx() * 20); - fpbuf.get(fp, 0, 20); + byte[] aid = getAid(); + String userId = parseHolderName(getUserId()); + String url = getUrl(); + byte[] pwInfo = getPwStatusBytes(); + boolean hasLifeCycleManagement = cardCapabilities.hasLifeCycleManagement(); - return fp; + TransportType transportType = transport.getTransportType(); + + return SecurityTokenInfo + .create(transportType, tokenType, fingerprints, aid, userId, url, pwInfo[4], pwInfo[6], + hasLifeCycleManagement); } + public boolean isPersistentConnectionAllowed() { return transport.isPersistentConnectionAllowed() && (secureMessaging == null || !secureMessaging.isEstablished()); @@ -399,38 +415,48 @@ public class SecurityTokenConnection { return tokenType; } - public void clearSecureMessaging() { - if (secureMessaging != null) { - secureMessaging.clearSession(); - } - secureMessaging = null; + OpenPgpCapabilities getOpenPgpCapabilities() { + return openPgpCapabilities; } - void setSecureMessaging(SecureMessaging sm) { - clearSecureMessaging(); - secureMessaging = sm; + OpenPgpCommandApduFactory getCommandFactory() { + return commandFactory; } - public SecurityTokenInfo getTokenInfo() throws IOException { - byte[] rawFingerprints = getFingerprints(); + byte[] getPwStatusBytes() { + return openPgpCapabilities.getPwStatusBytes(); + } - byte[][] fingerprints = new byte[rawFingerprints.length / 20][]; - ByteBuffer buf = ByteBuffer.wrap(rawFingerprints); - for (int i = 0; i < rawFingerprints.length / 20; i++) { - fingerprints[i] = new byte[20]; - buf.get(fingerprints[i]); + public byte[] getAid() { + return openPgpCapabilities.getAid(); + } + + public byte[] getKeyFingerprint(@NonNull KeyType keyType) { + byte[] data = openPgpCapabilities.getFingerprints(); + if (data == null) { + return null; } - byte[] aid = getAid(); - String userId = getUserId(); - String url = getUrl(); - byte[] pwInfo = getPwStatusBytes(); - boolean hasLifeCycleManagement = cardCapabilities.hasLifeCycleManagement(); + // return the master key fingerprint + ByteBuffer fpbuf = ByteBuffer.wrap(data); + byte[] fp = new byte[20]; + fpbuf.position(keyType.getIdx() * 20); + fpbuf.get(fp, 0, 20); - TransportType transportType = transport.getTransportType(); + return fp; + } - return SecurityTokenInfo - .create(transportType, tokenType, fingerprints, aid, userId, url, pwInfo[4], pwInfo[6], - hasLifeCycleManagement); + + private static String parseHolderName(byte[] name) { + try { + 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 + // https://github.com/FluffyKaon/OpenPGP-Card, thus return an empty string for now! + + Log.e(Constants.TAG, "Couldn't get holder name, returning empty string!", e); + return ""; + } } }