From 1ed2cae2b03c2caa114674abf341fc646287b236 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 02:02:29 +0100 Subject: [PATCH 01/15] Extract OpenPGP card operation logic from SecurityTokenConnection class --- .../securitytoken/PsoDecryptUseCase.java | 165 ++++++ .../SecurityTokenChangeKeyUseCase.java | 177 ++++++ .../SecurityTokenConnection.java | 522 ++---------------- .../SecurityTokenPsoSignUseCase.java | 180 ++++++ .../ui/SecurityTokenOperationActivity.java | 15 +- .../SecurityTokenConnectionTest.java | 3 +- 6 files changed, 569 insertions(+), 493 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCase.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java new file mode 100644 index 000000000..1bc5eb600 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java @@ -0,0 +1,165 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import android.support.annotation.NonNull; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; +import org.bouncycastle.asn1.nist.NISTNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.jcajce.util.MessageDigestUtils; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.operator.PGPPad; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; + + +public class PsoDecryptUseCase { + private final SecurityTokenConnection connection; + private final JcaKeyFingerprintCalculator fingerprintCalculator; + + public static PsoDecryptUseCase create(SecurityTokenConnection connection) { + return new PsoDecryptUseCase(connection); + } + + private PsoDecryptUseCase(SecurityTokenConnection connection) { + this.connection = connection; + this.fingerprintCalculator = new JcaKeyFingerprintCalculator(); + } + + public byte[] decryptSessionKey(@NonNull byte[] encryptedSessionKey, + CanonicalizedPublicKey publicKey) + throws IOException { + final KeyFormat kf = connection.getOpenPgpCapabilities().getFormatForKeyType(KeyType.ENCRYPT); + + connection.verifyPinForOther(); + + byte[] data; + byte[] dataLen; + int pLen = 0; + + X9ECParameters x9Params; + + switch (kf.keyFormatType()) { + case RSAKeyFormatType: + data = Arrays.copyOfRange(encryptedSessionKey, 2, encryptedSessionKey.length); + if (data[0] != 0) { + data = Arrays.prepend(data, (byte) 0x00); + } + break; + + case ECKeyFormatType: + pLen = ((((encryptedSessionKey[0] & 0xff) << 8) + (encryptedSessionKey[1] & 0xff)) + 7) / 8; + data = new byte[pLen]; + + System.arraycopy(encryptedSessionKey, 2, data, 0, pLen); + + final ECKeyFormat eckf = (ECKeyFormat) kf; + x9Params = NISTNamedCurves.getByOID(eckf.getCurveOID()); + + final ECPoint p = x9Params.getCurve().decodePoint(data); + if (!p.isValid()) { + throw new CardException("Invalid EC point!"); + } + + data = p.getEncoded(false); + + if (data.length < 128) { + dataLen = new byte[]{(byte) data.length}; + } else { + dataLen = new byte[]{(byte) 0x81, (byte) data.length}; + } + data = Arrays.concatenate(Hex.decode("86"), dataLen, data); + + if (data.length < 128) { + dataLen = new byte[]{(byte) data.length}; + } else { + dataLen = new byte[]{(byte) 0x81, (byte) data.length}; + } + data = Arrays.concatenate(Hex.decode("7F49"), dataLen, data); + + if (data.length < 128) { + dataLen = new byte[]{(byte) data.length}; + } else { + dataLen = new byte[]{(byte) 0x81, (byte) data.length}; + } + data = Arrays.concatenate(Hex.decode("A6"), dataLen, data); + break; + + default: + throw new CardException("Unknown encryption key type!"); + } + + CommandApdu command = connection.getCommandFactory().createDecipherCommand(data); + ResponseApdu response = connection.communicate(command); + + if (!response.isSuccess()) { + throw new CardException("Deciphering with Security token failed on receive", response.getSw()); + } + + switch (connection.getOpenPgpCapabilities().getFormatForKeyType(KeyType.ENCRYPT).keyFormatType()) { + case RSAKeyFormatType: + return response.getData(); + + /* From 3.x OpenPGP card specification : + In case of ECDH the card supports a partial decrypt only. + With its own private key and the given public key the card calculates a shared secret + in compliance with the Elliptic Curve Key Agreement Scheme from Diffie-Hellman. + The shared secret is returned in the response, all other calculation for deciphering + are done outside of the card. + + The shared secret obtained is a KEK (Key Encryption Key) that is used to wrap the + session key. + + From rfc6637#section-13 : + This document explicitly discourages the use of algorithms other than AES as a KEK algorithm. + */ + case ECKeyFormatType: + data = response.getData(); + + final byte[] keyEnc = new byte[encryptedSessionKey[pLen + 2]]; + + System.arraycopy(encryptedSessionKey, 2 + pLen + 1, keyEnc, 0, keyEnc.length); + + try { + final MessageDigest kdf = MessageDigest.getInstance(MessageDigestUtils.getDigestName(publicKey.getSecurityTokenHashAlgorithm())); + + kdf.update(new byte[]{(byte) 0, (byte) 0, (byte) 0, (byte) 1}); + kdf.update(data); + kdf.update(publicKey.createUserKeyingMaterial(fingerprintCalculator)); + + final byte[] kek = kdf.digest(); + final Cipher c = Cipher.getInstance("AESWrap"); + + c.init(Cipher.UNWRAP_MODE, new SecretKeySpec(kek, 0, publicKey.getSecurityTokenSymmetricKeySize() / 8, "AES")); + + final Key paddedSessionKey = c.unwrap(keyEnc, "Session", Cipher.SECRET_KEY); + + Arrays.fill(kek, (byte) 0); + + return PGPPad.unpadSessionData(paddedSessionKey.getEncoded()); + } catch (NoSuchAlgorithmException e) { + throw new CardException("Unknown digest/encryption algorithm!"); + } catch (NoSuchPaddingException e) { + throw new CardException("Unknown padding algorithm!"); + } catch (PGPException e) { + throw new CardException(e.getMessage()); + } catch (InvalidKeyException e) { + throw new CardException("Invalid KEK!"); + } + + default: + throw new CardException("Unknown encryption key type!"); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCase.java new file mode 100644 index 000000000..d9866188a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCase.java @@ -0,0 +1,177 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateCrtKey; + +import android.support.annotation.VisibleForTesting; + +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.util.Passphrase; + + +public class SecurityTokenChangeKeyUseCase { + 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 final SecurityTokenConnection connection; + + public static SecurityTokenChangeKeyUseCase create(SecurityTokenConnection stConnection) { + return new SecurityTokenChangeKeyUseCase(stConnection); + } + + private SecurityTokenChangeKeyUseCase(SecurityTokenConnection connection) { + this.connection = connection; + } + + public void changeKey(CanonicalizedSecretKey secretKey, Passphrase passphrase, Passphrase adminPin) throws IOException { + long keyGenerationTimestamp = secretKey.getCreationTime().getTime() / 1000; + byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array(); + KeyType keyType = KeyType.from(secretKey); + + if (keyType == null) { + throw new IOException("Inappropriate key flags for smart card key."); + } + + // Slot is empty, or contains this key already. PUT KEY operation is safe + boolean canPutKey = isSlotEmpty(keyType) + || keyMatchesFingerPrint(keyType, secretKey.getFingerprint()); + + if (!canPutKey) { + throw new IOException(String.format("Key slot occupied; card must be reset to put new %s key.", + keyType.toString())); + } + + putKey(keyType, secretKey, passphrase, adminPin); + putData(adminPin, keyType.getFingerprintObjectId(), secretKey.getFingerprint()); + putData(adminPin, keyType.getTimestampObjectId(), timestampBytes); + } + + /** + * Puts a key on the token in the given slot. + * + * @param slot The slot on the token where the key should be stored: + * 0xB6: Signature Key + * 0xB8: Decipherment Key + * 0xA4: Authentication Key + */ + @VisibleForTesting + void putKey(KeyType slot, CanonicalizedSecretKey secretKey, Passphrase passphrase, Passphrase adminPin) + throws IOException { + RSAPrivateCrtKey crtSecretKey; + ECPrivateKey ecSecretKey; + ECPublicKey ecPublicKey; + + connection.verifyAdminPin(adminPin); + + // Now we're ready to communicate with the token. + byte[] keyBytes; + + try { + secretKey.unlock(passphrase); + + OpenPgpCapabilities openPgpCapabilities = connection.getOpenPgpCapabilities(); + + setKeyAttributes(adminPin, slot, SecurityTokenUtils.attributesFromSecretKey(slot, secretKey, + openPgpCapabilities.getFormatForKeyType(slot))); + + KeyFormat formatForKeyType = openPgpCapabilities.getFormatForKeyType(slot); + switch (formatForKeyType.keyFormatType()) { + case RSAKeyFormatType: + if (!secretKey.isRSA()) { + throw new IOException("Security Token not configured for RSA key."); + } + crtSecretKey = secretKey.getSecurityTokenRSASecretKey(); + + // Should happen only rarely; all GnuPG keys since 2006 use public exponent 65537. + if (!crtSecretKey.getPublicExponent().equals(new BigInteger("65537"))) { + throw new IOException("Invalid public exponent for smart Security Token."); + } + + keyBytes = SecurityTokenUtils.createRSAPrivKeyTemplate(crtSecretKey, slot, + (RSAKeyFormat) formatForKeyType); + break; + + case ECKeyFormatType: + if (!secretKey.isEC()) { + throw new IOException("Security Token not configured for EC key."); + } + + secretKey.unlock(passphrase); + ecSecretKey = secretKey.getSecurityTokenECSecretKey(); + ecPublicKey = secretKey.getSecurityTokenECPublicKey(); + + keyBytes = SecurityTokenUtils.createECPrivKeyTemplate(ecSecretKey, ecPublicKey, slot, + (ECKeyFormat) formatForKeyType); + break; + + default: + throw new IOException("Key type unsupported by security token."); + } + } catch (PgpGeneralException e) { + throw new IOException(e.getMessage()); + } + + CommandApdu apdu = connection.getCommandFactory().createPutKeyCommand(keyBytes); + ResponseApdu response = connection.communicate(apdu); + + if (!response.isSuccess()) { + throw new CardException("Key export to Security Token failed", response.getSw()); + } + } + + private void setKeyAttributes(Passphrase adminPin, KeyType keyType, byte[] data) throws IOException { + if (!connection.getOpenPgpCapabilities().isAttributesChangable()) { + return; + } + + putData(adminPin, keyType.getAlgoAttributeSlot(), data); + + connection.refreshConnectionCapabilities(); + } + + /** + * Stores a data object on the token. Automatically validates the proper PIN for the operation. + * Supported for all data objects < 255 bytes in length. Only the cardholder certificate + * (0x7F21) can exceed this length. + * + * @param dataObject The data object to be stored. + * @param data The data to store in the object + */ + private void putData(Passphrase adminPin, int dataObject, byte[] data) throws IOException { + if (data.length > 254) { + throw new IOException("Cannot PUT DATA with length > 254"); + } + // TODO use admin pin regardless, if we have it? + if (dataObject == 0x0101 || dataObject == 0x0103) { + connection.verifyPinForOther(); + } else { + connection.verifyAdminPin(adminPin); + } + + CommandApdu command = connection.getCommandFactory().createPutDataCommand(dataObject, data); + ResponseApdu response = connection.communicate(command); + + if (!response.isSuccess()) { + throw new CardException("Failed to put data.", response.getSw()); + } + } + + private boolean isSlotEmpty(KeyType keyType) throws IOException { + // Note: special case: This should not happen, but happens with + // https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true + if (connection.getKeyFingerprint(keyType) == null) { + return true; + } + + return keyMatchesFingerPrint(keyType, BLANK_FINGERPRINT); + } + + private boolean keyMatchesFingerPrint(KeyType keyType, byte[] fingerprint) throws IOException { + return java.util.Arrays.equals(connection.getKeyFingerprint(keyType), fingerprint); + } +} 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 02f659ab0..92c44db65 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -17,52 +17,23 @@ package org.sufficientlysecure.keychain.securitytoken; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; -import org.bouncycastle.asn1.ASN1Encodable; -import org.bouncycastle.asn1.ASN1Integer; -import org.bouncycastle.asn1.ASN1OutputStream; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.nist.NISTNamedCurves; -import org.bouncycastle.asn1.x9.X9ECParameters; -import org.bouncycastle.bcpg.HashAlgorithmTags; -import org.bouncycastle.jcajce.util.MessageDigestUtils; -import org.bouncycastle.math.ec.ECPoint; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.operator.PGPPad; -import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; -import org.bouncycastle.util.Arrays; -import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; -import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; -import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; - -import javax.crypto.Cipher; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.SecretKeySpec; - import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.MessageDigest; -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 @@ -74,12 +45,8 @@ public class SecurityTokenConnection { private static final String AID_PREFIX_FIDESMO = "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 static SecurityTokenConnection sCachedInstance; - private final JcaKeyFingerprintCalculator fingerprintCalculator = new JcaKeyFingerprintCalculator(); - @NonNull private final Transport mTransport; @Nullable @@ -120,6 +87,20 @@ public class SecurityTokenConnection { this.commandFactory = commandFactory; } + OpenPgpCapabilities getOpenPgpCapabilities() { + return mOpenPgpCapabilities; + } + + OpenPgpCommandApduFactory getCommandFactory() { + return commandFactory; + } + + void maybeInvalidatePw1() { + if (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) { + mPw1ValidatedForSignature = false; + } + } + private String getHolderName(byte[] name) { try { return (new String(name, 4, name[3])).replace('<', ' '); @@ -133,43 +114,6 @@ public class SecurityTokenConnection { } } - public void changeKey(CanonicalizedSecretKey secretKey, Passphrase passphrase, Passphrase adminPin) throws IOException { - long keyGenerationTimestamp = secretKey.getCreationTime().getTime() / 1000; - byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array(); - KeyType keyType = KeyType.from(secretKey); - - if (keyType == null) { - throw new IOException("Inappropriate key flags for smart card key."); - } - - // Slot is empty, or contains this key already. PUT KEY operation is safe - boolean canPutKey = isSlotEmpty(keyType) - || keyMatchesFingerPrint(keyType, secretKey.getFingerprint()); - - if (!canPutKey) { - throw new IOException(String.format("Key slot occupied; card must be reset to put new %s key.", - keyType.toString())); - } - - putKey(keyType, secretKey, passphrase, adminPin); - putData(adminPin, keyType.getFingerprintObjectId(), secretKey.getFingerprint()); - putData(adminPin, keyType.getTimestampObjectId(), timestampBytes); - } - - private boolean isSlotEmpty(KeyType keyType) throws IOException { - // Note: special case: This should not happen, but happens with - // https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true - if (getKeyFingerprint(keyType) == null) { - return true; - } - - return keyMatchesFingerPrint(keyType, BLANK_FINGERPRINT); - } - - private boolean keyMatchesFingerPrint(KeyType keyType, byte[] fingerprint) throws IOException { - return java.util.Arrays.equals(getKeyFingerprint(keyType), fingerprint); - } - public void connectIfNecessary(Context context) throws IOException { if (isConnected()) { return; @@ -239,7 +183,7 @@ public class SecurityTokenConnection { tokenType = TokenType.UNKNOWN; } - private void refreshConnectionCapabilities() throws IOException { + void refreshConnectionCapabilities() throws IOException { byte[] rawOpenPgpCapabilities = getData(0x00, 0x6E); OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities(rawOpenPgpCapabilities); @@ -299,145 +243,14 @@ public class SecurityTokenConnection { } } - /** - * Call DECIPHER command - * - * @param encryptedSessionKey the encoded session key - * @param publicKey - * @return the decoded session key - */ - public byte[] decryptSessionKey(@NonNull byte[] encryptedSessionKey, - CanonicalizedPublicKey publicKey) - throws IOException { - final KeyFormat kf = mOpenPgpCapabilities.getFormatForKeyType(KeyType.ENCRYPT); - - if (!mPw1ValidatedForDecrypt) { - verifyPinForOther(); - } - - byte[] data; - byte[] dataLen; - int pLen = 0; - - X9ECParameters x9Params; - - switch (kf.keyFormatType()) { - case RSAKeyFormatType: - data = Arrays.copyOfRange(encryptedSessionKey, 2, encryptedSessionKey.length); - if (data[0] != 0) { - data = Arrays.prepend(data, (byte) 0x00); - } - break; - - case ECKeyFormatType: - pLen = ((((encryptedSessionKey[0] & 0xff) << 8) + (encryptedSessionKey[1] & 0xff)) + 7) / 8; - data = new byte[pLen]; - - System.arraycopy(encryptedSessionKey, 2, data, 0, pLen); - - final ECKeyFormat eckf = (ECKeyFormat) kf; - x9Params = NISTNamedCurves.getByOID(eckf.getCurveOID()); - - final ECPoint p = x9Params.getCurve().decodePoint(data); - if (!p.isValid()) { - throw new CardException("Invalid EC point!"); - } - - data = p.getEncoded(false); - - if (data.length < 128) { - dataLen = new byte[]{(byte) data.length}; - } else { - dataLen = new byte[]{(byte) 0x81, (byte) data.length}; - } - data = Arrays.concatenate(Hex.decode("86"), dataLen, data); - - if (data.length < 128) { - dataLen = new byte[]{(byte) data.length}; - } else { - dataLen = new byte[]{(byte) 0x81, (byte) data.length}; - } - data = Arrays.concatenate(Hex.decode("7F49"), dataLen, data); - - if (data.length < 128) { - dataLen = new byte[]{(byte) data.length}; - } else { - dataLen = new byte[]{(byte) 0x81, (byte) data.length}; - } - data = Arrays.concatenate(Hex.decode("A6"), dataLen, data); - break; - - default: - throw new CardException("Unknown encryption key type!"); - } - - CommandApdu command = commandFactory.createDecipherCommand(data); - ResponseApdu response = communicate(command); - - if (!response.isSuccess()) { - throw new CardException("Deciphering with Security token failed on receive", response.getSw()); - } - - switch (mOpenPgpCapabilities.getFormatForKeyType(KeyType.ENCRYPT).keyFormatType()) { - case RSAKeyFormatType: - return response.getData(); - - /* From 3.x OpenPGP card specification : - In case of ECDH the card supports a partial decrypt only. - With its own private key and the given public key the card calculates a shared secret - in compliance with the Elliptic Curve Key Agreement Scheme from Diffie-Hellman. - The shared secret is returned in the response, all other calculation for deciphering - are done outside of the card. - - The shared secret obtained is a KEK (Key Encryption Key) that is used to wrap the - session key. - - From rfc6637#section-13 : - This document explicitly discourages the use of algorithms other than AES as a KEK algorithm. - */ - case ECKeyFormatType: - data = response.getData(); - - final byte[] keyEnc = new byte[encryptedSessionKey[pLen + 2]]; - - System.arraycopy(encryptedSessionKey, 2 + pLen + 1, keyEnc, 0, keyEnc.length); - - try { - final MessageDigest kdf = MessageDigest.getInstance(MessageDigestUtils.getDigestName(publicKey.getSecurityTokenHashAlgorithm())); - - kdf.update(new byte[]{(byte) 0, (byte) 0, (byte) 0, (byte) 1}); - kdf.update(data); - kdf.update(publicKey.createUserKeyingMaterial(fingerprintCalculator)); - - final byte[] kek = kdf.digest(); - final Cipher c = Cipher.getInstance("AESWrap"); - - c.init(Cipher.UNWRAP_MODE, new SecretKeySpec(kek, 0, publicKey.getSecurityTokenSymmetricKeySize() / 8, "AES")); - - final Key paddedSessionKey = c.unwrap(keyEnc, "Session", Cipher.SECRET_KEY); - - Arrays.fill(kek, (byte) 0); - - return PGPPad.unpadSessionData(paddedSessionKey.getEncoded()); - } catch (NoSuchAlgorithmException e) { - throw new CardException("Unknown digest/encryption algorithm!"); - } catch (NoSuchPaddingException e) { - throw new CardException("Unknown padding algorithm!"); - } catch (PGPException e) { - throw new CardException(e.getMessage()); - } catch (InvalidKeyException e) { - throw new CardException("Invalid KEK!"); - } - - default: - throw new CardException("Unknown encryption key type!"); - } - } - /** * Verifies the user's PW1 with the appropriate mode. */ - private void verifyPinForSignature() throws IOException { + void verifyPinForSignature() throws IOException { + if (mPw1ValidatedForSignature) { + return; + } + if (mPin == null) { throw new IllegalStateException("Connection not initialized with Pin!"); } @@ -454,7 +267,10 @@ public class SecurityTokenConnection { /** * Verifies the user's PW1 with the appropriate mode. */ - private void verifyPinForOther() throws IOException { + void verifyPinForOther() throws IOException { + if (mPw1ValidatedForDecrypt) { + return; + } if (mPin == null) { throw new IllegalStateException("Connection not initialized with Pin!"); } @@ -473,7 +289,10 @@ public class SecurityTokenConnection { /** * Verifies the user's PW1 or PW3 with the appropriate mode. */ - private void verifyAdminPin(Passphrase adminPin) throws IOException { + void verifyAdminPin(Passphrase adminPin) throws IOException { + if (mPw3Validated) { + return; + } // Command APDU for VERIFY command (page 32) ResponseApdu response = communicate(commandFactory.createVerifyPw3Command(adminPin.toStringUnsafe().getBytes())); @@ -484,117 +303,6 @@ public class SecurityTokenConnection { mPw3Validated = true; } - /** - * Stores a data object on the token. Automatically validates the proper PIN for the operation. - * Supported for all data objects < 255 bytes in length. Only the cardholder certificate - * (0x7F21) can exceed this length. - * - * @param dataObject The data object to be stored. - * @param data The data to store in the object - */ - private void putData(Passphrase adminPin, int dataObject, byte[] data) throws IOException { - if (data.length > 254) { - throw new IOException("Cannot PUT DATA with length > 254"); - } - // TODO use admin pin regardless, if we have it? - if (dataObject == 0x0101 || dataObject == 0x0103) { - if (!mPw1ValidatedForDecrypt) { - verifyPinForOther(); - } - } else if (!mPw3Validated) { - verifyAdminPin(adminPin); - } - - CommandApdu command = commandFactory.createPutDataCommand(dataObject, data); - ResponseApdu response = communicate(command); // put data - - if (!response.isSuccess()) { - throw new CardException("Failed to put data.", response.getSw()); - } - } - - private void setKeyAttributes(Passphrase adminPin, KeyType keyType, byte[] data) throws IOException { - if (!mOpenPgpCapabilities.isAttributesChangable()) { - return; - } - - putData(adminPin, keyType.getAlgoAttributeSlot(), data); - refreshConnectionCapabilities(); - } - - /** - * Puts a key on the token in the given slot. - * - * @param slot The slot on the token where the key should be stored: - * 0xB6: Signature Key - * 0xB8: Decipherment Key - * 0xA4: Authentication Key - */ - @VisibleForTesting - void putKey(KeyType slot, CanonicalizedSecretKey secretKey, Passphrase passphrase, Passphrase adminPin) - throws IOException { - RSAPrivateCrtKey crtSecretKey; - ECPrivateKey ecSecretKey; - ECPublicKey ecPublicKey; - - if (!mPw3Validated) { - verifyAdminPin(adminPin); - } - - // Now we're ready to communicate with the token. - byte[] keyBytes; - - try { - secretKey.unlock(passphrase); - - setKeyAttributes(adminPin, slot, SecurityTokenUtils.attributesFromSecretKey(slot, secretKey, - mOpenPgpCapabilities.getFormatForKeyType(slot))); - - KeyFormat formatForKeyType = mOpenPgpCapabilities.getFormatForKeyType(slot); - switch (formatForKeyType.keyFormatType()) { - case RSAKeyFormatType: - if (!secretKey.isRSA()) { - throw new IOException("Security Token not configured for RSA key."); - } - crtSecretKey = secretKey.getSecurityTokenRSASecretKey(); - - // Should happen only rarely; all GnuPG keys since 2006 use public exponent 65537. - if (!crtSecretKey.getPublicExponent().equals(new BigInteger("65537"))) { - throw new IOException("Invalid public exponent for smart Security Token."); - } - - keyBytes = SecurityTokenUtils.createRSAPrivKeyTemplate(crtSecretKey, slot, - (RSAKeyFormat) formatForKeyType); - break; - - case ECKeyFormatType: - if (!secretKey.isEC()) { - throw new IOException("Security Token not configured for EC key."); - } - - secretKey.unlock(passphrase); - ecSecretKey = secretKey.getSecurityTokenECSecretKey(); - ecPublicKey = secretKey.getSecurityTokenECPublicKey(); - - keyBytes = SecurityTokenUtils.createECPrivKeyTemplate(ecSecretKey, ecPublicKey, slot, - (ECKeyFormat) formatForKeyType); - break; - - default: - throw new IOException("Key type unsupported by security token."); - } - } catch (PgpGeneralException e) { - throw new IOException(e.getMessage()); - } - - CommandApdu apdu = commandFactory.createPutKeyCommand(keyBytes); - ResponseApdu response = communicate(apdu); - - if (!response.isSuccess()) { - throw new CardException("Key export to Security Token failed", response.getSw()); - } - } - /** * Return fingerprints of all keys from application specific data stored * on tag, or null if data not available. @@ -635,168 +343,6 @@ public class SecurityTokenConnection { return response.getData(); } - - private byte[] prepareDsi(byte[] hash, int hashAlgo) throws IOException { - byte[] dsi; - - Log.i(Constants.TAG, "Hash: " + hashAlgo); - switch (hashAlgo) { - case HashAlgorithmTags.SHA1: - if (hash.length != 20) { - throw new IOException("Bad hash length (" + hash.length + ", expected 10!"); - } - 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"), 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 = 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 = 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 = 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 = 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 = Arrays.concatenate(Hex.decode("3051300D060960864801650304020305000440"), hash); - break; - default: - throw new IOException("Not supported hash algo!"); - } - return dsi; - } - - private byte[] prepareData(byte[] hash, int hashAlgo, KeyFormat keyFormat) throws IOException { - byte[] data; - switch (keyFormat.keyFormatType()) { - case RSAKeyFormatType: - data = prepareDsi(hash, hashAlgo); - break; - case ECKeyFormatType: - data = hash; - break; - default: - throw new IOException("Not supported key type!"); - } - return data; - } - - - private byte[] encodeSignature(byte[] signature, KeyFormat keyFormat) throws IOException { - // Make sure the signature we received is actually the expected number of bytes long! - switch (keyFormat.keyFormatType()) { - case RSAKeyFormatType: - // no encoding necessary - int modulusLength = ((RSAKeyFormat) keyFormat).getModulusLength(); - if (signature.length != (modulusLength / 8)) { - throw new IOException("Bad signature length! Expected " + (modulusLength / 8) + - " bytes, got " + signature.length); - } - break; - - case ECKeyFormatType: - // "plain" encoding, see https://github.com/open-keychain/open-keychain/issues/2108 - if (signature.length % 2 != 0) { - throw new IOException("Bad signature length!"); - } - final byte[] br = new byte[signature.length / 2]; - final byte[] bs = new byte[signature.length / 2]; - for (int i = 0; i < br.length; ++i) { - br[i] = signature[i]; - bs[i] = signature[br.length + i]; - } - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ASN1OutputStream out = new ASN1OutputStream(baos); - out.writeObject(new DERSequence(new ASN1Encodable[]{new ASN1Integer(br), new ASN1Integer(bs)})); - out.flush(); - signature = baos.toByteArray(); - break; - } - return signature; - } - - /** - * Call COMPUTE DIGITAL SIGNATURE command and returns the MPI value - * - * @param hash the hash for signing - * @return a big integer representing the MPI for the given hash - */ - public byte[] calculateSignature(byte[] hash, int hashAlgo) throws IOException { - if (!mPw1ValidatedForSignature) { - verifyPinForSignature(); - } - - KeyFormat signKeyFormat = mOpenPgpCapabilities.getFormatForKeyType(KeyType.SIGN); - - byte[] data = prepareData(hash, hashAlgo, signKeyFormat); - - // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37) - CommandApdu command = commandFactory.createComputeDigitalSignatureCommand(data); - ResponseApdu response = communicate(command); - - if (!response.isSuccess()) { - throw new CardException("Failed to sign", response.getSw()); - } - - if (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) { - mPw1ValidatedForSignature = false; - } - - return encodeSignature(response.getData(), signKeyFormat); - } - - /** - * Call INTERNAL AUTHENTICATE command and returns the MPI value - * - * @param hash the hash for signing - * @return a big integer representing the MPI for the given hash - */ - public byte[] calculateAuthenticationSignature(byte[] hash, int hashAlgo) throws IOException { - if (!mPw1ValidatedForDecrypt) { - verifyPinForOther(); - } - - KeyFormat authKeyFormat = mOpenPgpCapabilities.getFormatForKeyType(KeyType.AUTH); - - byte[] data = prepareData(hash, hashAlgo, authKeyFormat); - - // Command APDU for INTERNAL AUTHENTICATE (page 55) - CommandApdu command = commandFactory.createInternalAuthCommand(data); - ResponseApdu response = communicate(command); - - if (!response.isSuccess()) { - throw new CardException("Failed to sign", response.getSw()); - } - - if (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) { - mPw1ValidatedForSignature = false; - } - - return encodeSignature(response.getData(), authKeyFormat); - } - /** * Transceives APDU * Splits extended APDU into short APDUs and chains them if necessary diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java new file mode 100644 index 000000000..2ce483422 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java @@ -0,0 +1,180 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1OutputStream; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + + +public class SecurityTokenPsoSignUseCase { + private final SecurityTokenConnection connection; + + public static SecurityTokenPsoSignUseCase create(SecurityTokenConnection connection) { + return new SecurityTokenPsoSignUseCase(connection); + } + + private SecurityTokenPsoSignUseCase(SecurityTokenConnection connection) { + this.connection = connection; + } + + private byte[] prepareDsi(byte[] hash, int hashAlgo) throws IOException { + byte[] dsi; + + Log.i(Constants.TAG, "Hash: " + hashAlgo); + switch (hashAlgo) { + case HashAlgorithmTags.SHA1: + if (hash.length != 20) { + throw new IOException("Bad hash length (" + hash.length + ", expected 10!"); + } + 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"), 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 = 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 = 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 = 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 = 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 = Arrays.concatenate(Hex.decode("3051300D060960864801650304020305000440"), hash); + break; + default: + throw new IOException("Not supported hash algo!"); + } + return dsi; + } + + private byte[] prepareData(byte[] hash, int hashAlgo, KeyFormat keyFormat) throws IOException { + byte[] data; + switch (keyFormat.keyFormatType()) { + case RSAKeyFormatType: + data = prepareDsi(hash, hashAlgo); + break; + case ECKeyFormatType: + data = hash; + break; + default: + throw new IOException("Not supported key type!"); + } + return data; + } + + private byte[] encodeSignature(byte[] signature, KeyFormat keyFormat) throws IOException { + // Make sure the signature we received is actually the expected number of bytes long! + switch (keyFormat.keyFormatType()) { + case RSAKeyFormatType: + // no encoding necessary + int modulusLength = ((RSAKeyFormat) keyFormat).getModulusLength(); + if (signature.length != (modulusLength / 8)) { + throw new IOException("Bad signature length! Expected " + (modulusLength / 8) + + " bytes, got " + signature.length); + } + break; + + case ECKeyFormatType: + // "plain" encoding, see https://github.com/open-keychain/open-keychain/issues/2108 + if (signature.length % 2 != 0) { + throw new IOException("Bad signature length!"); + } + final byte[] br = new byte[signature.length / 2]; + final byte[] bs = new byte[signature.length / 2]; + for (int i = 0; i < br.length; ++i) { + br[i] = signature[i]; + bs[i] = signature[br.length + i]; + } + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ASN1OutputStream out = new ASN1OutputStream(baos); + out.writeObject(new DERSequence(new ASN1Encodable[] { new ASN1Integer(br), new ASN1Integer(bs) })); + out.flush(); + signature = baos.toByteArray(); + break; + } + return signature; + } + + /** + * Call COMPUTE DIGITAL SIGNATURE command and returns the MPI value + * + * @param hash the hash for signing + * @return a big integer representing the MPI for the given hash + */ + public byte[] calculateSignature(byte[] hash, int hashAlgo) throws IOException { + connection.verifyPinForSignature(); + + OpenPgpCapabilities openPgpCapabilities = connection.getOpenPgpCapabilities(); + KeyFormat signKeyFormat = openPgpCapabilities.getFormatForKeyType(KeyType.SIGN); + + byte[] data = prepareData(hash, hashAlgo, signKeyFormat); + + // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37) + CommandApdu command = connection.getCommandFactory().createComputeDigitalSignatureCommand(data); + ResponseApdu response = connection.communicate(command); + + connection.maybeInvalidatePw1(); + + if (!response.isSuccess()) { + throw new CardException("Failed to sign", response.getSw()); + } + + return encodeSignature(response.getData(), signKeyFormat); + } + + /** + * Call INTERNAL AUTHENTICATE command and returns the MPI value + * + * @param hash the hash for signing + * @return a big integer representing the MPI for the given hash + */ + public byte[] calculateAuthenticationSignature(byte[] hash, int hashAlgo) throws IOException { + connection.verifyPinForOther(); + + OpenPgpCapabilities openPgpCapabilities = connection.getOpenPgpCapabilities(); + KeyFormat authKeyFormat = openPgpCapabilities.getFormatForKeyType(KeyType.AUTH); + + byte[] data = prepareData(hash, hashAlgo, authKeyFormat); + + // Command APDU for INTERNAL AUTHENTICATE (page 55) + CommandApdu command = connection.getCommandFactory().createInternalAuthCommand(data); + ResponseApdu response = connection.communicate(command); + + if (!response.isSuccess()) { + throw new CardException("Failed to sign", response.getSw()); + } + + return encodeSignature(response.getData(), authKeyFormat); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index e962e8008..04a51296f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -42,6 +42,9 @@ import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.securitytoken.KeyType; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; +import org.sufficientlysecure.keychain.securitytoken.PsoDecryptUseCase; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenPsoSignUseCase; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenChangeKeyUseCase; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; @@ -205,9 +208,10 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { throw new IOException("Couldn't find subkey for key to token operation."); } + PsoDecryptUseCase psoDecryptUseCase = PsoDecryptUseCase.create(stConnection); for (int i = 0; i < mRequiredInput.mInputData.length; i++) { byte[] encryptedSessionKey = mRequiredInput.mInputData[i]; - byte[] decryptedSessionKey = stConnection + byte[] decryptedSessionKey = psoDecryptUseCase .decryptSessionKey(encryptedSessionKey, publicKeyRing.getPublicKey(tokenKeyId)); mInputParcel = mInputParcel.withCryptoData(encryptedSessionKey, decryptedSessionKey); } @@ -223,10 +227,11 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { mInputParcel = mInputParcel.withSignatureTime(mRequiredInput.mSignatureTime); + SecurityTokenPsoSignUseCase psoSignUseCase = SecurityTokenPsoSignUseCase.create(stConnection); for (int i = 0; i < mRequiredInput.mInputData.length; i++) { byte[] hash = mRequiredInput.mInputData[i]; int algo = mRequiredInput.mSignAlgos[i]; - byte[] signedHash = stConnection.calculateSignature(hash, algo); + byte[] signedHash = psoSignUseCase.calculateSignature(hash, algo); mInputParcel = mInputParcel.withCryptoData(hash, signedHash); } break; @@ -239,10 +244,11 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { throw new IOException(getString(R.string.error_wrong_security_token)); } + SecurityTokenPsoSignUseCase psoSignUseCase = SecurityTokenPsoSignUseCase.create(stConnection); for (int i = 0; i < mRequiredInput.mInputData.length; i++) { byte[] hash = mRequiredInput.mInputData[i]; int algo = mRequiredInput.mSignAlgos[i]; - byte[] signedHash = stConnection.calculateAuthenticationSignature(hash, algo); + byte[] signedHash = psoSignUseCase.calculateAuthenticationSignature(hash, algo); mInputParcel = mInputParcel.withCryptoData(hash, signedHash); } @@ -282,7 +288,8 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { throw new IOException("Unable to get cached passphrase!"); } - stConnection.changeKey(key, passphrase, adminPin); + SecurityTokenChangeKeyUseCase putKeyUseCase = SecurityTokenChangeKeyUseCase.create(stConnection); + putKeyUseCase.changeKey(key, passphrase, adminPin); // TODO: Is this really used anywhere? mInputParcel = mInputParcel.withCryptoData(subkeyBytes, tokenSerialNumber); diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java index aa1f95b3a..0524b6904 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java @@ -177,7 +177,8 @@ public class SecurityTokenConnectionTest { "e2702d93e9316b6176726121dd29c8a8b75ec19e1deb09e4cc3b95b054541d", "9000"); - securityTokenConnection.putKey(KeyType.SIGN, signKey, new Passphrase("123456"), new Passphrase("12345678")); + SecurityTokenChangeKeyUseCase changeKeyUseCase = SecurityTokenChangeKeyUseCase.create(securityTokenConnection); + changeKeyUseCase.putKey(KeyType.SIGN, signKey, new Passphrase("123456"), new Passphrase("12345678")); verifyDialog(); From 3cf458274716ba8d3426f064c2f539a4399f27e8 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 02:25:02 +0100 Subject: [PATCH 02/15] unit test for PsoDecryptUseCase --- .../securitytoken/PsoDecryptUseCaseTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java new file mode 100644 index 000000000..b2d250811 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java @@ -0,0 +1,65 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType; +import org.sufficientlysecure.keychain.util.Passphrase; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +@RunWith(KeychainTestRunner.class) +public class PsoDecryptUseCaseTest { + private static final byte[] RSA_ENC_SESSIONKEY_MPI = Hex.decode( + "07ff7b9ff36f70da1fe7a6b59168c24a7e5b48a938c4f970de46524a06ebf4a9175a9737cf2e6f30957110b31db7" + + "0e9a2992401b1d5e99389f976356f4e3a28ff537362e7ce14b81200e21d4f0e77d46bd89f3a54ca06062289148a5938748" + + "8ac01d30d2baf58e6b35e32434720473604a9f7d5083ca6d40e4a2dadedd68033a4d4bbdb06d075d6980c0c0ca19078dcd" + + "fb9d8cbcb34f28d0b968b6e09eda0e1d3ab6b251eb09f9fb9d9abfeaf9010001733b9015e9e4b6c9df61bbc76041f439d1" + + "273e41f5d0e8414a2b8d6d4c7e86f30b94cfba308b38de53d694a8ca15382301ace806c8237641b03525b3e3e8cbb017e2" + + "51265229bcbb0da5d5aeb4eafbad9779"); + private SecurityTokenConnection securityTokenConnection; + private OpenPgpCommandApduFactory commandFactory; + private PsoDecryptUseCase useCase; + + private CommandApdu dummyCommandApdu = mock(CommandApdu.class); + + @Before + public void setUp() throws Exception { + securityTokenConnection = mock(SecurityTokenConnection.class); + + commandFactory = mock(OpenPgpCommandApduFactory.class); + when(securityTokenConnection.getCommandFactory()).thenReturn(commandFactory); + + useCase = PsoDecryptUseCase.create(securityTokenConnection); + } + + @Test + public void testRsaDecrypt() throws Exception { + OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities( + Hex.decode("6e81de4f10d27600012401020000060364311500005f520f0073000080000000000000000000007381b7c00af" + + "00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f03" + + "0303c53c4ec5fee25c4e89654d58cad8492510a89d3c3d8468da7b24e15bfc624c6a792794f15b759" + + "9915f703aab55ed25424d60b17026b7b06c6ad4b9be30a3c63c000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "000000000cd0c59cd0f2a59cd0af059cd0c95" + )); + when(securityTokenConnection.getOpenPgpCapabilities()).thenReturn(openPgpCapabilities); + + ResponseApdu dummyResponseApdu = ResponseApdu.fromBytes(Hex.decode("010203049000")); + + when(commandFactory.createDecipherCommand(any(byte[].class))).thenReturn(dummyCommandApdu); + when(securityTokenConnection.communicate(dummyCommandApdu)).thenReturn(dummyResponseApdu); + + byte[] response = useCase.decryptSessionKey(RSA_ENC_SESSIONKEY_MPI, null); + + assertArrayEquals(Hex.decode("01020304"), response); + } +} \ No newline at end of file From 0ab71ea498ad2a6d439daa6d6e9e84c7ef07dd1a Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 02:46:06 +0100 Subject: [PATCH 03/15] extract unit test for SecurityTokenChangeKeyUseCase --- .../SecurityTokenChangeKeyUseCaseTest.java | 133 ++++++++++++++++++ .../SecurityTokenConnectionTest.java | 79 ----------- 2 files changed, 133 insertions(+), 79 deletions(-) create mode 100644 OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCaseTest.java diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCaseTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCaseTest.java new file mode 100644 index 000000000..4dd1f9bb0 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCaseTest.java @@ -0,0 +1,133 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import java.io.IOException; +import java.util.LinkedList; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalMatchers; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowLog; +import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType; +import org.sufficientlysecure.keychain.util.Passphrase; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("WeakerAccess") +@RunWith(KeychainTestRunner.class) +public class SecurityTokenChangeKeyUseCaseTest { + SecurityTokenChangeKeyUseCase useCase; + OpenPgpCommandApduFactory commandFactory; + SecurityTokenConnection securityTokenConnection; + + CommandApdu dummyCommandApdu = mock(CommandApdu.class); + + @Before + public void setUp() throws Exception { + ShadowLog.stream = System.out; + + securityTokenConnection = mock(SecurityTokenConnection.class); + + commandFactory = mock(OpenPgpCommandApduFactory.class); + when(securityTokenConnection.getCommandFactory()).thenReturn(commandFactory); + + useCase = SecurityTokenChangeKeyUseCase.create(securityTokenConnection); + + } + + @Test + public void testPutKey() throws Exception { + OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities( + Hex.decode("6e81de4f10d27600012401020000060364311500005f520f0073000080000000000000000000007381b7c00af" + + "00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f03" + + "0303c53c4ec5fee25c4e89654d58cad8492510a89d3c3d8468da7b24e15bfc624c6a792794f15b759" + + "9915f703aab55ed25424d60b17026b7b06c6ad4b9be30a3c63c000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "000000000cd0c59cd0f2a59cd0af059cd0c95" + )); + when(securityTokenConnection.getOpenPgpCapabilities()).thenReturn(openPgpCapabilities); + + /* + Security.insertProviderAt(new BouncyCastleProvider(), 1); + ShadowLog.stream = System.out; + + SaveKeyringParcel.Builder builder = SaveKeyringParcel.buildNewKeyringParcel(); + builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd( + Algorithm.RSA, 2048, null, KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, 0L)); + + builder.addUserId("gnuk"); + builder.setNewUnlock(ChangeUnlockParcel.createUnLockParcelForNewKey(new Passphrase())); + + PgpKeyOperation op = new PgpKeyOperation(null); + + PgpEditKeyResult result = op.createSecretKeyRing(builder.build()); + Assert.assertTrue("initial test key creation must succeed", result.success()); + Assert.assertNotNull("initial test key creation must succeed", result.getRing()); + */ + + UncachedKeyRing staticRing = readRingFromResource("/test-keys/token-import-rsa.sec"); + + CanonicalizedSecretKeyRing canRing = (CanonicalizedSecretKeyRing) staticRing.canonicalize(new OperationLog(), 0); + CanonicalizedSecretKey signKey = canRing.getSecretKey(); + signKey.unlock(null); + + byte[] expectedKeyData = Hex.decode( + "4d8203a2b6007f48159103928180938180948180958180968180978201005f48820383010001be5e36a09458313f95" + + "94f3f76a972989dfa1d4a416f7f461c8a4ccf9b9de8ee59447e44b4a4833a00c655ae5516214a72efa5c140fd7d4" + + "29d9b15805c77c881e6ad10711b4614d2183497a5a6d36ed221146301ce6ccf42581004c313d533d14c57abc3288" + + "6419665b67091d652aa6cb074da682135115657d90752fb9d2e69fffd7580adddf1d7822d9d40220401056674b93" + + "3efeb3bc51eafe2c5a5162ec2b466591b689d9af07004bb81a509c7601043a2da871f5989e4e338b901afb9ba8f2" + + "b8bc18ac3300e750bda2a0886459363542bb5b59cab2ed934388c486b602a3f6a63164afe55139ad9f4ed230421b" + + "4039b09f5c0b7c609ba9927f1f7c4db7c3895bbe8e58787ddae295468d034a0c29964b80f3551d308722de7ac698" + + "e840eb68c81c75d140ac347e35d8167bd9ac175610f811f049061b72ebc491d89610fc6ba1344c8d03e2871181f0" + + "408f87779149a1b1835705b407baf28c30e94da82c8e15b845f2473deee6987f29a2d25232361818fd83283a0959" + + "2345ac56d9a56408ef5b19065d6d5252aeff1469c85686c61c4e62b541461320dbbb532d4a28e2d5a6da2c3e7c4d" + + "100204efd33b92a2ed85e2f2576eb6ee9a58415ea446ccad86dff497a45917080bbea1c0406647e1b16ba623b3f7" + + "913f14538db405cb9f108add09f9b3557b7d45b49c8d6cf7c69cb582ce3e3674b9a58b71ed49d2c7a2027955ba0a" + + "be596a11add7bfb5d2a08bd6ed9cdf2e0fc5b8e4396ecc8c801715569d89912f2a4336b5f75a9a04ae8ca460c626" + + "6c7830213f724c5957dc44054699fa1a9adc2c48472ede53a7b77ea3353ccf75394f1e65100eb49ccbdc603de36f" + + "2f11cece6e36a2587d4338466917d28edf0e75a8706748ddf64af3d3b4f129f991be3ffb024c13038806fb6d32f0" + + "dc20adb288fc190985dc9d0a976e108dcecffdf94b97a0de2f94ff6c8996fa6aaeeb97cc9d466fa8f92e2dd179c2" + + "4b46bd165a68efbdce4e397e841e44ffa48991fa23abbd6ff4d0c387a048a9ca323b4867c504d61af02048b4af78" + + "7b0994fd71b9bc39dda6a4f3b610297c8b35affde21a53ec4954c6b1da6403a7cb627555686acc779ca19fb14d7e" + + "c2ca3655571f36587e025bdc0ce053193a7c4be1db17e9ba5788edb43f81f02ef07cc7c1d967e8435fba00e986ab" + + "30fa69746857fc082b5b797d5eea3c6fb1be4a1a12bba89d4ca8c204e2702d93e9316b6176726121dd29c8a8b75e" + + "c19e1deb09e4cc3b95b054541d"); + + ResponseApdu dummyResponseApdu = ResponseApdu.fromBytes(Hex.decode("9000")); + + when(commandFactory.createPutKeyCommand(AdditionalMatchers.aryEq(expectedKeyData))).thenReturn(dummyCommandApdu); + when(securityTokenConnection.communicate(dummyCommandApdu)).thenReturn(dummyResponseApdu); + + Passphrase adminPin = new Passphrase("12345678"); + + + useCase.putKey(KeyType.SIGN, signKey, new Passphrase("123456"), adminPin); + + + verify(securityTokenConnection).verifyAdminPin(adminPin); + verify(securityTokenConnection).communicate(dummyCommandApdu); + } + + UncachedKeyRing readRingFromResource(String name) throws Exception { + return UncachedKeyRing.fromStream(SecurityTokenChangeKeyUseCaseTest.class.getResourceAsStream(name)).next(); + } +} \ No newline at end of file diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java index 0524b6904..4cd842cd0 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java @@ -112,81 +112,6 @@ public class SecurityTokenConnectionTest { verifyDialog(); } - @Test - public void testPutKey() throws Exception { - /* - Security.insertProviderAt(new BouncyCastleProvider(), 1); - ShadowLog.stream = System.out; - - SaveKeyringParcel.Builder builder = SaveKeyringParcel.buildNewKeyringParcel(); - builder.addSubkeyAdd(SubkeyAdd.createSubkeyAdd( - Algorithm.RSA, 2048, null, KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, 0L)); - - builder.addUserId("gnuk"); - builder.setNewUnlock(ChangeUnlockParcel.createUnLockParcelForNewKey(new Passphrase())); - - PgpKeyOperation op = new PgpKeyOperation(null); - - PgpEditKeyResult result = op.createSecretKeyRing(builder.build()); - Assert.assertTrue("initial test key creation must succeed", result.success()); - Assert.assertNotNull("initial test key creation must succeed", result.getRing()); - */ - - UncachedKeyRing staticRing = readRingFromResource("/test-keys/token-import-rsa.sec"); - - CanonicalizedSecretKeyRing canRing = (CanonicalizedSecretKeyRing) staticRing.canonicalize(new OperationLog(), 0); - CanonicalizedSecretKey signKey = canRing.getSecretKey(); - signKey.unlock(null); - - SecurityTokenConnection securityTokenConnection = - new SecurityTokenConnection(transport, new Passphrase("123456"), new OpenPgpCommandApduFactory()); - OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities( - Hex.decode( - "6e81de4f10d27600012401020000060364311500005f520f0073000080000000000000000000007381b7c00af" + - "00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f03" + - "0303c53c4ec5fee25c4e89654d58cad8492510a89d3c3d8468da7b24e15bfc624c6a792794f15b759" + - "9915f703aab55ed25424d60b17026b7b06c6ad4b9be30a3c63c000000000000000000000000000000" + - "000000000000000000000000000000000000000000000000000000000000000000000000000000000" + - "000000000cd0c59cd0f2a59cd0af059cd0c95" - )); - securityTokenConnection.setConnectionCapabilities(openPgpCapabilities); - securityTokenConnection.determineTokenType(); - - expect("00200083083132333435363738", "9000"); - expect("10db3fffff4d8203a2b6007f48159103928180938180948180958180968180978201005f48820383010001be5e36a094" + - "58313f9594f3f76a972989dfa1d4a416f7f461c8a4ccf9b9de8ee59447e44b4a4833a00c655ae5516214a72efa5c140" + - "fd7d429d9b15805c77c881e6ad10711b4614d2183497a5a6d36ed221146301ce6ccf42581004c313d533d14c57abc32" + - "886419665b67091d652aa6cb074da682135115657d90752fb9d2e69fffd7580adddf1d7822d9d40220401056674b933" + - "efeb3bc51eafe2c5a5162ec2b466591b689d9af07004bb81a509c7601043a2da871f5989e4e338b901afb9ba8f2b8bc" + - "18ac3300e750bda2a0886459363542bb5b59cab2ed93", "9000"); - expect("10db3fffff4388c486b602a3f6a63164afe55139ad9f4ed230421b4039b09f5c0b7c609ba9927f1f7c4db7c3895bbe8e" + - "58787ddae295468d034a0c29964b80f3551d308722de7ac698e840eb68c81c75d140ac347e35d8167bd9ac175610f81" + - "1f049061b72ebc491d89610fc6ba1344c8d03e2871181f0408f87779149a1b1835705b407baf28c30e94da82c8e15b8" + - "45f2473deee6987f29a2d25232361818fd83283a09592345ac56d9a56408ef5b19065d6d5252aeff1469c85686c61c4" + - "e62b541461320dbbb532d4a28e2d5a6da2c3e7c4d100204efd33b92a2ed85e2f2576eb6ee9a58415ea446ccad86dff4" + - "97a45917080bbea1c0406647e1b16ba623b3f7913f14", "9000"); - expect("10db3fffff538db405cb9f108add09f9b3557b7d45b49c8d6cf7c69cb582ce3e3674b9a58b71ed49d2c7a2027955ba0a" + - "be596a11add7bfb5d2a08bd6ed9cdf2e0fc5b8e4396ecc8c801715569d89912f2a4336b5f75a9a04ae8ca460c6266c7" + - "830213f724c5957dc44054699fa1a9adc2c48472ede53a7b77ea3353ccf75394f1e65100eb49ccbdc603de36f2f11ce" + - "ce6e36a2587d4338466917d28edf0e75a8706748ddf64af3d3b4f129f991be3ffb024c13038806fb6d32f0dc20adb28" + - "8fc190985dc9d0a976e108dcecffdf94b97a0de2f94ff6c8996fa6aaeeb97cc9d466fa8f92e2dd179c24b46bd165a68" + - "efbdce4e397e841e44ffa48991fa23abbd6ff4d0c387", "9000"); - expect("00db3fffa9a048a9ca323b4867c504d61af02048b4af787b0994fd71b9bc39dda6a4f3b610297c8b35affde21a53ec49" + - "54c6b1da6403a7cb627555686acc779ca19fb14d7ec2ca3655571f36587e025bdc0ce053193a7c4be1db17e9ba5788e" + - "db43f81f02ef07cc7c1d967e8435fba00e986ab30fa69746857fc082b5b797d5eea3c6fb1be4a1a12bba89d4ca8c204" + - "e2702d93e9316b6176726121dd29c8a8b75ec19e1deb09e4cc3b95b054541d", "9000"); - - - SecurityTokenChangeKeyUseCase changeKeyUseCase = SecurityTokenChangeKeyUseCase.create(securityTokenConnection); - changeKeyUseCase.putKey(KeyType.SIGN, signKey, new Passphrase("123456"), new Passphrase("12345678")); - - - verifyDialog(); - } - - private void debugTransceive() throws IOException { - } - private void expect(String commandApdu, String responseApdu) { expect(CommandApdu.fromBytes(Hex.decode(commandApdu)), ResponseApdu.fromBytes(Hex.decode(responseApdu))); } @@ -200,8 +125,4 @@ public class SecurityTokenConnectionTest { assertTrue(expectCommands.isEmpty()); assertTrue(expectReplies.isEmpty()); } - - UncachedKeyRing readRingFromResource(String name) throws Exception { - return UncachedKeyRing.fromStream(SecurityTokenConnectionTest.class.getResourceAsStream(name)).next(); - } } \ No newline at end of file From 139735f0e1c3460cdb66567b1d9431eae0634f50 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 02:04:58 +0100 Subject: [PATCH 04/15] refactor decryptSessionKey --- .../securitytoken/PsoDecryptUseCase.java | 199 +++++++++--------- .../ui/SecurityTokenOperationActivity.java | 2 +- .../securitytoken/PsoDecryptUseCaseTest.java | 5 +- 3 files changed, 106 insertions(+), 100 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java index 1bc5eb600..45d54b063 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java @@ -24,142 +24,151 @@ import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; +/** This class implements the PSO:DECIPHER operation, as specified in OpenPGP card spec / 7.2.11 (p52 in v3.0.1). + * + * See https://www.g10code.com/docs/openpgp-card-3.0.pdf + */ public class PsoDecryptUseCase { private final SecurityTokenConnection connection; private final JcaKeyFingerprintCalculator fingerprintCalculator; public static PsoDecryptUseCase create(SecurityTokenConnection connection) { - return new PsoDecryptUseCase(connection); + return new PsoDecryptUseCase(connection, new JcaKeyFingerprintCalculator()); } - private PsoDecryptUseCase(SecurityTokenConnection connection) { + private PsoDecryptUseCase(SecurityTokenConnection connection, + JcaKeyFingerprintCalculator jcaKeyFingerprintCalculator) { this.connection = connection; - this.fingerprintCalculator = new JcaKeyFingerprintCalculator(); + this.fingerprintCalculator = jcaKeyFingerprintCalculator; } - public byte[] decryptSessionKey(@NonNull byte[] encryptedSessionKey, - CanonicalizedPublicKey publicKey) + public byte[] verifyAndDecryptSessionKey(@NonNull byte[] encryptedSessionKeyMpi, CanonicalizedPublicKey publicKey) throws IOException { - final KeyFormat kf = connection.getOpenPgpCapabilities().getFormatForKeyType(KeyType.ENCRYPT); - connection.verifyPinForOther(); - byte[] data; - byte[] dataLen; - int pLen = 0; - - X9ECParameters x9Params; - + KeyFormat kf = connection.getOpenPgpCapabilities().getFormatForKeyType(KeyType.ENCRYPT); switch (kf.keyFormatType()) { case RSAKeyFormatType: - data = Arrays.copyOfRange(encryptedSessionKey, 2, encryptedSessionKey.length); - if (data[0] != 0) { - data = Arrays.prepend(data, (byte) 0x00); - } - break; + return decryptSessionKeyRsa(encryptedSessionKeyMpi); case ECKeyFormatType: - pLen = ((((encryptedSessionKey[0] & 0xff) << 8) + (encryptedSessionKey[1] & 0xff)) + 7) / 8; - data = new byte[pLen]; - - System.arraycopy(encryptedSessionKey, 2, data, 0, pLen); - - final ECKeyFormat eckf = (ECKeyFormat) kf; - x9Params = NISTNamedCurves.getByOID(eckf.getCurveOID()); - - final ECPoint p = x9Params.getCurve().decodePoint(data); - if (!p.isValid()) { - throw new CardException("Invalid EC point!"); - } - - data = p.getEncoded(false); - - if (data.length < 128) { - dataLen = new byte[]{(byte) data.length}; - } else { - dataLen = new byte[]{(byte) 0x81, (byte) data.length}; - } - data = Arrays.concatenate(Hex.decode("86"), dataLen, data); - - if (data.length < 128) { - dataLen = new byte[]{(byte) data.length}; - } else { - dataLen = new byte[]{(byte) 0x81, (byte) data.length}; - } - data = Arrays.concatenate(Hex.decode("7F49"), dataLen, data); - - if (data.length < 128) { - dataLen = new byte[]{(byte) data.length}; - } else { - dataLen = new byte[]{(byte) 0x81, (byte) data.length}; - } - data = Arrays.concatenate(Hex.decode("A6"), dataLen, data); - break; + return decryptSessionKeyEcdh(encryptedSessionKeyMpi, (ECKeyFormat) kf, publicKey); default: throw new CardException("Unknown encryption key type!"); } + } - CommandApdu command = connection.getCommandFactory().createDecipherCommand(data); + private byte[] decryptSessionKeyRsa(byte[] encryptedSessionKeyMpi) throws IOException { + int mpiLength = getMpiLength(encryptedSessionKeyMpi); + if (mpiLength != encryptedSessionKeyMpi.length - 2) { + throw new IOException("Malformed RSA session key!"); + } + + byte[] psoDecipherPayload = new byte[mpiLength + 1]; + psoDecipherPayload[0] = (byte) 0x00; // RSA Padding Indicator Byte + System.arraycopy(encryptedSessionKeyMpi, 2, psoDecipherPayload, 1, mpiLength); + + CommandApdu command = connection.getCommandFactory().createDecipherCommand(psoDecipherPayload); ResponseApdu response = connection.communicate(command); if (!response.isSuccess()) { throw new CardException("Deciphering with Security token failed on receive", response.getSw()); } - switch (connection.getOpenPgpCapabilities().getFormatForKeyType(KeyType.ENCRYPT).keyFormatType()) { - case RSAKeyFormatType: - return response.getData(); + return response.getData(); + } - /* From 3.x OpenPGP card specification : - In case of ECDH the card supports a partial decrypt only. - With its own private key and the given public key the card calculates a shared secret - in compliance with the Elliptic Curve Key Agreement Scheme from Diffie-Hellman. - The shared secret is returned in the response, all other calculation for deciphering - are done outside of the card. + private byte[] decryptSessionKeyEcdh(byte[] encryptedSessionKeyMpi, ECKeyFormat eckf, CanonicalizedPublicKey publicKey) + throws IOException { + int mpiLength = getMpiLength(encryptedSessionKeyMpi); + byte[] encryptedPoint = Arrays.copyOfRange(encryptedSessionKeyMpi, 2, mpiLength); - The shared secret obtained is a KEK (Key Encryption Key) that is used to wrap the - session key. + X9ECParameters x9Params = NISTNamedCurves.getByOID(eckf.getCurveOID()); + ECPoint p = x9Params.getCurve().decodePoint(encryptedPoint); + if (!p.isValid()) { + throw new CardException("Invalid EC point!"); + } - From rfc6637#section-13 : - This document explicitly discourages the use of algorithms other than AES as a KEK algorithm. - */ - case ECKeyFormatType: - data = response.getData(); + byte[] psoDecipherPayload = p.getEncoded(false); - final byte[] keyEnc = new byte[encryptedSessionKey[pLen + 2]]; + byte[] dataLen; + if (psoDecipherPayload.length < 128) { + dataLen = new byte[]{(byte) psoDecipherPayload.length}; + } else { + dataLen = new byte[]{(byte) 0x81, (byte) psoDecipherPayload.length}; + } + psoDecipherPayload = Arrays.concatenate(Hex.decode("86"), dataLen, psoDecipherPayload); - System.arraycopy(encryptedSessionKey, 2 + pLen + 1, keyEnc, 0, keyEnc.length); + if (psoDecipherPayload.length < 128) { + dataLen = new byte[]{(byte) psoDecipherPayload.length}; + } else { + dataLen = new byte[]{(byte) 0x81, (byte) psoDecipherPayload.length}; + } + psoDecipherPayload = Arrays.concatenate(Hex.decode("7F49"), dataLen, psoDecipherPayload); - try { - final MessageDigest kdf = MessageDigest.getInstance(MessageDigestUtils.getDigestName(publicKey.getSecurityTokenHashAlgorithm())); + if (psoDecipherPayload.length < 128) { + dataLen = new byte[]{(byte) psoDecipherPayload.length}; + } else { + dataLen = new byte[]{(byte) 0x81, (byte) psoDecipherPayload.length}; + } + psoDecipherPayload = Arrays.concatenate(Hex.decode("A6"), dataLen, psoDecipherPayload); - kdf.update(new byte[]{(byte) 0, (byte) 0, (byte) 0, (byte) 1}); - kdf.update(data); - kdf.update(publicKey.createUserKeyingMaterial(fingerprintCalculator)); + CommandApdu command = connection.getCommandFactory().createDecipherCommand(psoDecipherPayload); + ResponseApdu response = connection.communicate(command); - final byte[] kek = kdf.digest(); - final Cipher c = Cipher.getInstance("AESWrap"); + if (!response.isSuccess()) { + throw new CardException("Deciphering with Security token failed on receive", response.getSw()); + } - c.init(Cipher.UNWRAP_MODE, new SecretKeySpec(kek, 0, publicKey.getSecurityTokenSymmetricKeySize() / 8, "AES")); + /* From 3.x OpenPGP card specification : + In case of ECDH the card supports a partial decrypt only. + With its own private key and the given public key the card calculates a shared secret + in compliance with the Elliptic Curve Key Agreement Scheme from Diffie-Hellman. + The shared secret is returned in the response, all other calculation for deciphering + are done outside of the card. - final Key paddedSessionKey = c.unwrap(keyEnc, "Session", Cipher.SECRET_KEY); + The shared secret obtained is a KEK (Key Encryption Key) that is used to wrap the + session key. - Arrays.fill(kek, (byte) 0); + From rfc6637#section-13 : + This document explicitly discourages the use of algorithms other than AES as a KEK algorithm. + */ + byte[] keyEncryptionKey = response.getData(); - return PGPPad.unpadSessionData(paddedSessionKey.getEncoded()); - } catch (NoSuchAlgorithmException e) { - throw new CardException("Unknown digest/encryption algorithm!"); - } catch (NoSuchPaddingException e) { - throw new CardException("Unknown padding algorithm!"); - } catch (PGPException e) { - throw new CardException(e.getMessage()); - } catch (InvalidKeyException e) { - throw new CardException("Invalid KEK!"); - } + final byte[] keyEnc = new byte[encryptedSessionKeyMpi[mpiLength + 2]]; - default: - throw new CardException("Unknown encryption key type!"); + System.arraycopy(encryptedSessionKeyMpi, 2 + mpiLength + 1, keyEnc, 0, keyEnc.length); + + try { + final MessageDigest kdf = MessageDigest.getInstance(MessageDigestUtils.getDigestName(publicKey.getSecurityTokenHashAlgorithm())); + + kdf.update(new byte[]{(byte) 0, (byte) 0, (byte) 0, (byte) 1}); + kdf.update(keyEncryptionKey); + kdf.update(publicKey.createUserKeyingMaterial(fingerprintCalculator)); + + byte[] kek = kdf.digest(); + Cipher c = Cipher.getInstance("AESWrap"); + + c.init(Cipher.UNWRAP_MODE, new SecretKeySpec(kek, 0, publicKey.getSecurityTokenSymmetricKeySize() / 8, "AES")); + + Key paddedSessionKey = c.unwrap(keyEnc, "Session", Cipher.SECRET_KEY); + + Arrays.fill(kek, (byte) 0); + + return PGPPad.unpadSessionData(paddedSessionKey.getEncoded()); + } catch (NoSuchAlgorithmException e) { + throw new CardException("Unknown digest/encryption algorithm!"); + } catch (NoSuchPaddingException e) { + throw new CardException("Unknown padding algorithm!"); + } catch (PGPException e) { + throw new CardException(e.getMessage()); + } catch (InvalidKeyException e) { + throw new CardException("Invalid KEK!"); } } + + private int getMpiLength(byte[] multiPrecisionInteger) { + return ((((multiPrecisionInteger[0] & 0xff) << 8) + (multiPrecisionInteger[1] & 0xff)) + 7) / 8; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index 04a51296f..1d6642523 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -212,7 +212,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { for (int i = 0; i < mRequiredInput.mInputData.length; i++) { byte[] encryptedSessionKey = mRequiredInput.mInputData[i]; byte[] decryptedSessionKey = psoDecryptUseCase - .decryptSessionKey(encryptedSessionKey, publicKeyRing.getPublicKey(tokenKeyId)); + .verifyAndDecryptSessionKey(encryptedSessionKey, publicKeyRing.getPublicKey(tokenKeyId)); mInputParcel = mInputParcel.withCryptoData(encryptedSessionKey, decryptedSessionKey); } break; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java index b2d250811..a41ebf305 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java @@ -6,9 +6,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.sufficientlysecure.keychain.KeychainTestRunner; -import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType; -import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType; -import org.sufficientlysecure.keychain.util.Passphrase; import static org.junit.Assert.*; import static org.mockito.Matchers.any; @@ -58,7 +55,7 @@ public class PsoDecryptUseCaseTest { when(commandFactory.createDecipherCommand(any(byte[].class))).thenReturn(dummyCommandApdu); when(securityTokenConnection.communicate(dummyCommandApdu)).thenReturn(dummyResponseApdu); - byte[] response = useCase.decryptSessionKey(RSA_ENC_SESSIONKEY_MPI, null); + byte[] response = useCase.verifyAndDecryptSessionKey(RSA_ENC_SESSIONKEY_MPI, null); assertArrayEquals(Hex.decode("01020304"), response); } From 72121489130d229ad6793614f60e477d8c38aa8b Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 04:42:02 +0100 Subject: [PATCH 05/15] add unit test for against commit e22cd98 --- .../SecurityTokenConnectionCompatTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java new file mode 100644 index 000000000..406ee6471 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java @@ -0,0 +1,82 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sufficientlysecure.keychain.KeychainTestRunner; + +import static junit.framework.Assert.assertEquals; + + +@RunWith(KeychainTestRunner.class) +@Ignore("Only for reference right now") +public class SecurityTokenConnectionCompatTest { + private byte[] encryptedSessionKey; + private OpenPgpCommandApduFactory openPgpCommandApduFactory; + + @Before + public void setUp() throws Exception { + encryptedSessionKey = Hex.decode("07ff7b9ff36f70da1fe7a6b59168c24a7e5b48a938c4f970de46524a06ebf4a9175a9737cf2e6f30957110b31db70e9a2992401b1d5e99389f976356f4e3a28ff537362e7ce14b81200e21d4f0e77d46bd89f3a54ca06062289148a59387488ac01d30d2baf58e6b35e32434720473604a9f7d5083ca6d40e4a2dadedd68033a4d4bbdb06d075d6980c0c0ca19078dcdfb9d8cbcb34f28d0b968b6e09eda0e1d3ab6b251eb09f9fb9d9abfeaf9010001733b9015e9e4b6c9df61bbc76041f439d1273e41f5d0e8414a2b8d6d4c7e86f30b94cfba308b38de53d694a8ca15382301ace806c8237641b03525b3e3e8cbb017e251265229bcbb0da5d5aeb4eafbad9779"); + + openPgpCommandApduFactory = new OpenPgpCommandApduFactory(); + } + + /* we have a report of breaking compatibility on some earlier version. + this test checks what was sent in that version to what we send now. + // see https://github.com/open-keychain/open-keychain/issues/2049 + // see https://github.com/open-keychain/open-keychain/commit/ee8cd3862f65de580ed949bc838628610e22cd98 + */ + + @Test + public void testPrePostEquals() { + List preApdus = decryptPre_ee8cd38(); + List postApdus = decryptNow(); + + assertEquals(preApdus, postApdus); + } + + public List decryptPre_ee8cd38() { + final int MAX_APDU_DATAFIELD_SIZE = 254; + + int offset = 1; // Skip first byte + List apduData = new ArrayList<>(); + + // 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); + apduData.add(cla + "2a8086" + Hex.toHexString(new byte[]{(byte) len}) + + Hex.toHexString(encryptedSessionKey, offset, len)); + + offset += MAX_APDU_DATAFIELD_SIZE; + } + + return apduData; + } + + public List decryptNow() { + int mpiLength = ((((encryptedSessionKey[0] & 0xff) << 8) + (encryptedSessionKey[1] & 0xff)) + 7) / 8; + byte[] psoDecipherPayload = new byte[mpiLength + 1]; + psoDecipherPayload[0] = (byte) 0x00; + System.arraycopy(encryptedSessionKey, 2, psoDecipherPayload, 1, mpiLength); + + CommandApdu command = openPgpCommandApduFactory.createDecipherCommand(psoDecipherPayload); + List chainedApdus = openPgpCommandApduFactory.createChainedApdus(command); + + List apduData = new ArrayList<>(); + for (CommandApdu chainCommand : chainedApdus) { + apduData.add(Hex.toHexString(chainCommand.toBytes())); + } + + return apduData; + } +} \ No newline at end of file From bb2b37cff6b0d05b632d52d2af572538e2f304b2 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 15:44:23 +0100 Subject: [PATCH 06/15] SecurityTokenConnection code style --- .../SecurityTokenConnection.java | 129 +++++++++--------- .../securitytoken/SecurityTokenInfo.java | 7 + .../CreateSecurityTokenAlgorithmFragment.java | 2 +- .../ui/CreateSecurityTokenPinFragment.java | 2 +- 4 files changed, 71 insertions(+), 69 deletions(-) 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 92c44db65..a158aed3a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -48,26 +48,27 @@ public class SecurityTokenConnection { private static SecurityTokenConnection sCachedInstance; @NonNull - private final Transport mTransport; + private final Transport transport; @Nullable - private final Passphrase mPin; + private final Passphrase cachedPin; private final OpenPgpCommandApduFactory commandFactory; private TokenType tokenType; - private CardCapabilities mCardCapabilities; - private OpenPgpCapabilities mOpenPgpCapabilities; - private SecureMessaging mSecureMessaging; + private CardCapabilities cardCapabilities; + private OpenPgpCapabilities openPgpCapabilities; - private boolean mPw1ValidatedForSignature; - private boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming? - private boolean mPw3Validated; + private SecureMessaging secureMessaging; + + private boolean isPw1ValidatedForSignature; // Mode 81 + private boolean isPw1ValidatedForOther; // Mode 82 + private boolean isPw3Validated; public static SecurityTokenConnection getInstanceForTransport( @NonNull Transport transport, @Nullable Passphrase pin) { if (sCachedInstance == null || !sCachedInstance.isPersistentConnectionAllowed() || - !sCachedInstance.isConnected() || !sCachedInstance.mTransport.equals(transport) || - (pin != null && !pin.equals(sCachedInstance.mPin))) { + !sCachedInstance.isConnected() || !sCachedInstance.transport.equals(transport) || + (pin != null && !pin.equals(sCachedInstance.cachedPin))) { sCachedInstance = new SecurityTokenConnection(transport, pin, new OpenPgpCommandApduFactory()); } return sCachedInstance; @@ -81,14 +82,14 @@ public class SecurityTokenConnection { @VisibleForTesting SecurityTokenConnection(@NonNull Transport transport, @Nullable Passphrase pin, OpenPgpCommandApduFactory commandFactory) { - this.mTransport = transport; - this.mPin = pin; + this.transport = transport; + this.cachedPin = pin; this.commandFactory = commandFactory; } OpenPgpCapabilities getOpenPgpCapabilities() { - return mOpenPgpCapabilities; + return openPgpCapabilities; } OpenPgpCommandApduFactory getCommandFactory() { @@ -96,8 +97,8 @@ public class SecurityTokenConnection { } void maybeInvalidatePw1() { - if (!mOpenPgpCapabilities.isPw1ValidForMultipleSignatures()) { - mPw1ValidatedForSignature = false; + if (!openPgpCapabilities.isPw1ValidForMultipleSignatures()) { + isPw1ValidatedForSignature = false; } } @@ -128,10 +129,10 @@ public class SecurityTokenConnection { @VisibleForTesting void connectToDevice(Context context) throws IOException { // Connect on transport layer - mTransport.connect(); + transport.connect(); // dummy instance for initial communicate() calls - mCardCapabilities = new CardCapabilities(); + cardCapabilities = new CardCapabilities(); determineTokenType(); @@ -144,15 +145,15 @@ public class SecurityTokenConnection { refreshConnectionCapabilities(); - mPw1ValidatedForSignature = false; - mPw1ValidatedForDecrypt = false; - mPw3Validated = false; + isPw1ValidatedForSignature = false; + isPw1ValidatedForOther = false; + isPw3Validated = false; - if (mOpenPgpCapabilities.isHasSCP11bSM()) { + if (openPgpCapabilities.isHasSCP11bSM()) { try { SCP11bSecureMessaging.establish(this, context, commandFactory); } catch (SecureMessagingException e) { - mSecureMessaging = null; + secureMessaging = null; Log.e(Constants.TAG, "failed to establish secure messaging", e); } } @@ -160,7 +161,7 @@ public class SecurityTokenConnection { @VisibleForTesting void determineTokenType() throws IOException { - tokenType = mTransport.getTokenTypeIfAvailable(); + tokenType = transport.getTokenTypeIfAvailable(); if (tokenType != null) { return; } @@ -192,12 +193,12 @@ public class SecurityTokenConnection { @VisibleForTesting void setConnectionCapabilities(OpenPgpCapabilities openPgpCapabilities) throws IOException { - this.mOpenPgpCapabilities = openPgpCapabilities; - this.mCardCapabilities = new CardCapabilities(openPgpCapabilities.getHistoricalBytes()); + this.openPgpCapabilities = openPgpCapabilities; + this.cardCapabilities = new CardCapabilities(openPgpCapabilities.getHistoricalBytes()); } public void resetPin(byte[] newPin, Passphrase adminPin) throws IOException { - if (!mPw3Validated) { + if (!isPw3Validated) { verifyAdminPin(adminPin); } @@ -236,7 +237,7 @@ public class SecurityTokenConnection { CommandApdu changePin = commandFactory.createChangePw3Command(pin, newAdminPin); ResponseApdu response = communicate(changePin); - mPw3Validated = false; + isPw3Validated = false; if (!response.isSuccess()) { throw new CardException("Failed to change PIN", response.getSw()); @@ -247,35 +248,35 @@ public class SecurityTokenConnection { * Verifies the user's PW1 with the appropriate mode. */ void verifyPinForSignature() throws IOException { - if (mPw1ValidatedForSignature) { + if (isPw1ValidatedForSignature) { return; } - if (mPin == null) { + if (cachedPin == null) { throw new IllegalStateException("Connection not initialized with Pin!"); } - byte[] pin = mPin.toStringUnsafe().getBytes(); + byte[] pin = cachedPin.toStringUnsafe().getBytes(); ResponseApdu response = communicate(commandFactory.createVerifyPw1ForSignatureCommand(pin)); if (!response.isSuccess()) { throw new CardException("Bad PIN!", response.getSw()); } - mPw1ValidatedForSignature = true; + isPw1ValidatedForSignature = true; } /** * Verifies the user's PW1 with the appropriate mode. */ void verifyPinForOther() throws IOException { - if (mPw1ValidatedForDecrypt) { + if (isPw1ValidatedForOther) { return; } - if (mPin == null) { + if (cachedPin == null) { throw new IllegalStateException("Connection not initialized with Pin!"); } - byte[] pin = mPin.toStringUnsafe().getBytes(); + byte[] pin = cachedPin.toStringUnsafe().getBytes(); // Command APDU for VERIFY command (page 32) ResponseApdu response = communicate(commandFactory.createVerifyPw1ForOtherCommand(pin)); @@ -283,14 +284,14 @@ public class SecurityTokenConnection { throw new CardException("Bad PIN!", response.getSw()); } - mPw1ValidatedForDecrypt = true; + isPw1ValidatedForOther = true; } /** * Verifies the user's PW1 or PW3 with the appropriate mode. */ void verifyAdminPin(Passphrase adminPin) throws IOException { - if (mPw3Validated) { + if (isPw3Validated) { return; } // Command APDU for VERIFY command (page 32) @@ -300,7 +301,7 @@ public class SecurityTokenConnection { throw new CardException("Bad PIN!", response.getSw()); } - mPw3Validated = true; + isPw3Validated = true; } /** @@ -310,7 +311,7 @@ public class SecurityTokenConnection { * @return The fingerprints of all subkeys in a contiguous byte array. */ public byte[] getFingerprints() throws IOException { - return mOpenPgpCapabilities.getFingerprints(); + return openPgpCapabilities.getFingerprints(); } /** @@ -319,11 +320,11 @@ public class SecurityTokenConnection { * @return Seven bytes in fixed format, plus 0x9000 status word at the end. */ private byte[] getPwStatusBytes() throws IOException { - return mOpenPgpCapabilities.getPwStatusBytes(); + return openPgpCapabilities.getPwStatusBytes(); } public byte[] getAid() throws IOException { - return mOpenPgpCapabilities.getAid(); + return openPgpCapabilities.getAid(); } public String getUrl() throws IOException { @@ -353,9 +354,9 @@ public class SecurityTokenConnection { * @throws IOException */ ResponseApdu communicate(CommandApdu apdu) throws IOException { - if ((mSecureMessaging != null) && mSecureMessaging.isEstablished()) { + if ((secureMessaging != null) && secureMessaging.isEstablished()) { try { - apdu = mSecureMessaging.encryptAndSign(apdu); + apdu = secureMessaging.encryptAndSign(apdu); } catch (SecureMessagingException e) { clearSecureMessaging(); throw new IOException("secure messaging encrypt/sign failure : " + e.getMessage()); @@ -364,16 +365,16 @@ public class SecurityTokenConnection { ResponseApdu lastResponse = null; // Transmit - if (mCardCapabilities.hasExtended()) { - lastResponse = mTransport.transceive(apdu); + if (cardCapabilities.hasExtended()) { + lastResponse = transport.transceive(apdu); } else if (commandFactory.isSuitableForShortApdu(apdu)) { CommandApdu shortApdu = commandFactory.createShortApdu(apdu); - lastResponse = mTransport.transceive(shortApdu); - } else if (mCardCapabilities.hasChaining()) { + 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 = mTransport.transceive(chainedApdu); + lastResponse = transport.transceive(chainedApdu); boolean isLastCommand = (i == totalCommands - 1); if (!isLastCommand && !lastResponse.isSuccess()) { @@ -393,7 +394,7 @@ public class SecurityTokenConnection { while (lastResponse.getSw1() == APDU_SW1_RESPONSE_AVAILABLE) { // GET RESPONSE ISO/IEC 7816-4 par.7.6.1 CommandApdu getResponse = commandFactory.createGetResponseCommand(lastResponse.getSw2()); - lastResponse = mTransport.transceive(getResponse); + lastResponse = transport.transceive(getResponse); result.write(lastResponse.getData()); } @@ -402,9 +403,9 @@ public class SecurityTokenConnection { lastResponse = ResponseApdu.fromBytes(result.toByteArray()); - if ((mSecureMessaging != null) && mSecureMessaging.isEstablished()) { + if ((secureMessaging != null) && secureMessaging.isEstablished()) { try { - lastResponse = mSecureMessaging.verifyAndDecrypt(lastResponse); + lastResponse = secureMessaging.verifyAndDecrypt(lastResponse); } catch (SecureMessagingException e) { clearSecureMessaging(); throw new IOException("secure messaging verify/decrypt failure : " + e.getMessage()); @@ -433,7 +434,7 @@ public class SecurityTokenConnection { throw new IOException("Invalid key slot"); } - if (!mPw3Validated) { + if (!isPw3Validated) { verifyAdminPin(adminPin); } @@ -516,12 +517,12 @@ public class SecurityTokenConnection { } public boolean isPersistentConnectionAllowed() { - return mTransport.isPersistentConnectionAllowed() && - (mSecureMessaging == null || !mSecureMessaging.isEstablished()); + return transport.isPersistentConnectionAllowed() && + (secureMessaging == null || !secureMessaging.isEstablished()); } public boolean isConnected() { - return mTransport.isConnected(); + return transport.isConnected(); } public TokenType getTokenType() { @@ -529,15 +530,15 @@ public class SecurityTokenConnection { } public void clearSecureMessaging() { - if (mSecureMessaging != null) { - mSecureMessaging.clearSession(); + if (secureMessaging != null) { + secureMessaging.clearSession(); } - mSecureMessaging = null; + secureMessaging = null; } - void setSecureMessaging(final SecureMessaging sm) { + void setSecureMessaging(SecureMessaging sm) { clearSecureMessaging(); - mSecureMessaging = sm; + secureMessaging = sm; } public SecurityTokenInfo getTokenInfo() throws IOException { @@ -554,18 +555,12 @@ public class SecurityTokenConnection { String userId = getUserId(); String url = getUrl(); byte[] pwInfo = getPwStatusBytes(); - boolean hasLifeCycleManagement = mCardCapabilities.hasLifeCycleManagement(); + boolean hasLifeCycleManagement = cardCapabilities.hasLifeCycleManagement(); - TransportType transportType = mTransport.getTransportType(); + TransportType transportType = transport.getTransportType(); return SecurityTokenInfo .create(transportType, tokenType, fingerprints, aid, userId, url, pwInfo[4], pwInfo[6], hasLifeCycleManagement); } - - public static double parseOpenPgpVersion(final byte[] aid) { - float minv = aid[7]; - while (minv > 0) minv /= 10.0; - return aid[6] + minv; - } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenInfo.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenInfo.java index a46d79084..d8945dad6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenInfo.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenInfo.java @@ -166,6 +166,13 @@ public abstract class SecurityTokenInfo implements Parcelable { return Version.create(matcher.group(1)); } + public double getOpenPgpVersion() { + byte[] aid = getAid(); + float minv = aid[7]; + while (minv > 0) minv /= 10.0; + return aid[6] + minv; + } + @AutoValue public static abstract class Version implements Comparable { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenAlgorithmFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenAlgorithmFragment.java index 08304a5d4..b3b9cd33e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenAlgorithmFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenAlgorithmFragment.java @@ -100,7 +100,7 @@ public class CreateSecurityTokenAlgorithmFragment extends Fragment { choices.add(new Choice<>(SupportedKeyType.RSA_4096, getResources().getString( R.string.rsa_4096), getResources().getString(R.string.rsa_4096_description_html))); - final double version = SecurityTokenConnection.parseOpenPgpVersion(mCreateKeyActivity.tokenInfo.getAid()); + final double version = mCreateKeyActivity.tokenInfo.getOpenPgpVersion(); if (version >= 3.0) { choices.add(new Choice<>(SupportedKeyType.ECC_P256, getResources().getString( diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenPinFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenPinFragment.java index 473687064..894250513 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenPinFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenPinFragment.java @@ -205,7 +205,7 @@ public class CreateSecurityTokenPinFragment extends Fragment { mCreateKeyActivity.mSecurityTokenPin = new Passphrase(mPin.getText().toString()); - final double version = SecurityTokenConnection.parseOpenPgpVersion(mCreateKeyActivity.tokenInfo.getAid()); + final double version = mCreateKeyActivity.tokenInfo.getOpenPgpVersion(); Fragment frag; if (version >= 3.0) { From 4cbdad7cb8b1b19679d3b5292846caafa4076643 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 15:51:27 +0100 Subject: [PATCH 07/15] extract ResetAndWipeUseCase --- .../securitytoken/ResetAndWipeUseCase.java | 68 +++++++++++++++++++ .../SecurityTokenConnection.java | 46 ------------- .../ui/SecurityTokenOperationActivity.java | 3 +- 3 files changed, 70 insertions(+), 47 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ResetAndWipeUseCase.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ResetAndWipeUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ResetAndWipeUseCase.java new file mode 100644 index 000000000..84df31b53 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ResetAndWipeUseCase.java @@ -0,0 +1,68 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import java.io.IOException; + + +public class ResetAndWipeUseCase { + private final SecurityTokenConnection connection; + + public static ResetAndWipeUseCase create(SecurityTokenConnection connection) { + return new ResetAndWipeUseCase(connection); + } + + private ResetAndWipeUseCase(SecurityTokenConnection connection) { + this.connection = connection; + } + + /** + * Resets security token, which deletes all keys and data objects. + * This works by entering a wrong PIN and then Admin PIN 4 times respectively. + * Afterwards, the token is reactivated. + */ + public void resetAndWipeToken() throws IOException { + // try wrong PIN 4 times until counter goes to C0 + byte[] pin = "XXXXXX".getBytes(); + CommandApdu verifyPw1ForSignatureCommand = + connection.getCommandFactory().createVerifyPw1ForSignatureCommand(pin); + for (int i = 0; i <= 4; i++) { + // Command APDU for VERIFY command (page 32) + ResponseApdu response = connection.communicate(verifyPw1ForSignatureCommand); + if (response.isSuccess()) { + 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(); + CommandApdu verifyPw3Command = connection.getCommandFactory().createVerifyPw3Command(adminPin); + for (int i = 0; i <= 4; i++) { + // Command APDU for VERIFY command (page 32) + ResponseApdu response = connection.communicate( + verifyPw3Command); + if (response.isSuccess()) { // Should NOT accept! + throw new CardException("Should never happen, XXXXXXXX has been accepted", response.getSw()); + } + } + + // secure messaging must be disabled before reactivation + connection.clearSecureMessaging(); + + // 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 = connection.getCommandFactory().createReactivate1Command(); + ResponseApdu response1 = connection.communicate(reactivate1); + if (!response1.isSuccess()) { + throw new CardException("Reactivating failed!", response1.getSw()); + } + + CommandApdu reactivate2 = connection.getCommandFactory().createReactivate2Command(); + ResponseApdu response2 = connection.communicate(reactivate2); + if (!response2.isSuccess()) { + throw new CardException("Reactivating failed!", response2.getSw()); + } + + connection.refreshConnectionCapabilities(); + } +} 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 a158aed3a..f10cefe52 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -448,52 +448,6 @@ public class SecurityTokenConnection { return response.getData(); } - /** - * Resets security token, which deletes all keys and data objects. - * This works by entering a wrong PIN and then Admin PIN 4 times respectively. - * Afterwards, the token is reactivated. - */ - public void resetAndWipeToken() throws IOException { - // try wrong PIN 4 times until counter goes to C0 - byte[] pin = "XXXXXX".getBytes(); - for (int i = 0; i <= 4; i++) { - // Command APDU for VERIFY command (page 32) - ResponseApdu response = communicate(commandFactory.createVerifyPw1ForSignatureCommand(pin)); - if (response.isSuccess()) { - 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++) { - // Command APDU for VERIFY command (page 32) - ResponseApdu response = communicate(commandFactory.createVerifyPw3Command(adminPin)); - if (response.isSuccess()) { // Should NOT accept! - throw new CardException("Should never happen, XXXXXXXX has been accepted", response.getSw()); - } - } - - // secure messaging must be disabled before reactivation - clearSecureMessaging(); - - // 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 = commandFactory.createReactivate1Command(); - CommandApdu reactivate2 = commandFactory.createReactivate2Command(); - ResponseApdu response1 = communicate(reactivate1); - ResponseApdu response2 = communicate(reactivate2); - if (!response1.isSuccess()) { - throw new CardException("Reactivating failed!", response1.getSw()); - } - if (!response2.isSuccess()) { - throw new CardException("Reactivating failed!", response2.getSw()); - } - - refreshConnectionCapabilities(); - } - /** * Return the fingerprint from application specific data stored on tag, or * null if it doesn't exist. diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index 1d6642523..a40570538 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -45,6 +45,7 @@ import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; import org.sufficientlysecure.keychain.securitytoken.PsoDecryptUseCase; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenPsoSignUseCase; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenChangeKeyUseCase; +import org.sufficientlysecure.keychain.securitytoken.ResetAndWipeUseCase; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; @@ -306,7 +307,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { break; } case SECURITY_TOKEN_RESET_CARD: { - stConnection.resetAndWipeToken(); + ResetAndWipeUseCase.create(stConnection).resetAndWipeToken(); mResultTokenInfo = stConnection.getTokenInfo(); break; From 47319d22b9bb250132fc61e95103d9ae06cd4431 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 16:00:52 +0100 Subject: [PATCH 08/15] extract GenerateKeyUseCase --- .../securitytoken/GenerateKeyUseCase.java | 50 +++++++++++++++++++ .../SecurityTokenConnection.java | 33 ------------ 2 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/GenerateKeyUseCase.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/GenerateKeyUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/GenerateKeyUseCase.java new file mode 100644 index 000000000..d196a19f5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/GenerateKeyUseCase.java @@ -0,0 +1,50 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import java.io.IOException; + +import org.sufficientlysecure.keychain.util.Passphrase; + + +public class GenerateKeyUseCase { + private final SecurityTokenConnection connection; + + public static GenerateKeyUseCase create(SecurityTokenConnection connection) { + return new GenerateKeyUseCase(connection); + } + + private GenerateKeyUseCase(SecurityTokenConnection connection) { + this.connection = connection; + } + + /** + * Generates a key on the card in the given slot. If the slot is 0xB6 (the signature key), + * this command also has the effect of resetting the digital signature counter. + * NOTE: This does not set the key fingerprint data object! After calling this command, you + * must construct a public key packet using the returned public key data objects, compute the + * key fingerprint, and store it on the card using: putData(0xC8, key.getFingerprint()) + * + * @param slot The slot on the card where the key should be generated: + * 0xB6: Signature Key + * 0xB8: Decipherment Key + * 0xA4: Authentication Key + * @return the public key data objects, in TLV format. For RSA this will be the public modulus + * (0x81) and exponent (0x82). These may come out of order; proper TLV parsing is required. + */ + public byte[] generateKey(Passphrase adminPin, int slot) throws IOException { + if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) { + throw new IOException("Invalid key slot"); + } + + connection.verifyAdminPin(adminPin); + + CommandApdu apdu = connection.getCommandFactory().createGenerateKeyCommand(slot); + ResponseApdu response = connection.communicate(apdu); + + if (!response.isSuccess()) { + throw new IOException("On-card key generation failed"); + } + + return response.getData(); + } +} 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 f10cefe52..33fd48c04 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -415,39 +415,6 @@ public class SecurityTokenConnection { return lastResponse; } - /** - * Generates a key on the card in the given slot. If the slot is 0xB6 (the signature key), - * this command also has the effect of resetting the digital signature counter. - * NOTE: This does not set the key fingerprint data object! After calling this command, you - * must construct a public key packet using the returned public key data objects, compute the - * key fingerprint, and store it on the card using: putData(0xC8, key.getFingerprint()) - * - * @param slot The slot on the card where the key should be generated: - * 0xB6: Signature Key - * 0xB8: Decipherment Key - * 0xA4: Authentication Key - * @return the public key data objects, in TLV format. For RSA this will be the public modulus - * (0x81) and exponent (0x82). These may come out of order; proper TLV parsing is required. - */ - public byte[] generateKey(Passphrase adminPin, int slot) throws IOException { - if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) { - throw new IOException("Invalid key slot"); - } - - if (!isPw3Validated) { - verifyAdminPin(adminPin); - } - - CommandApdu apdu = commandFactory.createGenerateKeyCommand(slot); - ResponseApdu response = communicate(apdu); - - if (!response.isSuccess()) { - throw new IOException("On-card key generation failed"); - } - - return response.getData(); - } - /** * Return the fingerprint from application specific data stored on tag, or * null if it doesn't exist. From 626c08bbbe7b2522aa48facd23fad027c90b0f53 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 16:17:28 +0100 Subject: [PATCH 09/15] extract ModifyPinUseCase --- .../securitytoken/ModifyPinUseCase.java | 76 ++++++++++++++++ .../SecurityTokenConnection.java | 89 ++++--------------- .../SecurityTokenPsoSignUseCase.java | 2 +- ...curityTokenChangePinOperationActivity.java | 3 +- .../ui/SecurityTokenOperationActivity.java | 7 +- 5 files changed, 100 insertions(+), 77 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ModifyPinUseCase.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ModifyPinUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ModifyPinUseCase.java new file mode 100644 index 000000000..66cc5e306 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ModifyPinUseCase.java @@ -0,0 +1,76 @@ +package org.sufficientlysecure.keychain.securitytoken; + + +import java.io.IOException; + +import org.sufficientlysecure.keychain.util.Passphrase; + + +public class ModifyPinUseCase { + private static final int MAX_PW3_LENGTH_INDEX = 3; + private static final int MIN_PW3_LENGTH = 8; + + private final SecurityTokenConnection connection; + private final Passphrase adminPin; + + public static ModifyPinUseCase create(SecurityTokenConnection connection, Passphrase adminPin) { + return new ModifyPinUseCase(connection, adminPin); + } + + private ModifyPinUseCase(SecurityTokenConnection connection, + Passphrase adminPin) { + this.connection = connection; + this.adminPin = adminPin; + } + + public void modifyPw1andPw3Pins(byte[] newPin, byte[] newAdminPin) throws IOException { + // Order is important for Gnuk, otherwise it will be set up in "admin less mode". + // http://www.fsij.org/doc-gnuk/gnuk-passphrase-setting.html#set-up-pw1-pw3-and-reset-code + modifyPw3Pin(newAdminPin); + modifyPw1PinWithEffectiveAdminPin(new Passphrase(new String(newAdminPin)), newPin); + } + + public void modifyPw1Pin(byte[] newPin) throws IOException { + modifyPw1PinWithEffectiveAdminPin(adminPin, newPin); + } + + private void modifyPw1PinWithEffectiveAdminPin(Passphrase effectiveAdminPin, byte[] newPin) throws IOException { + connection.verifyAdminPin(effectiveAdminPin); + + final int MAX_PW1_LENGTH_INDEX = 1; + byte[] pwStatusBytes = connection.getPwStatusBytes(); + if (newPin.length < 6 || newPin.length > pwStatusBytes[MAX_PW1_LENGTH_INDEX]) { + throw new IOException("Invalid PIN length"); + } + + CommandApdu changePin = connection.getCommandFactory().createResetPw1Command(newPin); + ResponseApdu response = connection.communicate(changePin); + + if (!response.isSuccess()) { + throw new CardException("Failed to change PIN", response.getSw()); + } + } + + /** + * Modifies the user's PW3. Before sending, the new PIN will be validated for + * conformance to the token's requirements for key length. + */ + private void modifyPw3Pin(byte[] newAdminPin) throws IOException { + byte[] pwStatusBytes = connection.getPwStatusBytes(); + + if (newAdminPin.length < MIN_PW3_LENGTH || newAdminPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) { + throw new IOException("Invalid PIN length"); + } + + byte[] pin = adminPin.toStringUnsafe().getBytes(); + + CommandApdu changePin = connection.getCommandFactory().createChangePw3Command(pin, newAdminPin); + ResponseApdu response = connection.communicate(changePin); + + connection.invalidatePw3(); + + if (!response.isSuccess()) { + throw new CardException("Failed to change PIN", response.getSw()); + } + } +} 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 33fd48c04..26a68469b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -96,12 +96,6 @@ public class SecurityTokenConnection { return commandFactory; } - void maybeInvalidatePw1() { - if (!openPgpCapabilities.isPw1ValidForMultipleSignatures()) { - isPw1ValidatedForSignature = false; - } - } - private String getHolderName(byte[] name) { try { return (new String(name, 4, name[3])).replace('<', ' '); @@ -197,67 +191,18 @@ public class SecurityTokenConnection { this.cardCapabilities = new CardCapabilities(openPgpCapabilities.getHistoricalBytes()); } - public void resetPin(byte[] newPin, Passphrase adminPin) throws IOException { - if (!isPw3Validated) { - verifyAdminPin(adminPin); - } - - final int MAX_PW1_LENGTH_INDEX = 1; - byte[] pwStatusBytes = getPwStatusBytes(); - if (newPin.length < 6 || newPin.length > pwStatusBytes[MAX_PW1_LENGTH_INDEX]) { - throw new IOException("Invalid PIN length"); - } - - // Command APDU for RESET RETRY COUNTER command (page 33) - CommandApdu changePin = commandFactory.createResetPw1Command(newPin); - ResponseApdu response = communicate(changePin); - - if (!response.isSuccess()) { - throw new CardException("Failed to change PIN", response.getSw()); - } - } - - /** - * Modifies the user's PW3. Before sending, the new PIN will be validated for - * conformance to the token's requirements for key length. - * - * @param newAdminPin The new PW3. - */ - public void modifyPw3Pin(byte[] newAdminPin, Passphrase adminPin) throws IOException { - final int MAX_PW3_LENGTH_INDEX = 3; - - byte[] pwStatusBytes = getPwStatusBytes(); - - if (newAdminPin.length < 8 || newAdminPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) { - throw new IOException("Invalid PIN length"); - } - - byte[] pin = adminPin.toStringUnsafe().getBytes(); - - CommandApdu changePin = commandFactory.createChangePw3Command(pin, newAdminPin); - ResponseApdu response = communicate(changePin); - - isPw3Validated = false; - - if (!response.isSuccess()) { - throw new CardException("Failed to change PIN", response.getSw()); - } - } - - /** - * Verifies the user's PW1 with the appropriate mode. - */ void verifyPinForSignature() throws IOException { if (isPw1ValidatedForSignature) { return; } - if (cachedPin == null) { throw new IllegalStateException("Connection not initialized with Pin!"); } + byte[] pin = cachedPin.toStringUnsafe().getBytes(); - ResponseApdu response = communicate(commandFactory.createVerifyPw1ForSignatureCommand(pin)); + CommandApdu verifyPw1ForSignatureCommand = commandFactory.createVerifyPw1ForSignatureCommand(pin); + ResponseApdu response = communicate(verifyPw1ForSignatureCommand); if (!response.isSuccess()) { throw new CardException("Bad PIN!", response.getSw()); } @@ -265,9 +210,6 @@ public class SecurityTokenConnection { isPw1ValidatedForSignature = true; } - /** - * Verifies the user's PW1 with the appropriate mode. - */ void verifyPinForOther() throws IOException { if (isPw1ValidatedForOther) { return; @@ -278,8 +220,8 @@ public class SecurityTokenConnection { byte[] pin = cachedPin.toStringUnsafe().getBytes(); - // Command APDU for VERIFY command (page 32) - ResponseApdu response = communicate(commandFactory.createVerifyPw1ForOtherCommand(pin)); + CommandApdu verifyPw1ForOtherCommand = commandFactory.createVerifyPw1ForOtherCommand(pin); + ResponseApdu response = communicate(verifyPw1ForOtherCommand); if (!response.isSuccess()) { throw new CardException("Bad PIN!", response.getSw()); } @@ -287,16 +229,13 @@ public class SecurityTokenConnection { isPw1ValidatedForOther = true; } - /** - * Verifies the user's PW1 or PW3 with the appropriate mode. - */ void verifyAdminPin(Passphrase adminPin) throws IOException { if (isPw3Validated) { return; } - // Command APDU for VERIFY command (page 32) - ResponseApdu response = - communicate(commandFactory.createVerifyPw3Command(adminPin.toStringUnsafe().getBytes())); + + CommandApdu verifyPw3Command = commandFactory.createVerifyPw3Command(adminPin.toStringUnsafe().getBytes()); + ResponseApdu response = communicate(verifyPw3Command); if (!response.isSuccess()) { throw new CardException("Bad PIN!", response.getSw()); } @@ -304,6 +243,16 @@ public class SecurityTokenConnection { isPw3Validated = true; } + void invalidateSingleUsePw1() { + if (!openPgpCapabilities.isPw1ValidForMultipleSignatures()) { + isPw1ValidatedForSignature = false; + } + } + + void invalidatePw3() { + isPw3Validated = false; + } + /** * Return fingerprints of all keys from application specific data stored * on tag, or null if data not available. @@ -319,7 +268,7 @@ public class SecurityTokenConnection { * * @return Seven bytes in fixed format, plus 0x9000 status word at the end. */ - private byte[] getPwStatusBytes() throws IOException { + byte[] getPwStatusBytes() throws IOException { return openPgpCapabilities.getPwStatusBytes(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java index 2ce483422..78e6811aa 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java @@ -144,7 +144,7 @@ public class SecurityTokenPsoSignUseCase { CommandApdu command = connection.getCommandFactory().createComputeDigitalSignatureCommand(data); ResponseApdu response = connection.communicate(command); - connection.maybeInvalidatePw1(); + connection.invalidateSingleUsePw1(); if (!response.isSuccess()) { throw new CardException("Failed to sign", response.getSw()); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java index 349676e80..2bad186e2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java @@ -32,6 +32,7 @@ import android.widget.ViewAnimator; import nordpol.android.NfcGuideView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.securitytoken.ModifyPinUseCase; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; import org.sufficientlysecure.keychain.service.input.SecurityTokenChangePinParcel; @@ -141,7 +142,7 @@ public class SecurityTokenChangePinOperationActivity extends BaseSecurityTokenAc @Override protected void doSecurityTokenInBackground(SecurityTokenConnection stConnection) throws IOException { Passphrase adminPin = new Passphrase(changePinInput.getAdminPin()); - stConnection.resetPin(changePinInput.getNewPin().getBytes(), adminPin); + ModifyPinUseCase.create(stConnection, adminPin).modifyPw1Pin(changePinInput.getNewPin().getBytes()); resultTokenInfo = stConnection.getTokenInfo(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index a40570538..68f472ebf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -40,6 +40,7 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.provider.KeyRepository; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.securitytoken.KeyType; +import org.sufficientlysecure.keychain.securitytoken.ModifyPinUseCase; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; import org.sufficientlysecure.keychain.securitytoken.PsoDecryptUseCase; @@ -296,11 +297,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { mInputParcel = mInputParcel.withCryptoData(subkeyBytes, tokenSerialNumber); } - // First set Admin PIN, then PIN. - // Order is important for Gnuk, otherwise it will be set up in "admin less mode". - // http://www.fsij.org/doc-gnuk/gnuk-passphrase-setting.html#set-up-pw1-pw3-and-reset-code - stConnection.modifyPw3Pin(newAdminPin, adminPin); - stConnection.resetPin(newPin, new Passphrase(new String(newAdminPin))); + ModifyPinUseCase.create(stConnection, adminPin).modifyPw1andPw3Pins(newPin, newAdminPin); SecurityTokenConnection.clearCachedConnections(); From c00eb7b7f32b467d25b17897d63f8eb29a566cc1 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 16:49:27 +0100 Subject: [PATCH 10/15] Further refactor SecurityTokenConnection --- .../securitytoken/SCP11bSecureMessaging.java | 7 +- .../SecurityTokenConnection.java | 362 ++++++++++-------- 2 files changed, 199 insertions(+), 170 deletions(-) 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 ""; + } } } From 401b90a493aa4562741e600f651a975a4f4ce877 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 18:50:45 +0100 Subject: [PATCH 11/15] rename UseCases to TokenOps, and move to operations package --- .../keychain/securitytoken/KeyFormat.java | 4 +- .../securitytoken/OpenPgpCapabilities.java | 10 +- .../OpenPgpCommandApduFactory.java | 120 +++++++++--------- .../SecurityTokenConnection.java | 20 +-- .../securitytoken/SecurityTokenUtils.java | 9 +- .../GenerateKeyTokenOp.java} | 13 +- .../ModifyPinTokenOp.java} | 14 +- .../PsoDecryptTokenOp.java} | 17 ++- .../ResetAndWipeTokenOp.java} | 15 ++- .../SecurityTokenChangeKeyTokenOp.java} | 20 ++- .../SecurityTokenPsoSignTokenOp.java} | 18 ++- ...curityTokenChangePinOperationActivity.java | 4 +- .../ui/SecurityTokenOperationActivity.java | 24 ++-- ...seTest.java => PsoDecryptTokenOpTest.java} | 7 +- .../SecurityTokenChangeKeyTokenOpTest.java} | 28 ++-- 15 files changed, 178 insertions(+), 145 deletions(-) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{GenerateKeyUseCase.java => operations/GenerateKeyTokenOp.java} (77%) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{ModifyPinUseCase.java => operations/ModifyPinTokenOp.java} (82%) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{PsoDecryptUseCase.java => operations/PsoDecryptTokenOp.java} (90%) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{ResetAndWipeUseCase.java => operations/ResetAndWipeTokenOp.java} (82%) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{SecurityTokenChangeKeyUseCase.java => operations/SecurityTokenChangeKeyTokenOp.java} (87%) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/{SecurityTokenPsoSignUseCase.java => operations/SecurityTokenPsoSignTokenOp.java} (89%) rename OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/{PsoDecryptUseCaseTest.java => PsoDecryptTokenOpTest.java} (93%) rename OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/{SecurityTokenChangeKeyUseCaseTest.java => operations/SecurityTokenChangeKeyTokenOpTest.java} (90%) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyFormat.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyFormat.java index de7e1a97a..a1b00397a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyFormat.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyFormat.java @@ -25,7 +25,7 @@ import org.sufficientlysecure.keychain.ui.CreateSecurityTokenAlgorithmFragment; public abstract class KeyFormat { - enum KeyFormatType { + public enum KeyFormatType { RSAKeyFormatType, ECKeyFormatType } @@ -36,7 +36,7 @@ public abstract class KeyFormat { mKeyFormatType = keyFormatType; } - final KeyFormatType keyFormatType() { + public final KeyFormatType keyFormatType() { return mKeyFormatType; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java index 85adf56e1..761799bcd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java @@ -22,7 +22,7 @@ import java.util.HashMap; import java.util.Map; @SuppressWarnings("unused") // just expose all included data -class OpenPgpCapabilities { +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; @@ -42,12 +42,12 @@ class OpenPgpCapabilities { private byte[] mFingerprints; private byte[] mPwStatusBytes; - OpenPgpCapabilities(byte[] data) throws IOException { + public OpenPgpCapabilities(byte[] data) throws IOException { mKeyFormats = new HashMap<>(); updateWithData(data); } - void updateWithData(byte[] data) throws IOException { + private void updateWithData(byte[] data) throws IOException { Iso7816TLV[] tlvs = Iso7816TLV.readList(data, true); if (tlvs.length == 1 && tlvs[0].mT == 0x6E) { tlvs = ((Iso7816TLV.Iso7816CompositeTLV) tlvs[0]).mSubs; @@ -142,7 +142,7 @@ class OpenPgpCapabilities { return mHasSM; } - boolean isAttributesChangable() { + public boolean isAttributesChangable() { return mAttriburesChangable; } @@ -166,7 +166,7 @@ class OpenPgpCapabilities { return mMaxRspLen; } - KeyFormat getFormatForKeyType(KeyType keyType) { + public KeyFormat getFormatForKeyType(KeyType keyType) { return mKeyFormats.get(keyType); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCommandApduFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCommandApduFactory.java index fee4db509..5fa4f93e9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCommandApduFactory.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCommandApduFactory.java @@ -27,7 +27,7 @@ import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; -class OpenPgpCommandApduFactory { +public class OpenPgpCommandApduFactory { private static final int MAX_APDU_NC = 255; private static final int MAX_APDU_NC_EXT = 65535; @@ -90,66 +90,11 @@ class OpenPgpCommandApduFactory { private static final int P1_EMPTY = 0x00; private static final int P2_EMPTY = 0x00; - @NonNull - CommandApdu createPutDataCommand(int dataObject, byte[] data) { - return CommandApdu.create(CLA, INS_PUT_DATA, (dataObject & 0xFF00) >> 8, dataObject & 0xFF, data); - } - - @NonNull - CommandApdu createPutKeyCommand(byte[] keyBytes) { - // the odd PUT DATA INS is for compliance with ISO 7816-8. This is used only to put key data on the card - return CommandApdu.create(CLA, INS_PUT_DATA_ODD, P1_PUT_DATA_ODD_KEY, P2_PUT_DATA_ODD_KEY, keyBytes); - } - - @NonNull - CommandApdu createComputeDigitalSignatureCommand(byte[] data) { - return CommandApdu.create(CLA, INS_PERFORM_SECURITY_OPERATION, P1_PSO_COMPUTE_DIGITAL_SIGNATURE, - P2_PSO_COMPUTE_DIGITAL_SIGNATURE, data, MAX_APDU_NE_EXT); - } - - @NonNull - CommandApdu createDecipherCommand(byte[] data) { - return CommandApdu.create(CLA, INS_PERFORM_SECURITY_OPERATION, P1_PSO_DECIPHER, P2_PSO_DECIPHER, data, - MAX_APDU_NE_EXT); - } - - @NonNull - CommandApdu createChangePw3Command(byte[] adminPin, byte[] newAdminPin) { - return CommandApdu.create(CLA, INS_CHANGE_REFERENCE_DATA, P1_EMPTY, - P2_CHANGE_REFERENCE_DATA_PW3, Arrays.concatenate(adminPin, newAdminPin)); - } - - @NonNull - CommandApdu createResetPw1Command(byte[] newPin) { - return CommandApdu.create(CLA, INS_RESET_RETRY_COUNTER, P1_RESET_RETRY_COUNTER_NEW_PW, - P2_RESET_RETRY_COUNTER, newPin); - } - - @NonNull - CommandApdu createGetDataCommand(int p1, int p2) { - return CommandApdu.create(CLA, INS_GET_DATA, p1, p2, MAX_APDU_NE_EXT); - } - - @NonNull - CommandApdu createGetResponseCommand(int lastResponseSw2) { - return CommandApdu.create(CLA, INS_GET_RESPONSE, P1_EMPTY, P2_EMPTY, lastResponseSw2); - } - - @NonNull - CommandApdu createVerifyPw1ForSignatureCommand(byte[] pin) { - return CommandApdu.create(CLA, INS_VERIFY, P1_EMPTY, P2_VERIFY_PW1_SIGN, pin); - } - @NonNull CommandApdu createVerifyPw1ForOtherCommand(byte[] pin) { return CommandApdu.create(CLA, INS_VERIFY, P1_EMPTY, P2_VERIFY_PW1_OTHER, pin); } - @NonNull - CommandApdu createVerifyPw3Command(byte[] pin) { - return CommandApdu.create(CLA, INS_VERIFY, P1_EMPTY, P2_VERIFY_PW3, pin); - } - @NonNull CommandApdu createSelectFileOpenPgpCommand() { return CommandApdu.create(CLA, INS_SELECT_FILE, P1_SELECT_FILE, P2_EMPTY, AID_SELECT_FILE_OPENPGP); @@ -161,12 +106,67 @@ class OpenPgpCommandApduFactory { } @NonNull - CommandApdu createReactivate1Command() { + CommandApdu createGetDataCommand(int p1, int p2) { + return CommandApdu.create(CLA, INS_GET_DATA, p1, p2, MAX_APDU_NE_EXT); + } + + @NonNull + CommandApdu createGetResponseCommand(int lastResponseSw2) { + return CommandApdu.create(CLA, INS_GET_RESPONSE, P1_EMPTY, P2_EMPTY, lastResponseSw2); + } + + @NonNull + public CommandApdu createPutDataCommand(int dataObject, byte[] data) { + return CommandApdu.create(CLA, INS_PUT_DATA, (dataObject & 0xFF00) >> 8, dataObject & 0xFF, data); + } + + @NonNull + public CommandApdu createPutKeyCommand(byte[] keyBytes) { + // the odd PUT DATA INS is for compliance with ISO 7816-8. This is used only to put key data on the card + return CommandApdu.create(CLA, INS_PUT_DATA_ODD, P1_PUT_DATA_ODD_KEY, P2_PUT_DATA_ODD_KEY, keyBytes); + } + + @NonNull + public CommandApdu createComputeDigitalSignatureCommand(byte[] data) { + return CommandApdu.create(CLA, INS_PERFORM_SECURITY_OPERATION, P1_PSO_COMPUTE_DIGITAL_SIGNATURE, + P2_PSO_COMPUTE_DIGITAL_SIGNATURE, data, MAX_APDU_NE_EXT); + } + + @NonNull + public CommandApdu createDecipherCommand(byte[] data) { + return CommandApdu.create(CLA, INS_PERFORM_SECURITY_OPERATION, P1_PSO_DECIPHER, P2_PSO_DECIPHER, data, + MAX_APDU_NE_EXT); + } + + @NonNull + public CommandApdu createChangePw3Command(byte[] adminPin, byte[] newAdminPin) { + return CommandApdu.create(CLA, INS_CHANGE_REFERENCE_DATA, P1_EMPTY, + P2_CHANGE_REFERENCE_DATA_PW3, Arrays.concatenate(adminPin, newAdminPin)); + } + + @NonNull + public CommandApdu createResetPw1Command(byte[] newPin) { + return CommandApdu.create(CLA, INS_RESET_RETRY_COUNTER, P1_RESET_RETRY_COUNTER_NEW_PW, + P2_RESET_RETRY_COUNTER, newPin); + } + + @NonNull + public CommandApdu createVerifyPw1ForSignatureCommand(byte[] pin) { + return CommandApdu.create(CLA, INS_VERIFY, P1_EMPTY, P2_VERIFY_PW1_SIGN, pin); + } + + @NonNull + public CommandApdu createVerifyPw3Command(byte[] pin) { + return CommandApdu.create(CLA, INS_VERIFY, P1_EMPTY, P2_VERIFY_PW3, pin); + } + + @NonNull + public CommandApdu createReactivate1Command() { return CommandApdu.create(CLA, INS_TERMINATE_DF, P1_EMPTY, P2_EMPTY); } @NonNull - CommandApdu createReactivate2Command() { + public CommandApdu createReactivate2Command() { return CommandApdu.create(CLA, INS_ACTIVATE_FILE, P1_EMPTY, P2_EMPTY); } @@ -177,12 +177,12 @@ class OpenPgpCommandApduFactory { } @NonNull - CommandApdu createInternalAuthCommand(byte[] authData) { + public CommandApdu createInternalAuthCommand(byte[] authData) { return CommandApdu.create(CLA, INS_INTERNAL_AUTHENTICATE, P1_EMPTY, P2_EMPTY, authData, MAX_APDU_NE_EXT); } @NonNull - CommandApdu createGenerateKeyCommand(int slot) { + public CommandApdu createGenerateKeyCommand(int slot) { return CommandApdu.create(CLA, INS_GENERATE_ASYMMETRIC_KEY_PAIR, P1_GAKP_GENERATE, P2_EMPTY, new byte[] { (byte) slot, 0x00 }, MAX_APDU_NE_EXT); } 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 78a5aa883..a645263be 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -152,7 +152,7 @@ public class SecurityTokenConnection { tokenType = TokenType.UNKNOWN; } - void refreshConnectionCapabilities() throws IOException { + public void refreshConnectionCapabilities() throws IOException { byte[] rawOpenPgpCapabilities = getData(0x00, 0x6E); OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities(rawOpenPgpCapabilities); @@ -177,7 +177,7 @@ public class SecurityTokenConnection { * @param commandApdu short or extended APDU to transceive * @return response from the card */ - ResponseApdu communicate(CommandApdu commandApdu) throws IOException { + public ResponseApdu communicate(CommandApdu commandApdu) throws IOException { commandApdu = smEncryptIfAvailable(commandApdu); ResponseApdu lastResponse; @@ -296,7 +296,7 @@ public class SecurityTokenConnection { // region pin management - void verifyPinForSignature() throws IOException { + public void verifyPinForSignature() throws IOException { if (isPw1ValidatedForSignature) { return; } @@ -315,7 +315,7 @@ public class SecurityTokenConnection { isPw1ValidatedForSignature = true; } - void verifyPinForOther() throws IOException { + public void verifyPinForOther() throws IOException { if (isPw1ValidatedForOther) { return; } @@ -334,7 +334,7 @@ public class SecurityTokenConnection { isPw1ValidatedForOther = true; } - void verifyAdminPin(Passphrase adminPin) throws IOException { + public void verifyAdminPin(Passphrase adminPin) throws IOException { if (isPw3Validated) { return; } @@ -348,13 +348,13 @@ public class SecurityTokenConnection { isPw3Validated = true; } - void invalidateSingleUsePw1() { + public void invalidateSingleUsePw1() { if (!openPgpCapabilities.isPw1ValidForMultipleSignatures()) { isPw1ValidatedForSignature = false; } } - void invalidatePw3() { + public void invalidatePw3() { isPw3Validated = false; } @@ -415,15 +415,15 @@ public class SecurityTokenConnection { return tokenType; } - OpenPgpCapabilities getOpenPgpCapabilities() { + public OpenPgpCapabilities getOpenPgpCapabilities() { return openPgpCapabilities; } - OpenPgpCommandApduFactory getCommandFactory() { + public OpenPgpCommandApduFactory getCommandFactory() { return commandFactory; } - byte[] getPwStatusBytes() { + public byte[] getPwStatusBytes() { return openPgpCapabilities.getPwStatusBytes(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtils.java index 165238088..108dbc7cf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtils.java @@ -32,8 +32,9 @@ import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateCrtKey; -class SecurityTokenUtils { - static byte[] attributesFromSecretKey(KeyType slot, CanonicalizedSecretKey secretKey, KeyFormat formatForKeyType) +public class SecurityTokenUtils { + public static byte[] attributesFromSecretKey(KeyType slot, CanonicalizedSecretKey secretKey, + KeyFormat formatForKeyType) throws IOException { if (secretKey.isRSA()) { return attributesForRsaKey(secretKey.getBitStrength(), (RSAKeyFormat) formatForKeyType); @@ -73,7 +74,7 @@ class SecurityTokenUtils { return attrs; } - static byte[] createRSAPrivKeyTemplate(RSAPrivateCrtKey secretKey, KeyType slot, + public static byte[] createRSAPrivKeyTemplate(RSAPrivateCrtKey secretKey, KeyType slot, RSAKeyFormat format) throws IOException { ByteArrayOutputStream stream = new ByteArrayOutputStream(), template = new ByteArrayOutputStream(), @@ -141,7 +142,7 @@ class SecurityTokenUtils { return res.toByteArray(); } - static byte[] createECPrivKeyTemplate(ECPrivateKey secretKey, ECPublicKey publicKey, KeyType slot, + public static byte[] createECPrivKeyTemplate(ECPrivateKey secretKey, ECPublicKey publicKey, KeyType slot, ECKeyFormat format) throws IOException { ByteArrayOutputStream stream = new ByteArrayOutputStream(), template = new ByteArrayOutputStream(), diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/GenerateKeyUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/GenerateKeyTokenOp.java similarity index 77% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/GenerateKeyUseCase.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/GenerateKeyTokenOp.java index d196a19f5..0cd566db4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/GenerateKeyUseCase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/GenerateKeyTokenOp.java @@ -1,19 +1,22 @@ -package org.sufficientlysecure.keychain.securitytoken; +package org.sufficientlysecure.keychain.securitytoken.operations; import java.io.IOException; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.util.Passphrase; -public class GenerateKeyUseCase { +public class GenerateKeyTokenOp { private final SecurityTokenConnection connection; - public static GenerateKeyUseCase create(SecurityTokenConnection connection) { - return new GenerateKeyUseCase(connection); + public static GenerateKeyTokenOp create(SecurityTokenConnection connection) { + return new GenerateKeyTokenOp(connection); } - private GenerateKeyUseCase(SecurityTokenConnection connection) { + private GenerateKeyTokenOp(SecurityTokenConnection connection) { this.connection = connection; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ModifyPinUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java similarity index 82% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ModifyPinUseCase.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java index 66cc5e306..c8818dec3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ModifyPinUseCase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java @@ -1,23 +1,27 @@ -package org.sufficientlysecure.keychain.securitytoken; +package org.sufficientlysecure.keychain.securitytoken.operations; import java.io.IOException; +import org.sufficientlysecure.keychain.securitytoken.CardException; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.util.Passphrase; -public class ModifyPinUseCase { +public class ModifyPinTokenOp { private static final int MAX_PW3_LENGTH_INDEX = 3; private static final int MIN_PW3_LENGTH = 8; private final SecurityTokenConnection connection; private final Passphrase adminPin; - public static ModifyPinUseCase create(SecurityTokenConnection connection, Passphrase adminPin) { - return new ModifyPinUseCase(connection, adminPin); + public static ModifyPinTokenOp create(SecurityTokenConnection connection, Passphrase adminPin) { + return new ModifyPinTokenOp(connection, adminPin); } - private ModifyPinUseCase(SecurityTokenConnection connection, + private ModifyPinTokenOp(SecurityTokenConnection connection, Passphrase adminPin) { this.connection = connection; this.adminPin = adminPin; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java similarity index 90% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java index 45d54b063..130ffe7e5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java @@ -1,4 +1,4 @@ -package org.sufficientlysecure.keychain.securitytoken; +package org.sufficientlysecure.keychain.securitytoken.operations; import java.io.IOException; @@ -22,21 +22,28 @@ import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; +import org.sufficientlysecure.keychain.securitytoken.CardException; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.ECKeyFormat; +import org.sufficientlysecure.keychain.securitytoken.KeyFormat; +import org.sufficientlysecure.keychain.securitytoken.KeyType; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; /** This class implements the PSO:DECIPHER operation, as specified in OpenPGP card spec / 7.2.11 (p52 in v3.0.1). * * See https://www.g10code.com/docs/openpgp-card-3.0.pdf */ -public class PsoDecryptUseCase { +public class PsoDecryptTokenOp { private final SecurityTokenConnection connection; private final JcaKeyFingerprintCalculator fingerprintCalculator; - public static PsoDecryptUseCase create(SecurityTokenConnection connection) { - return new PsoDecryptUseCase(connection, new JcaKeyFingerprintCalculator()); + public static PsoDecryptTokenOp create(SecurityTokenConnection connection) { + return new PsoDecryptTokenOp(connection, new JcaKeyFingerprintCalculator()); } - private PsoDecryptUseCase(SecurityTokenConnection connection, + private PsoDecryptTokenOp(SecurityTokenConnection connection, JcaKeyFingerprintCalculator jcaKeyFingerprintCalculator) { this.connection = connection; this.fingerprintCalculator = jcaKeyFingerprintCalculator; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ResetAndWipeUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java similarity index 82% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ResetAndWipeUseCase.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java index 84df31b53..2befafe0d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/ResetAndWipeUseCase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java @@ -1,17 +1,22 @@ -package org.sufficientlysecure.keychain.securitytoken; +package org.sufficientlysecure.keychain.securitytoken.operations; import java.io.IOException; +import org.sufficientlysecure.keychain.securitytoken.CardException; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; -public class ResetAndWipeUseCase { + +public class ResetAndWipeTokenOp { private final SecurityTokenConnection connection; - public static ResetAndWipeUseCase create(SecurityTokenConnection connection) { - return new ResetAndWipeUseCase(connection); + public static ResetAndWipeTokenOp create(SecurityTokenConnection connection) { + return new ResetAndWipeTokenOp(connection); } - private ResetAndWipeUseCase(SecurityTokenConnection connection) { + private ResetAndWipeTokenOp(SecurityTokenConnection connection) { this.connection = connection; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java similarity index 87% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCase.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java index d9866188a..9394bcbcb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java @@ -1,4 +1,4 @@ -package org.sufficientlysecure.keychain.securitytoken; +package org.sufficientlysecure.keychain.securitytoken.operations; import java.io.IOException; @@ -12,19 +12,29 @@ import android.support.annotation.VisibleForTesting; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.securitytoken.CardException; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.ECKeyFormat; +import org.sufficientlysecure.keychain.securitytoken.KeyFormat; +import org.sufficientlysecure.keychain.securitytoken.KeyType; +import org.sufficientlysecure.keychain.securitytoken.OpenPgpCapabilities; +import org.sufficientlysecure.keychain.securitytoken.RSAKeyFormat; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenUtils; import org.sufficientlysecure.keychain.util.Passphrase; -public class SecurityTokenChangeKeyUseCase { +public class SecurityTokenChangeKeyTokenOp { 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 final SecurityTokenConnection connection; - public static SecurityTokenChangeKeyUseCase create(SecurityTokenConnection stConnection) { - return new SecurityTokenChangeKeyUseCase(stConnection); + public static SecurityTokenChangeKeyTokenOp create(SecurityTokenConnection stConnection) { + return new SecurityTokenChangeKeyTokenOp(stConnection); } - private SecurityTokenChangeKeyUseCase(SecurityTokenConnection connection) { + private SecurityTokenChangeKeyTokenOp(SecurityTokenConnection connection) { this.connection = connection; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java similarity index 89% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java index 78e6811aa..83fa49fd5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenPsoSignUseCase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java @@ -1,4 +1,4 @@ -package org.sufficientlysecure.keychain.securitytoken; +package org.sufficientlysecure.keychain.securitytoken.operations; import java.io.ByteArrayOutputStream; @@ -12,17 +12,25 @@ import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.securitytoken.CardException; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.KeyFormat; +import org.sufficientlysecure.keychain.securitytoken.KeyType; +import org.sufficientlysecure.keychain.securitytoken.OpenPgpCapabilities; +import org.sufficientlysecure.keychain.securitytoken.RSAKeyFormat; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.util.Log; -public class SecurityTokenPsoSignUseCase { +public class SecurityTokenPsoSignTokenOp { private final SecurityTokenConnection connection; - public static SecurityTokenPsoSignUseCase create(SecurityTokenConnection connection) { - return new SecurityTokenPsoSignUseCase(connection); + public static SecurityTokenPsoSignTokenOp create(SecurityTokenConnection connection) { + return new SecurityTokenPsoSignTokenOp(connection); } - private SecurityTokenPsoSignUseCase(SecurityTokenConnection connection) { + private SecurityTokenPsoSignTokenOp(SecurityTokenConnection connection) { this.connection = connection; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java index 2bad186e2..6b2c427bf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java @@ -32,7 +32,7 @@ import android.widget.ViewAnimator; import nordpol.android.NfcGuideView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.securitytoken.ModifyPinUseCase; +import org.sufficientlysecure.keychain.securitytoken.operations.ModifyPinTokenOp; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; import org.sufficientlysecure.keychain.service.input.SecurityTokenChangePinParcel; @@ -142,7 +142,7 @@ public class SecurityTokenChangePinOperationActivity extends BaseSecurityTokenAc @Override protected void doSecurityTokenInBackground(SecurityTokenConnection stConnection) throws IOException { Passphrase adminPin = new Passphrase(changePinInput.getAdminPin()); - ModifyPinUseCase.create(stConnection, adminPin).modifyPw1Pin(changePinInput.getNewPin().getBytes()); + ModifyPinTokenOp.create(stConnection, adminPin).modifyPw1Pin(changePinInput.getNewPin().getBytes()); resultTokenInfo = stConnection.getTokenInfo(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index 68f472ebf..df450c3c3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -40,13 +40,13 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.provider.KeyRepository; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.securitytoken.KeyType; -import org.sufficientlysecure.keychain.securitytoken.ModifyPinUseCase; +import org.sufficientlysecure.keychain.securitytoken.operations.ModifyPinTokenOp; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; -import org.sufficientlysecure.keychain.securitytoken.PsoDecryptUseCase; -import org.sufficientlysecure.keychain.securitytoken.SecurityTokenPsoSignUseCase; -import org.sufficientlysecure.keychain.securitytoken.SecurityTokenChangeKeyUseCase; -import org.sufficientlysecure.keychain.securitytoken.ResetAndWipeUseCase; +import org.sufficientlysecure.keychain.securitytoken.operations.PsoDecryptTokenOp; +import org.sufficientlysecure.keychain.securitytoken.operations.SecurityTokenPsoSignTokenOp; +import org.sufficientlysecure.keychain.securitytoken.operations.SecurityTokenChangeKeyTokenOp; +import org.sufficientlysecure.keychain.securitytoken.operations.ResetAndWipeTokenOp; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; @@ -210,10 +210,10 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { throw new IOException("Couldn't find subkey for key to token operation."); } - PsoDecryptUseCase psoDecryptUseCase = PsoDecryptUseCase.create(stConnection); + PsoDecryptTokenOp psoDecryptTokenOp = PsoDecryptTokenOp.create(stConnection); for (int i = 0; i < mRequiredInput.mInputData.length; i++) { byte[] encryptedSessionKey = mRequiredInput.mInputData[i]; - byte[] decryptedSessionKey = psoDecryptUseCase + byte[] decryptedSessionKey = psoDecryptTokenOp .verifyAndDecryptSessionKey(encryptedSessionKey, publicKeyRing.getPublicKey(tokenKeyId)); mInputParcel = mInputParcel.withCryptoData(encryptedSessionKey, decryptedSessionKey); } @@ -229,7 +229,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { mInputParcel = mInputParcel.withSignatureTime(mRequiredInput.mSignatureTime); - SecurityTokenPsoSignUseCase psoSignUseCase = SecurityTokenPsoSignUseCase.create(stConnection); + SecurityTokenPsoSignTokenOp psoSignUseCase = SecurityTokenPsoSignTokenOp.create(stConnection); for (int i = 0; i < mRequiredInput.mInputData.length; i++) { byte[] hash = mRequiredInput.mInputData[i]; int algo = mRequiredInput.mSignAlgos[i]; @@ -246,7 +246,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { throw new IOException(getString(R.string.error_wrong_security_token)); } - SecurityTokenPsoSignUseCase psoSignUseCase = SecurityTokenPsoSignUseCase.create(stConnection); + SecurityTokenPsoSignTokenOp psoSignUseCase = SecurityTokenPsoSignTokenOp.create(stConnection); for (int i = 0; i < mRequiredInput.mInputData.length; i++) { byte[] hash = mRequiredInput.mInputData[i]; int algo = mRequiredInput.mSignAlgos[i]; @@ -290,21 +290,21 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { throw new IOException("Unable to get cached passphrase!"); } - SecurityTokenChangeKeyUseCase putKeyUseCase = SecurityTokenChangeKeyUseCase.create(stConnection); + SecurityTokenChangeKeyTokenOp putKeyUseCase = SecurityTokenChangeKeyTokenOp.create(stConnection); putKeyUseCase.changeKey(key, passphrase, adminPin); // TODO: Is this really used anywhere? mInputParcel = mInputParcel.withCryptoData(subkeyBytes, tokenSerialNumber); } - ModifyPinUseCase.create(stConnection, adminPin).modifyPw1andPw3Pins(newPin, newAdminPin); + ModifyPinTokenOp.create(stConnection, adminPin).modifyPw1andPw3Pins(newPin, newAdminPin); SecurityTokenConnection.clearCachedConnections(); break; } case SECURITY_TOKEN_RESET_CARD: { - ResetAndWipeUseCase.create(stConnection).resetAndWipeToken(); + ResetAndWipeTokenOp.create(stConnection).resetAndWipeToken(); mResultTokenInfo = stConnection.getTokenInfo(); break; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java similarity index 93% rename from OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java rename to OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java index a41ebf305..1b4c42dfc 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptUseCaseTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java @@ -6,6 +6,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.securitytoken.operations.PsoDecryptTokenOp; import static org.junit.Assert.*; import static org.mockito.Matchers.any; @@ -14,7 +15,7 @@ import static org.mockito.Mockito.when; @RunWith(KeychainTestRunner.class) -public class PsoDecryptUseCaseTest { +public class PsoDecryptTokenOpTest { private static final byte[] RSA_ENC_SESSIONKEY_MPI = Hex.decode( "07ff7b9ff36f70da1fe7a6b59168c24a7e5b48a938c4f970de46524a06ebf4a9175a9737cf2e6f30957110b31db7" + "0e9a2992401b1d5e99389f976356f4e3a28ff537362e7ce14b81200e21d4f0e77d46bd89f3a54ca06062289148a5938748" + @@ -24,7 +25,7 @@ public class PsoDecryptUseCaseTest { "51265229bcbb0da5d5aeb4eafbad9779"); private SecurityTokenConnection securityTokenConnection; private OpenPgpCommandApduFactory commandFactory; - private PsoDecryptUseCase useCase; + private PsoDecryptTokenOp useCase; private CommandApdu dummyCommandApdu = mock(CommandApdu.class); @@ -35,7 +36,7 @@ public class PsoDecryptUseCaseTest { commandFactory = mock(OpenPgpCommandApduFactory.class); when(securityTokenConnection.getCommandFactory()).thenReturn(commandFactory); - useCase = PsoDecryptUseCase.create(securityTokenConnection); + useCase = PsoDecryptTokenOp.create(securityTokenConnection); } @Test diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCaseTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java similarity index 90% rename from OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCaseTest.java rename to OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java index 4dd1f9bb0..a45f588f6 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenChangeKeyUseCaseTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java @@ -1,31 +1,25 @@ -package org.sufficientlysecure.keychain.securitytoken; +package org.sufficientlysecure.keychain.securitytoken.operations; -import java.io.IOException; -import java.util.LinkedList; - import org.bouncycastle.util.encoders.Hex; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.AdditionalMatchers; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowLog; import org.sufficientlysecure.keychain.KeychainTestRunner; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType; -import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.KeyType; +import org.sufficientlysecure.keychain.securitytoken.OpenPgpCapabilities; +import org.sufficientlysecure.keychain.securitytoken.OpenPgpCommandApduFactory; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.util.Passphrase; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,8 +27,8 @@ import static org.mockito.Mockito.when; @SuppressWarnings("WeakerAccess") @RunWith(KeychainTestRunner.class) -public class SecurityTokenChangeKeyUseCaseTest { - SecurityTokenChangeKeyUseCase useCase; +public class SecurityTokenChangeKeyTokenOpTest { + SecurityTokenChangeKeyTokenOp useCase; OpenPgpCommandApduFactory commandFactory; SecurityTokenConnection securityTokenConnection; @@ -49,7 +43,7 @@ public class SecurityTokenChangeKeyUseCaseTest { commandFactory = mock(OpenPgpCommandApduFactory.class); when(securityTokenConnection.getCommandFactory()).thenReturn(commandFactory); - useCase = SecurityTokenChangeKeyUseCase.create(securityTokenConnection); + useCase = SecurityTokenChangeKeyTokenOp.create(securityTokenConnection); } @@ -128,6 +122,6 @@ public class SecurityTokenChangeKeyUseCaseTest { } UncachedKeyRing readRingFromResource(String name) throws Exception { - return UncachedKeyRing.fromStream(SecurityTokenChangeKeyUseCaseTest.class.getResourceAsStream(name)).next(); + return UncachedKeyRing.fromStream(SecurityTokenChangeKeyTokenOpTest.class.getResourceAsStream(name)).next(); } } \ No newline at end of file From abf5e5d170895e416963354afaf7a3ae9254d88f Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 20:41:01 +0100 Subject: [PATCH 12/15] refactor OpenPgpCapabilities to use AutoValue --- .../securitytoken/OpenPgpCapabilities.java | 310 +++++++++++------- .../SecurityTokenConnection.java | 43 +-- .../operations/ModifyPinTokenOp.java | 10 +- .../operations/PsoDecryptTokenOp.java | 2 +- .../SecurityTokenChangeKeyTokenOp.java | 8 +- .../SecurityTokenPsoSignTokenOp.java | 4 +- .../ui/SecurityTokenOperationActivity.java | 8 +- .../keychain/ui/util/KeyFormattingUtils.java | 10 - .../securitytoken/PsoDecryptTokenOpTest.java | 2 +- .../SecurityTokenConnectionTest.java | 3 +- .../securitytoken/SecurityTokenUtilsTest.java | 2 +- .../SecurityTokenChangeKeyTokenOpTest.java | 2 +- 12 files changed, 215 insertions(+), 189 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java index 761799bcd..064091b62 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java @@ -18,159 +18,223 @@ package org.sufficientlysecure.keychain.securitytoken; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import java.nio.ByteBuffer; + +import android.support.annotation.NonNull; + +import com.google.auto.value.AutoValue; +import org.jetbrains.annotations.Nullable; + @SuppressWarnings("unused") // just expose all included data -public class OpenPgpCapabilities { +@AutoValue +public abstract 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 byte[] mAid; - private byte[] mHistoricalBytes; + private static final int MAX_PW1_LENGTH_INDEX = 1; + private static final int MAX_PW3_LENGTH_INDEX = 3; - private boolean mHasSM; - private boolean mAttriburesChangable; - private boolean mHasKeyImport; + public abstract byte[] getAid(); + abstract byte[] getHistoricalBytes(); - private int mSMType; - private int mMaxCmdLen; - private int mMaxRspLen; + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] getFingerprintSign(); + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] getFingerprintEncrypt(); + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] getFingerprintAuth(); + public abstract byte[] getPwStatusBytes(); - private Map mKeyFormats; - private byte[] mFingerprints; - private byte[] mPwStatusBytes; + public abstract KeyFormat getSignKeyFormat(); + public abstract KeyFormat getEncryptKeyFormat(); + public abstract KeyFormat getAuthKeyFormat(); - public OpenPgpCapabilities(byte[] data) throws IOException { - mKeyFormats = new HashMap<>(); - updateWithData(data); + abstract boolean isHasKeyImport(); + public abstract boolean isAttributesChangable(); + + abstract boolean isHasSM(); + abstract boolean isHasAesSm(); + abstract boolean isHasScp11bSm(); + + @Nullable + abstract Integer getMaxCmdLen(); + @Nullable + abstract Integer getMaxRspLen(); + + public static OpenPgpCapabilities fromBytes(byte[] rawOpenPgpCapabilities) throws IOException { + Iso7816TLV[] parsedTlvData = Iso7816TLV.readList(rawOpenPgpCapabilities, true); + return new AutoValue_OpenPgpCapabilities.Builder().updateWithTLV(parsedTlvData).build(); } - private void updateWithData(byte[] data) throws IOException { - Iso7816TLV[] tlvs = Iso7816TLV.readList(data, true); - if (tlvs.length == 1 && tlvs[0].mT == 0x6E) { - tlvs = ((Iso7816TLV.Iso7816CompositeTLV) tlvs[0]).mSubs; + public KeyFormat getFormatForKeyType(@NonNull KeyType keyType) { + switch (keyType) { + case SIGN: return getSignKeyFormat(); + case ENCRYPT: return getEncryptKeyFormat(); + case AUTH: return getAuthKeyFormat(); } + return null; + } - 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, KeyFormat.fromBytes(tlv.mV)); - break; - case 0xC2: - mKeyFormats.put(KeyType.ENCRYPT, KeyFormat.fromBytes(tlv.mV)); - break; - case 0xC3: - mKeyFormats.put(KeyType.AUTH, KeyFormat.fromBytes(tlv.mV)); - break; - case 0xC4: - mPwStatusBytes = tlv.mV; - break; - case 0xC5: - mFingerprints = tlv.mV; - break; - } + @Nullable + public byte[] getKeyFingerprint(@NonNull KeyType keyType) { + switch (keyType) { + case SIGN: return getFingerprintSign(); + case ENCRYPT: return getFingerprintEncrypt(); + case AUTH: return getFingerprintAuth(); } - } - - 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, KeyFormat.fromBytes(tlv.mV)); - break; - case 0xC2: - mKeyFormats.put(KeyType.ENCRYPT, KeyFormat.fromBytes(tlv.mV)); - break; - case 0xC3: - mKeyFormats.put(KeyType.AUTH, KeyFormat.fromBytes(tlv.mV)); - break; - case 0xC4: - mPwStatusBytes = tlv.mV; - break; - case 0xC5: - mFingerprints = tlv.mV; - 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; - - mSMType = v[1]; - - mMaxCmdLen = (v[6] << 8) + v[7]; - mMaxRspLen = (v[8] << 8) + v[9]; - } - - byte[] getAid() { - return mAid; - } - - byte[] getPwStatusBytes() { - return mPwStatusBytes; + return null; } boolean isPw1ValidForMultipleSignatures() { - return mPwStatusBytes[0] == 1; + return getPwStatusBytes()[0] == 1; } - byte[] getHistoricalBytes() { - return mHistoricalBytes; + public int getPw1MaxLength() { + return getPwStatusBytes()[MAX_PW1_LENGTH_INDEX]; } - boolean isHasSM() { - return mHasSM; + public int getPw3MaxLength() { + return getPwStatusBytes()[MAX_PW3_LENGTH_INDEX]; } - public boolean isAttributesChangable() { - return mAttriburesChangable; - } + @AutoValue.Builder + @SuppressWarnings("UnusedReturnValue") + abstract static class Builder { + abstract Builder aid(byte[] mV); + abstract Builder historicalBytes(byte[] historicalBytes); - boolean isHasKeyImport() { - return mHasKeyImport; - } + abstract Builder fingerprintSign(byte[] fingerprint); + abstract Builder fingerprintEncrypt(byte[] fingerprint); + abstract Builder fingerprintAuth(byte[] fingerprint); - boolean isHasAESSM() { - return isHasSM() && ((mSMType == 1) || (mSMType == 2)); - } + abstract Builder pwStatusBytes(byte[] mV); + abstract Builder authKeyFormat(KeyFormat keyFormat); + abstract Builder encryptKeyFormat(KeyFormat keyFormat); + abstract Builder signKeyFormat(KeyFormat keyFormat); - boolean isHasSCP11bSM() { - return isHasSM() && (mSMType == 3); - } - int getMaxCmdLen() { - return mMaxCmdLen; - } + abstract Builder hasKeyImport(boolean hasKeyImport); + abstract Builder attributesChangable(boolean attributesChangable); - int getMaxRspLen() { - return mMaxRspLen; - } + abstract Builder hasSM(boolean hasSm); + abstract Builder hasAesSm(boolean hasAesSm); + abstract Builder hasScp11bSm(boolean hasScp11bSm); - public KeyFormat getFormatForKeyType(KeyType keyType) { - return mKeyFormats.get(keyType); - } + abstract Builder maxCmdLen(Integer maxCommandLen); + abstract Builder maxRspLen(Integer MaxResponseLen); + + abstract OpenPgpCapabilities build(); + + public Builder() { + hasKeyImport(false); + attributesChangable(false); + hasSM(false); + hasAesSm(false); + hasScp11bSm(false); + } + + Builder updateWithTLV(Iso7816TLV[] tlvs) { + if (tlvs.length == 1 && tlvs[0].mT == 0x6E) { + tlvs = ((Iso7816TLV.Iso7816CompositeTLV) tlvs[0]).mSubs; + } + + for (Iso7816TLV tlv : tlvs) { + switch (tlv.mT) { + case 0x4F: + aid(tlv.mV); + break; + case 0x5F52: + historicalBytes(tlv.mV); + break; + case 0x73: + parseDdo((Iso7816TLV.Iso7816CompositeTLV) tlv); + break; + case 0xC0: + parseExtendedCaps(tlv.mV); + break; + case 0xC1: + signKeyFormat(KeyFormat.fromBytes(tlv.mV)); + break; + case 0xC2: + encryptKeyFormat(KeyFormat.fromBytes(tlv.mV)); + break; + case 0xC3: + authKeyFormat(KeyFormat.fromBytes(tlv.mV)); + break; + case 0xC4: + pwStatusBytes(tlv.mV); + break; + case 0xC5: + parseFingerprints(tlv.mV); + break; + } + } + + return this; + } + + private void parseDdo(Iso7816TLV.Iso7816CompositeTLV tlvs) { + for (Iso7816TLV tlv : tlvs.mSubs) { + switch (tlv.mT) { + case 0xC0: + parseExtendedCaps(tlv.mV); + break; + case 0xC1: + signKeyFormat(KeyFormat.fromBytes(tlv.mV)); + break; + case 0xC2: + encryptKeyFormat(KeyFormat.fromBytes(tlv.mV)); + break; + case 0xC3: + authKeyFormat(KeyFormat.fromBytes(tlv.mV)); + break; + case 0xC4: + pwStatusBytes(tlv.mV); + break; + case 0xC5: + parseFingerprints(tlv.mV); + break; + } + } + } + + private void parseFingerprints(byte[] mV) { + ByteBuffer fpBuf = ByteBuffer.wrap(mV); + + byte[] buf; + + buf = new byte[20]; + fpBuf.get(buf); + fingerprintSign(buf); + + buf = new byte[20]; + fpBuf.get(buf); + fingerprintEncrypt(buf); + + buf = new byte[20]; + fpBuf.get(buf); + fingerprintAuth(buf); + } + + private void parseExtendedCaps(byte[] v) { + hasKeyImport((v[0] & MASK_KEY_IMPORT) != 0); + attributesChangable((v[0] & MASK_ATTRIBUTES_CHANGABLE) != 0); + + if ((v[0] & MASK_SM) != 0) { + hasSM(true); + int smType = v[1]; + hasAesSm(smType == 1 || smType == 2); + hasScp11bSm(smType == 3); + } + + maxCmdLen((v[6] << 8) + v[7]); + maxRspLen((v[8] << 8) + v[9]); + } - public byte[] getFingerprints() { - return mFingerprints; } } 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 a645263be..ed7c5f0b5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -155,7 +155,7 @@ public class SecurityTokenConnection { public void refreshConnectionCapabilities() throws IOException { byte[] rawOpenPgpCapabilities = getData(0x00, 0x6E); - OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities(rawOpenPgpCapabilities); + OpenPgpCapabilities openPgpCapabilities = OpenPgpCapabilities.fromBytes(rawOpenPgpCapabilities); setConnectionCapabilities(openPgpCapabilities); } @@ -249,7 +249,7 @@ public class SecurityTokenConnection { // region secure messaging private void smEstablishIfAvailable(Context context) throws IOException { - if (!openPgpCapabilities.isHasSCP11bSM()) { + if (!openPgpCapabilities.isHasAesSm()) { return; } @@ -379,19 +379,15 @@ public class SecurityTokenConnection { } public SecurityTokenInfo getTokenInfo() throws IOException { - byte[] rawFingerprints = openPgpCapabilities.getFingerprints(); + byte[][] fingerprints = new byte[3][]; + fingerprints[0] = openPgpCapabilities.getFingerprintSign(); + fingerprints[1] = openPgpCapabilities.getFingerprintEncrypt(); + fingerprints[2] = openPgpCapabilities.getFingerprintAuth(); - 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]); - } - - byte[] aid = getAid(); + byte[] aid = openPgpCapabilities.getAid(); String userId = parseHolderName(getUserId()); String url = getUrl(); - byte[] pwInfo = getPwStatusBytes(); + byte[] pwInfo = openPgpCapabilities.getPwStatusBytes(); boolean hasLifeCycleManagement = cardCapabilities.hasLifeCycleManagement(); TransportType transportType = transport.getTransportType(); @@ -423,29 +419,6 @@ public class SecurityTokenConnection { return commandFactory; } - public byte[] getPwStatusBytes() { - return openPgpCapabilities.getPwStatusBytes(); - } - - public byte[] getAid() { - return openPgpCapabilities.getAid(); - } - - public byte[] getKeyFingerprint(@NonNull KeyType keyType) { - byte[] data = openPgpCapabilities.getFingerprints(); - if (data == null) { - return null; - } - - // 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); - - return fp; - } - private static String parseHolderName(byte[] name) { try { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java index c8818dec3..662d8d851 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java @@ -11,7 +11,6 @@ import org.sufficientlysecure.keychain.util.Passphrase; public class ModifyPinTokenOp { - private static final int MAX_PW3_LENGTH_INDEX = 3; private static final int MIN_PW3_LENGTH = 8; private final SecurityTokenConnection connection; @@ -41,9 +40,8 @@ public class ModifyPinTokenOp { private void modifyPw1PinWithEffectiveAdminPin(Passphrase effectiveAdminPin, byte[] newPin) throws IOException { connection.verifyAdminPin(effectiveAdminPin); - final int MAX_PW1_LENGTH_INDEX = 1; - byte[] pwStatusBytes = connection.getPwStatusBytes(); - if (newPin.length < 6 || newPin.length > pwStatusBytes[MAX_PW1_LENGTH_INDEX]) { + int maxPw1Length = connection.getOpenPgpCapabilities().getPw3MaxLength(); + if (newPin.length < 6 || newPin.length > maxPw1Length) { throw new IOException("Invalid PIN length"); } @@ -60,9 +58,9 @@ public class ModifyPinTokenOp { * conformance to the token's requirements for key length. */ private void modifyPw3Pin(byte[] newAdminPin) throws IOException { - byte[] pwStatusBytes = connection.getPwStatusBytes(); + int maxPw3Length = connection.getOpenPgpCapabilities().getPw3MaxLength(); - if (newAdminPin.length < MIN_PW3_LENGTH || newAdminPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) { + if (newAdminPin.length < MIN_PW3_LENGTH || newAdminPin.length > maxPw3Length) { throw new IOException("Invalid PIN length"); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java index 130ffe7e5..c083cc3aa 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java @@ -53,7 +53,7 @@ public class PsoDecryptTokenOp { throws IOException { connection.verifyPinForOther(); - KeyFormat kf = connection.getOpenPgpCapabilities().getFormatForKeyType(KeyType.ENCRYPT); + KeyFormat kf = connection.getOpenPgpCapabilities().getEncryptKeyFormat(); switch (kf.keyFormatType()) { case RSAKeyFormatType: return decryptSessionKeyRsa(encryptedSessionKeyMpi); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java index 9394bcbcb..d757f40f6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java @@ -7,6 +7,7 @@ import java.nio.ByteBuffer; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateCrtKey; +import java.util.Arrays; import android.support.annotation.VisibleForTesting; @@ -174,14 +175,15 @@ public class SecurityTokenChangeKeyTokenOp { private boolean isSlotEmpty(KeyType keyType) throws IOException { // Note: special case: This should not happen, but happens with // https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true - if (connection.getKeyFingerprint(keyType) == null) { + if (connection.getOpenPgpCapabilities().getKeyFingerprint(keyType) == null) { return true; } return keyMatchesFingerPrint(keyType, BLANK_FINGERPRINT); } - private boolean keyMatchesFingerPrint(KeyType keyType, byte[] fingerprint) throws IOException { - return java.util.Arrays.equals(connection.getKeyFingerprint(keyType), fingerprint); + private boolean keyMatchesFingerPrint(KeyType keyType, byte[] expectedFingerprint) throws IOException { + byte[] actualFp = connection.getOpenPgpCapabilities().getKeyFingerprint(keyType); + return Arrays.equals(actualFp, expectedFingerprint); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java index 83fa49fd5..d7378d87a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java @@ -144,7 +144,7 @@ public class SecurityTokenPsoSignTokenOp { connection.verifyPinForSignature(); OpenPgpCapabilities openPgpCapabilities = connection.getOpenPgpCapabilities(); - KeyFormat signKeyFormat = openPgpCapabilities.getFormatForKeyType(KeyType.SIGN); + KeyFormat signKeyFormat = openPgpCapabilities.getSignKeyFormat(); byte[] data = prepareData(hash, hashAlgo, signKeyFormat); @@ -171,7 +171,7 @@ public class SecurityTokenPsoSignTokenOp { connection.verifyPinForOther(); OpenPgpCapabilities openPgpCapabilities = connection.getOpenPgpCapabilities(); - KeyFormat authKeyFormat = openPgpCapabilities.getFormatForKeyType(KeyType.AUTH); + KeyFormat authKeyFormat = openPgpCapabilities.getAuthKeyFormat(); byte[] data = prepareData(hash, hashAlgo, authKeyFormat); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index df450c3c3..b94ddf2be 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -194,7 +194,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { switch (mRequiredInput.mType) { case SECURITY_TOKEN_DECRYPT: { long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint( - stConnection.getKeyFingerprint(KeyType.ENCRYPT)); + stConnection.getOpenPgpCapabilities().getFingerprintEncrypt()); if (tokenKeyId != mRequiredInput.getSubKeyId()) { throw new IOException(getString(R.string.error_wrong_security_token)); @@ -221,7 +221,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { } case SECURITY_TOKEN_SIGN: { long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint( - stConnection.getKeyFingerprint(KeyType.SIGN)); + stConnection.getOpenPgpCapabilities().getFingerprintSign()); if (tokenKeyId != mRequiredInput.getSubKeyId()) { throw new IOException(getString(R.string.error_wrong_security_token)); @@ -240,7 +240,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { } case SECURITY_TOKEN_AUTH: { long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint( - stConnection.getKeyFingerprint(KeyType.AUTH)); + stConnection.getOpenPgpCapabilities().getFingerprintAuth()); if (tokenKeyId != mRequiredInput.getSubKeyId()) { throw new IOException(getString(R.string.error_wrong_security_token)); @@ -280,7 +280,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { long subkeyId = buf.getLong(); CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId); - byte[] tokenSerialNumber = Arrays.copyOf(stConnection.getAid(), 16); + byte[] tokenSerialNumber = Arrays.copyOf(stConnection.getOpenPgpCapabilities().getAid(), 16); Passphrase passphrase; try { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java index d5f507687..bb5f7b105 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java @@ -20,20 +20,13 @@ package org.sufficientlysecure.keychain.ui.util; import java.math.BigInteger; import java.nio.ByteBuffer; -import java.security.DigestException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Locale; import android.content.Context; import android.content.res.Resources; -import android.graphics.Color; import android.graphics.PorterDuff; import android.support.annotation.NonNull; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.style.ForegroundColorSpan; import android.view.View; import android.widget.ImageView; import android.widget.TextView; @@ -42,7 +35,6 @@ import android.widget.ViewAnimator; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.nist.NISTNamedCurves; import org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves; -import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.crypto.ec.CustomNamedCurves; @@ -52,13 +44,11 @@ import org.bouncycastle.util.encoders.Hex; import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.util.OpenPgpUtils; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve; -import org.sufficientlysecure.keychain.util.Log; public class KeyFormattingUtils { diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java index 1b4c42dfc..e225281c1 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java @@ -41,7 +41,7 @@ public class PsoDecryptTokenOpTest { @Test public void testRsaDecrypt() throws Exception { - OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities( + OpenPgpCapabilities openPgpCapabilities = OpenPgpCapabilities.fromBytes( Hex.decode("6e81de4f10d27600012401020000060364311500005f520f0073000080000000000000000000007381b7c00af" + "00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f03" + "0303c53c4ec5fee25c4e89654d58cad8492510a89d3c3d8468da7b24e15bfc624c6a792794f15b759" + diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java index 4cd842cd0..317a2c276 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java @@ -90,7 +90,7 @@ public class SecurityTokenConnectionTest { public void test_getTokenInfo() throws Exception { SecurityTokenConnection securityTokenConnection = new SecurityTokenConnection(transport, new Passphrase("123456"), new OpenPgpCommandApduFactory()); - OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities( + OpenPgpCapabilities openPgpCapabilities = OpenPgpCapabilities.fromBytes( Hex.decode( "6e81de4f10d27600012401020000060364311500005f520f0073000080000000000000000000007381b7c00af" + "00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f03" + @@ -108,7 +108,6 @@ public class SecurityTokenConnectionTest { securityTokenConnection.getTokenInfo(); - verifyDialog(); } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java index ed952550e..56e702f9f 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenUtilsTest.java @@ -208,7 +208,7 @@ public class SecurityTokenUtilsTest extends Mockito { "00000000000000000000000000000000000000cd0c5741e8695741e8695741e8" + "69"); - OpenPgpCapabilities caps = new OpenPgpCapabilities(data); + OpenPgpCapabilities caps = OpenPgpCapabilities.fromBytes(data); Assert.assertEquals(caps.isHasSM(), true); } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java index a45f588f6..af75cddf8 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java @@ -49,7 +49,7 @@ public class SecurityTokenChangeKeyTokenOpTest { @Test public void testPutKey() throws Exception { - OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities( + OpenPgpCapabilities openPgpCapabilities = OpenPgpCapabilities.fromBytes( Hex.decode("6e81de4f10d27600012401020000060364311500005f520f0073000080000000000000000000007381b7c00af" + "00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f03" + "0303c53c4ec5fee25c4e89654d58cad8492510a89d3c3d8468da7b24e15bfc624c6a792794f15b759" + From 0d2cbd0654bf95f15e858e2b6ebc87564d565e41 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Fri, 12 Jan 2018 20:43:00 +0100 Subject: [PATCH 13/15] rename methods that do io to "read*" instead of "get*" --- .../SecurityTokenConnection.java | 24 +++++++++---------- .../keychain/ui/CreateKeyActivity.java | 2 +- ...curityTokenChangePinOperationActivity.java | 2 +- .../ui/SecurityTokenOperationActivity.java | 3 +-- .../ui/base/BaseSecurityTokenActivity.java | 6 ++--- .../SecurityTokenConnectionTest.java | 7 +----- 6 files changed, 18 insertions(+), 26 deletions(-) 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 ed7c5f0b5..4471188df 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -20,7 +20,6 @@ package org.sufficientlysecure.keychain.securitytoken; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.ByteBuffer; import java.util.List; import android.content.Context; @@ -153,7 +152,7 @@ public class SecurityTokenConnection { } public void refreshConnectionCapabilities() throws IOException { - byte[] rawOpenPgpCapabilities = getData(0x00, 0x6E); + byte[] rawOpenPgpCapabilities = readData(0x00, 0x6E); OpenPgpCapabilities openPgpCapabilities = OpenPgpCapabilities.fromBytes(rawOpenPgpCapabilities); setConnectionCapabilities(openPgpCapabilities); @@ -360,7 +359,7 @@ public class SecurityTokenConnection { // endregion - private byte[] getData(int p1, int p2) throws IOException { + private byte[] readData(int p1, int p2) throws IOException { ResponseApdu response = communicate(commandFactory.createGetDataCommand(p1, p2)); if (!response.isSuccess()) { throw new CardException("Failed to get pw status bytes", response.getSw()); @@ -369,32 +368,31 @@ public class SecurityTokenConnection { } - public String getUrl() throws IOException { - byte[] data = getData(0x5F, 0x50); + private String readUrl() throws IOException { + byte[] data = readData(0x5F, 0x50); return new String(data).trim(); } - public byte[] getUserId() throws IOException { - return getData(0x00, 0x65); + private byte[] readUserId() throws IOException { + return readData(0x00, 0x65); } - public SecurityTokenInfo getTokenInfo() throws IOException { + public SecurityTokenInfo readTokenInfo() throws IOException { byte[][] fingerprints = new byte[3][]; fingerprints[0] = openPgpCapabilities.getFingerprintSign(); fingerprints[1] = openPgpCapabilities.getFingerprintEncrypt(); fingerprints[2] = openPgpCapabilities.getFingerprintAuth(); byte[] aid = openPgpCapabilities.getAid(); - String userId = parseHolderName(getUserId()); - String url = getUrl(); + String userId = parseHolderName(readUserId()); + String url = readUrl(); byte[] pwInfo = openPgpCapabilities.getPwStatusBytes(); boolean hasLifeCycleManagement = cardCapabilities.hasLifeCycleManagement(); TransportType transportType = transport.getTransportType(); - return SecurityTokenInfo - .create(transportType, tokenType, fingerprints, aid, userId, url, pwInfo[4], pwInfo[6], - hasLifeCycleManagement); + return SecurityTokenInfo.create(transportType, tokenType, fingerprints, aid, userId, url, pwInfo[4], pwInfo[6], + hasLifeCycleManagement); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java index 4160ab58d..90432a79c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java @@ -140,7 +140,7 @@ public class CreateKeyActivity extends BaseSecurityTokenActivity { return; } - tokenInfo = stConnection.getTokenInfo(); + tokenInfo = stConnection.readTokenInfo(); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java index 6b2c427bf..aa6256185 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenChangePinOperationActivity.java @@ -144,7 +144,7 @@ public class SecurityTokenChangePinOperationActivity extends BaseSecurityTokenAc Passphrase adminPin = new Passphrase(changePinInput.getAdminPin()); ModifyPinTokenOp.create(stConnection, adminPin).modifyPw1Pin(changePinInput.getNewPin().getBytes()); - resultTokenInfo = stConnection.getTokenInfo(); + resultTokenInfo = stConnection.readTokenInfo(); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java index b94ddf2be..b41e09865 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java @@ -39,7 +39,6 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.provider.KeyRepository; import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.securitytoken.KeyType; import org.sufficientlysecure.keychain.securitytoken.operations.ModifyPinTokenOp; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; @@ -305,7 +304,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { } case SECURITY_TOKEN_RESET_CARD: { ResetAndWipeTokenOp.create(stConnection).resetAndWipeToken(); - mResultTokenInfo = stConnection.getTokenInfo(); + mResultTokenInfo = stConnection.readTokenInfo(); break; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java index 53c16a72c..a13cb1a48 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java @@ -82,7 +82,7 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity * Override to implement SecurityToken operations (background thread) */ protected void doSecurityTokenInBackground(SecurityTokenConnection stConnection) throws IOException { - tokenInfo = stConnection.getTokenInfo(); + tokenInfo = stConnection.readTokenInfo(); Log.d(Constants.TAG, "Security Token: " + tokenInfo); } @@ -260,7 +260,7 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity SecurityTokenInfo tokeninfo = null; try { - tokeninfo = stConnection.getTokenInfo(); + tokeninfo = stConnection.readTokenInfo(); } catch (IOException e2) { // don't care } @@ -278,7 +278,7 @@ public abstract class BaseSecurityTokenActivity extends BaseActivity case 0x6982: { SecurityTokenInfo tokeninfo = null; try { - tokeninfo = stConnection.getTokenInfo(); + tokeninfo = stConnection.readTokenInfo(); } catch (IOException e2) { // don't care } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java index 317a2c276..ba58bf00d 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionTest.java @@ -1,7 +1,6 @@ package org.sufficientlysecure.keychain.securitytoken; -import java.io.IOException; import java.util.LinkedList; import org.bouncycastle.util.encoders.Hex; @@ -13,10 +12,6 @@ import org.mockito.stubbing.Answer; import org.robolectric.RuntimeEnvironment; import org.robolectric.shadows.ShadowLog; import org.sufficientlysecure.keychain.KeychainTestRunner; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; -import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; -import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; -import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType; import org.sufficientlysecure.keychain.util.Passphrase; @@ -106,7 +101,7 @@ public class SecurityTokenConnectionTest { expect("00ca5f5000", "9000"); - securityTokenConnection.getTokenInfo(); + securityTokenConnection.readTokenInfo(); verifyDialog(); } From 442845f1fe4ba3218ef99945d45e76fbd5843f77 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Sat, 13 Jan 2018 16:28:26 +0100 Subject: [PATCH 14/15] Split up ResetAndWipeTokenOp a bit, and add test --- .../securitytoken/OpenPgpCapabilities.java | 8 ++ .../SecurityTokenConnection.java | 7 +- .../operations/ResetAndWipeTokenOp.java | 58 +++++++------- .../operations/ResetAndWipeTokenOpTest.java | 77 +++++++++++++++++++ 4 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOpTest.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java index 064091b62..362e8bc55 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java @@ -102,6 +102,14 @@ public abstract class OpenPgpCapabilities { return getPwStatusBytes()[MAX_PW3_LENGTH_INDEX]; } + public int getPw1TriesLeft() { + return getPwStatusBytes()[4]; + } + + public int getPw3TriesLeft() { + return getPwStatusBytes()[6]; + } + @AutoValue.Builder @SuppressWarnings("UnusedReturnValue") abstract static class Builder { 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 4471188df..dd4277a31 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -386,13 +386,14 @@ public class SecurityTokenConnection { byte[] aid = openPgpCapabilities.getAid(); String userId = parseHolderName(readUserId()); String url = readUrl(); - byte[] pwInfo = openPgpCapabilities.getPwStatusBytes(); + int pw1TriesLeft = openPgpCapabilities.getPw1TriesLeft(); + int pw3TriesLeft = openPgpCapabilities.getPw3TriesLeft(); boolean hasLifeCycleManagement = cardCapabilities.hasLifeCycleManagement(); TransportType transportType = transport.getTransportType(); - return SecurityTokenInfo.create(transportType, tokenType, fingerprints, aid, userId, url, pwInfo[4], pwInfo[6], - hasLifeCycleManagement); + return SecurityTokenInfo.create(transportType, tokenType, fingerprints, aid, userId, url, pw1TriesLeft, + pw3TriesLeft, hasLifeCycleManagement); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java index 2befafe0d..c4347cb41 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java @@ -10,6 +10,8 @@ import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; public class ResetAndWipeTokenOp { + private static final byte[] INVALID_PIN = "XXXXXXXXXXX".getBytes(); + private final SecurityTokenConnection connection; public static ResetAndWipeTokenOp create(SecurityTokenConnection connection) { @@ -26,41 +28,16 @@ public class ResetAndWipeTokenOp { * Afterwards, the token is reactivated. */ public void resetAndWipeToken() throws IOException { - // try wrong PIN 4 times until counter goes to C0 - byte[] pin = "XXXXXX".getBytes(); - CommandApdu verifyPw1ForSignatureCommand = - connection.getCommandFactory().createVerifyPw1ForSignatureCommand(pin); - for (int i = 0; i <= 4; i++) { - // Command APDU for VERIFY command (page 32) - ResponseApdu response = connection.communicate(verifyPw1ForSignatureCommand); - if (response.isSuccess()) { - 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(); - CommandApdu verifyPw3Command = connection.getCommandFactory().createVerifyPw3Command(adminPin); - for (int i = 0; i <= 4; i++) { - // Command APDU for VERIFY command (page 32) - ResponseApdu response = connection.communicate( - verifyPw3Command); - if (response.isSuccess()) { // Should NOT accept! - throw new CardException("Should never happen, XXXXXXXX has been accepted", response.getSw()); - } - } + exhausePw1Tries(); + exhaustPw3Tries(); // secure messaging must be disabled before reactivation connection.clearSecureMessaging(); - // 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 = connection.getCommandFactory().createReactivate1Command(); - ResponseApdu response1 = connection.communicate(reactivate1); - if (!response1.isSuccess()) { - throw new CardException("Reactivating failed!", response1.getSw()); - } + connection.communicate(reactivate1); CommandApdu reactivate2 = connection.getCommandFactory().createReactivate2Command(); ResponseApdu response2 = connection.communicate(reactivate2); @@ -70,4 +47,29 @@ public class ResetAndWipeTokenOp { connection.refreshConnectionCapabilities(); } + + private void exhausePw1Tries() throws IOException { + CommandApdu verifyPw1ForSignatureCommand = + connection.getCommandFactory().createVerifyPw1ForSignatureCommand(INVALID_PIN); + + int pw1TriesLeft = Math.max(3, connection.getOpenPgpCapabilities().getPw1TriesLeft()); + for (int i = 0; i < pw1TriesLeft; i++) { + ResponseApdu response = connection.communicate(verifyPw1ForSignatureCommand); + if (response.isSuccess()) { + throw new CardException("Should never happen, PIN XXXXXXXX has been accepted!", response.getSw()); + } + } + } + + private void exhaustPw3Tries() throws IOException { + CommandApdu verifyPw3Command = connection.getCommandFactory().createVerifyPw3Command(INVALID_PIN); + + int pw3TriesLeft = Math.max(3, connection.getOpenPgpCapabilities().getPw3TriesLeft()); + for (int i = 0; i < pw3TriesLeft; i++) { + ResponseApdu response = connection.communicate(verifyPw3Command); + if (response.isSuccess()) { // Should NOT accept! + throw new CardException("Should never happen, PIN XXXXXXXX has been accepted!", response.getSw()); + } + } + } } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOpTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOpTest.java new file mode 100644 index 000000000..c808a0c8f --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOpTest.java @@ -0,0 +1,77 @@ +package org.sufficientlysecure.keychain.securitytoken.operations; + + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.OpenPgpCapabilities; +import org.sufficientlysecure.keychain.securitytoken.OpenPgpCommandApduFactory; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("WeakerAccess") +@RunWith(KeychainTestRunner.class) +public class ResetAndWipeTokenOpTest { + static final ResponseApdu RESPONSE_APDU_SUCCESS = ResponseApdu.fromBytes(Hex.decode("9000")); + static final ResponseApdu RESPONSE_APDU_BAD_PW = ResponseApdu.fromBytes(Hex.decode("63C0")); + + SecurityTokenConnection securityTokenConnection; + OpenPgpCommandApduFactory commandFactory; + ResetAndWipeTokenOp useCase; + + @Before + public void setUp() throws Exception { + securityTokenConnection = mock(SecurityTokenConnection.class); + + commandFactory = mock(OpenPgpCommandApduFactory.class); + when(securityTokenConnection.getCommandFactory()).thenReturn(commandFactory); + + useCase = ResetAndWipeTokenOp.create(securityTokenConnection); + } + + @Test + public void resetAndWipeToken() throws Exception { + OpenPgpCapabilities openPgpCapabilities = OpenPgpCapabilities.fromBytes( + Hex.decode("6e81de4f10d27600012401020000060364311500005f520f0073000080000000000000000000007381b7c00af" + + "00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f03" + + "0303c53c4ec5fee25c4e89654d58cad8492510a89d3c3d8468da7b24e15bfc624c6a792794f15b759" + + "9915f703aab55ed25424d60b17026b7b06c6ad4b9be30a3c63c000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "000000000cd0c59cd0f2a59cd0af059cd0c95" + )); + when(securityTokenConnection.getOpenPgpCapabilities()).thenReturn(openPgpCapabilities); + + CommandApdu verifyPw1Apdu = mock(CommandApdu.class); + CommandApdu verifyPw3Apdu = mock(CommandApdu.class); + when(commandFactory.createVerifyPw1ForSignatureCommand(any(byte[].class))).thenReturn(verifyPw1Apdu); + when(commandFactory.createVerifyPw3Command(any(byte[].class))).thenReturn(verifyPw3Apdu); + when(securityTokenConnection.communicate(verifyPw1Apdu)).thenReturn(RESPONSE_APDU_BAD_PW); + when(securityTokenConnection.communicate(verifyPw3Apdu)).thenReturn(RESPONSE_APDU_BAD_PW); + + CommandApdu reactivate1Apdu = mock(CommandApdu.class); + CommandApdu reactivate2Apdu = mock(CommandApdu.class); + when(commandFactory.createReactivate1Command()).thenReturn(reactivate1Apdu); + when(commandFactory.createReactivate2Command()).thenReturn(reactivate2Apdu); + when(securityTokenConnection.communicate(reactivate1Apdu)).thenReturn(RESPONSE_APDU_SUCCESS); + when(securityTokenConnection.communicate(reactivate2Apdu)).thenReturn(RESPONSE_APDU_SUCCESS); + + + useCase.resetAndWipeToken(); + + + verify(securityTokenConnection).communicate(reactivate1Apdu); + verify(securityTokenConnection).communicate(reactivate2Apdu); + verify(securityTokenConnection).refreshConnectionCapabilities(); + } + +} \ No newline at end of file From d95648546301b0da6b29ef26eed17e5ed1d84229 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Sun, 14 Jan 2018 04:07:19 +0100 Subject: [PATCH 15/15] add license headers --- .../operations/GenerateKeyTokenOp.java | 17 +++++++++++++ .../operations/ModifyPinTokenOp.java | 17 +++++++++++++ .../operations/PsoDecryptTokenOp.java | 17 +++++++++++++ .../operations/ResetAndWipeTokenOp.java | 17 +++++++++++++ .../SecurityTokenChangeKeyTokenOp.java | 17 +++++++++++++ .../SecurityTokenPsoSignTokenOp.java | 17 +++++++++++++ .../SecurityTokenConnectionCompatTest.java | 17 +++++++++++++ .../PsoDecryptTokenOpTest.java | 24 ++++++++++++++++++- .../operations/ResetAndWipeTokenOpTest.java | 17 +++++++++++++ .../SecurityTokenChangeKeyTokenOpTest.java | 17 +++++++++++++ 10 files changed, 176 insertions(+), 1 deletion(-) rename OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/{ => operations}/PsoDecryptTokenOpTest.java (73%) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/GenerateKeyTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/GenerateKeyTokenOp.java index 0cd566db4..4844e62ee 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/GenerateKeyTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/GenerateKeyTokenOp.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + package org.sufficientlysecure.keychain.securitytoken.operations; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java index 662d8d851..7b0fa4f00 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + package org.sufficientlysecure.keychain.securitytoken.operations; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java index c083cc3aa..3a2a8307d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + package org.sufficientlysecure.keychain.securitytoken.operations; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java index c4347cb41..0e8eff509 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + package org.sufficientlysecure.keychain.securitytoken.operations; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java index d757f40f6..25b358c8c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + package org.sufficientlysecure.keychain.securitytoken.operations; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java index d7378d87a..4c7a237ae 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + package org.sufficientlysecure.keychain.securitytoken.operations; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java index 406ee6471..fc368dc00 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + package org.sufficientlysecure.keychain.securitytoken; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOpTest.java similarity index 73% rename from OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java rename to OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOpTest.java index e225281c1..4911996d6 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/PsoDecryptTokenOpTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOpTest.java @@ -1,4 +1,21 @@ -package org.sufficientlysecure.keychain.securitytoken; +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + +package org.sufficientlysecure.keychain.securitytoken.operations; import org.bouncycastle.util.encoders.Hex; @@ -6,6 +23,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.OpenPgpCapabilities; +import org.sufficientlysecure.keychain.securitytoken.OpenPgpCommandApduFactory; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; +import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.operations.PsoDecryptTokenOp; import static org.junit.Assert.*; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOpTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOpTest.java index c808a0c8f..2068eb7f4 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOpTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOpTest.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + package org.sufficientlysecure.keychain.securitytoken.operations; diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java index af75cddf8..d57618308 100644 --- a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2018 Schürmann & Breitmoser GbR + * + * 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 . + */ + package org.sufficientlysecure.keychain.securitytoken.operations;