Further refactor SecurityTokenConnection

This commit is contained in:
Vincent Breitmoser
2018-01-12 16:49:27 +01:00
parent 626c08bbbe
commit c00eb7b7f3
2 changed files with 199 additions and 170 deletions

View File

@@ -55,6 +55,7 @@ import java.security.spec.InvalidParameterSpecException;
import java.util.ArrayList; import java.util.ArrayList;
import android.content.Context; import android.content.Context;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import javax.crypto.BadPaddingException; 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 { throws SecureMessagingException, IOException {
CommandApdu cmd; CommandApdu cmd;
@@ -478,7 +481,7 @@ class SCP11bSecureMessaging implements SecureMessaging {
final SCP11bSecureMessaging sm = new SCP11bSecureMessaging(); final SCP11bSecureMessaging sm = new SCP11bSecureMessaging();
sm.setKeys(sEnc, sMac, sRmac, receipt); sm.setKeys(sEnc, sMac, sRmac, receipt);
t.setSecureMessaging(sm); return sm;
} catch (InvalidKeySpecException e) { } catch (InvalidKeySpecException e) {
throw new SecureMessagingException("invalid key specification : " + e.getMessage()); throw new SecureMessagingException("invalid key specification : " + e.getMessage());

View File

@@ -88,26 +88,7 @@ public class SecurityTokenConnection {
this.commandFactory = commandFactory; this.commandFactory = commandFactory;
} }
OpenPgpCapabilities getOpenPgpCapabilities() { // region connection management
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 "";
}
}
public void connectIfNecessary(Context context) throws IOException { public void connectIfNecessary(Context context) throws IOException {
if (isConnected()) { if (isConnected()) {
@@ -143,14 +124,7 @@ public class SecurityTokenConnection {
isPw1ValidatedForOther = false; isPw1ValidatedForOther = false;
isPw3Validated = false; isPw3Validated = false;
if (openPgpCapabilities.isHasSCP11bSM()) { smEstablishIfAvailable(context);
try {
SCP11bSecureMessaging.establish(this, context, commandFactory);
} catch (SecureMessagingException e) {
secureMessaging = null;
Log.e(Constants.TAG, "failed to establish secure messaging", e);
}
}
} }
@VisibleForTesting @VisibleForTesting
@@ -191,6 +165,137 @@ public class SecurityTokenConnection {
this.cardCapabilities = new CardCapabilities(openPgpCapabilities.getHistoricalBytes()); 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<CommandApdu> 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 { void verifyPinForSignature() throws IOException {
if (isPw1ValidatedForSignature) { if (isPw1ValidatedForSignature) {
return; return;
@@ -253,37 +358,7 @@ public class SecurityTokenConnection {
isPw3Validated = false; isPw3Validated = false;
} }
/** // endregion
* 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));
}
private byte[] getData(int p1, int p2) throws IOException { private byte[] getData(int p1, int p2) throws IOException {
ResponseApdu response = communicate(commandFactory.createGetDataCommand(p1, p2)); ResponseApdu response = communicate(commandFactory.createGetDataCommand(p1, p2));
@@ -293,99 +368,40 @@ public class SecurityTokenConnection {
return response.getData(); 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; public String getUrl() throws IOException {
// Transmit byte[] data = getData(0x5F, 0x50);
if (cardCapabilities.hasExtended()) { return new String(data).trim();
lastResponse = transport.transceive(apdu);
} else if (commandFactory.isSuitableForShortApdu(apdu)) {
CommandApdu shortApdu = commandFactory.createShortApdu(apdu);
lastResponse = transport.transceive(shortApdu);
} else if (cardCapabilities.hasChaining()) {
List<CommandApdu> 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 byte[] getUserId() throws IOException {
* Return the fingerprint from application specific data stored on tag, or return getData(0x00, 0x65);
* null if it doesn't exist. }
*
* @param keyType key type public SecurityTokenInfo getTokenInfo() throws IOException {
* @return The fingerprint of the requested key, or null if not found. byte[] rawFingerprints = openPgpCapabilities.getFingerprints();
*/
public byte[] getKeyFingerprint(@NonNull KeyType keyType) throws IOException { byte[][] fingerprints = new byte[rawFingerprints.length / 20][];
byte[] data = getFingerprints(); ByteBuffer buf = ByteBuffer.wrap(rawFingerprints);
if (data == null) { for (int i = 0; i < rawFingerprints.length / 20; i++) {
return null; fingerprints[i] = new byte[20];
buf.get(fingerprints[i]);
} }
// return the master key fingerprint byte[] aid = getAid();
ByteBuffer fpbuf = ByteBuffer.wrap(data); String userId = parseHolderName(getUserId());
byte[] fp = new byte[20]; String url = getUrl();
fpbuf.position(keyType.getIdx() * 20); byte[] pwInfo = getPwStatusBytes();
fpbuf.get(fp, 0, 20); 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() { public boolean isPersistentConnectionAllowed() {
return transport.isPersistentConnectionAllowed() && return transport.isPersistentConnectionAllowed() &&
(secureMessaging == null || !secureMessaging.isEstablished()); (secureMessaging == null || !secureMessaging.isEstablished());
@@ -399,38 +415,48 @@ public class SecurityTokenConnection {
return tokenType; return tokenType;
} }
public void clearSecureMessaging() { OpenPgpCapabilities getOpenPgpCapabilities() {
if (secureMessaging != null) { return openPgpCapabilities;
secureMessaging.clearSession();
}
secureMessaging = null;
} }
void setSecureMessaging(SecureMessaging sm) { OpenPgpCommandApduFactory getCommandFactory() {
clearSecureMessaging(); return commandFactory;
secureMessaging = sm;
} }
public SecurityTokenInfo getTokenInfo() throws IOException { byte[] getPwStatusBytes() {
byte[] rawFingerprints = getFingerprints(); return openPgpCapabilities.getPwStatusBytes();
}
byte[][] fingerprints = new byte[rawFingerprints.length / 20][]; public byte[] getAid() {
ByteBuffer buf = ByteBuffer.wrap(rawFingerprints); return openPgpCapabilities.getAid();
for (int i = 0; i < rawFingerprints.length / 20; i++) { }
fingerprints[i] = new byte[20];
buf.get(fingerprints[i]); public byte[] getKeyFingerprint(@NonNull KeyType keyType) {
byte[] data = openPgpCapabilities.getFingerprints();
if (data == null) {
return null;
} }
byte[] aid = getAid(); // return the master key fingerprint
String userId = getUserId(); ByteBuffer fpbuf = ByteBuffer.wrap(data);
String url = getUrl(); byte[] fp = new byte[20];
byte[] pwInfo = getPwStatusBytes(); fpbuf.position(keyType.getIdx() * 20);
boolean hasLifeCycleManagement = cardCapabilities.hasLifeCycleManagement(); fpbuf.get(fp, 0, 20);
TransportType transportType = transport.getTransportType(); return fp;
}
return SecurityTokenInfo
.create(transportType, tokenType, fingerprints, aid, userId, url, pwInfo[4], pwInfo[6], private static String parseHolderName(byte[] name) {
hasLifeCycleManagement); 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 "";
}
} }
} }