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..362e8bc55 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,231 @@ 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 -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(); - 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(); } - 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]; } - boolean isAttributesChangable() { - return mAttriburesChangable; + public int getPw1TriesLeft() { + return getPwStatusBytes()[4]; } - boolean isHasKeyImport() { - return mHasKeyImport; + public int getPw3TriesLeft() { + return getPwStatusBytes()[6]; } - boolean isHasAESSM() { - return isHasSM() && ((mSMType == 1) || (mSMType == 2)); - } + @AutoValue.Builder + @SuppressWarnings("UnusedReturnValue") + abstract static class Builder { + abstract Builder aid(byte[] mV); + abstract Builder historicalBytes(byte[] historicalBytes); - boolean isHasSCP11bSM() { - return isHasSM() && (mSMType == 3); - } + abstract Builder fingerprintSign(byte[] fingerprint); + abstract Builder fingerprintEncrypt(byte[] fingerprint); + abstract Builder fingerprintAuth(byte[] fingerprint); - int getMaxCmdLen() { - return mMaxCmdLen; - } + abstract Builder pwStatusBytes(byte[] mV); + abstract Builder authKeyFormat(KeyFormat keyFormat); + abstract Builder encryptKeyFormat(KeyFormat keyFormat); + abstract Builder signKeyFormat(KeyFormat keyFormat); - int getMaxRspLen() { - return mMaxRspLen; - } - KeyFormat getFormatForKeyType(KeyType keyType) { - return mKeyFormats.get(keyType); - } + abstract Builder hasKeyImport(boolean hasKeyImport); + abstract Builder attributesChangable(boolean attributesChangable); + + abstract Builder hasSM(boolean hasSm); + abstract Builder hasAesSm(boolean hasAesSm); + abstract Builder hasScp11bSm(boolean hasScp11bSm); + + 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/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/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 02f659ab0..dd4277a31 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,22 @@ package org.sufficientlysecure.keychain.securitytoken; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +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,33 +44,30 @@ 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; + 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; @@ -114,61 +81,13 @@ 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; } - private String getHolderName(byte[] name) { - try { - return (new String(name, 4, name[3])).replace('<', ' '); - } catch (IndexOutOfBoundsException e) { - // try-catch for https://github.com/FluffyKaon/OpenPGP-Card - // Note: This should not happen, but happens with - // https://github.com/FluffyKaon/OpenPGP-Card, thus return an empty string for now! - - Log.e(Constants.TAG, "Couldn't get holder name, returning empty string!", e); - return ""; - } - } - - public void 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); - } + // region connection management public void connectIfNecessary(Context context) throws IOException { if (isConnected()) { @@ -184,10 +103,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(); @@ -200,23 +119,16 @@ public class SecurityTokenConnection { refreshConnectionCapabilities(); - mPw1ValidatedForSignature = false; - mPw1ValidatedForDecrypt = false; - mPw3Validated = false; + isPw1ValidatedForSignature = false; + isPw1ValidatedForOther = false; + isPw3Validated = false; - if (mOpenPgpCapabilities.isHasSCP11bSM()) { - try { - SCP11bSecureMessaging.establish(this, context, commandFactory); - } catch (SecureMessagingException e) { - mSecureMessaging = null; - Log.e(Constants.TAG, "failed to establish secure messaging", e); - } - } + smEstablishIfAvailable(context); } @VisibleForTesting void determineTokenType() throws IOException { - tokenType = mTransport.getTokenTypeIfAvailable(); + tokenType = transport.getTokenTypeIfAvailable(); if (tokenType != null) { return; } @@ -239,395 +151,215 @@ public class SecurityTokenConnection { tokenType = TokenType.UNKNOWN; } - private void refreshConnectionCapabilities() throws IOException { - byte[] rawOpenPgpCapabilities = getData(0x00, 0x6E); + public void refreshConnectionCapabilities() throws IOException { + byte[] rawOpenPgpCapabilities = readData(0x00, 0x6E); - OpenPgpCapabilities openPgpCapabilities = new OpenPgpCapabilities(rawOpenPgpCapabilities); + OpenPgpCapabilities openPgpCapabilities = OpenPgpCapabilities.fromBytes(rawOpenPgpCapabilities); setConnectionCapabilities(openPgpCapabilities); } @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) { - verifyAdminPin(adminPin); - } + // endregion - 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()); - } - } + // region communication /** - * Modifies the user's PW3. Before sending, the new PIN will be validated for - * conformance to the token's requirements for key length. + * 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 newAdminPin The new PW3. + * @param commandApdu short or extended APDU to transceive + * @return response from the card */ - public void modifyPw3Pin(byte[] newAdminPin, Passphrase adminPin) throws IOException { - final int MAX_PW3_LENGTH_INDEX = 3; + public ResponseApdu communicate(CommandApdu commandApdu) throws IOException { + commandApdu = smEncryptIfAvailable(commandApdu); - byte[] pwStatusBytes = getPwStatusBytes(); + ResponseApdu lastResponse; - if (newAdminPin.length < 8 || newAdminPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) { - throw new IOException("Invalid PIN length"); - } + lastResponse = transceiveWithChaining(commandApdu); + lastResponse = readChainedResponseIfAvailable(lastResponse); - byte[] pin = adminPin.toStringUnsafe().getBytes(); + lastResponse = smDecryptIfAvailable(lastResponse); - CommandApdu changePin = commandFactory.createChangePw3Command(pin, newAdminPin); - ResponseApdu response = communicate(changePin); - - mPw3Validated = false; - - if (!response.isSuccess()) { - throw new CardException("Failed to change PIN", response.getSw()); - } + return lastResponse; } - /** - * 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); + @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; - if (!mPw1ValidatedForDecrypt) { - verifyPinForOther(); - } + List chainedApdus = commandFactory.createChainedApdus(commandApdu); + for (int i = 0, totalCommands = chainedApdus.size(); i < totalCommands; i++) { + CommandApdu chainedApdu = chainedApdus.get(i); + lastResponse = transport.transceive(chainedApdu); - 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); + boolean isLastCommand = (i == totalCommands - 1); + if (!isLastCommand && !lastResponse.isSuccess()) { + throw new IOException("Failed to chain apdu " + + "(" + i + "/" + (totalCommands-1) + ", last SW: " + lastResponse.getSw() + ")"); } - 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 { - if (mPin == null) { - throw new IllegalStateException("Connection not initialized with Pin!"); - } - byte[] pin = mPin.toStringUnsafe().getBytes(); - - ResponseApdu response = communicate(commandFactory.createVerifyPw1ForSignatureCommand(pin)); - if (!response.isSuccess()) { - throw new CardException("Bad PIN!", response.getSw()); - } - - mPw1ValidatedForSignature = true; - } - - /** - * Verifies the user's PW1 with the appropriate mode. - */ - private void verifyPinForOther() throws IOException { - if (mPin == null) { - throw new IllegalStateException("Connection not initialized with Pin!"); - } - - byte[] pin = mPin.toStringUnsafe().getBytes(); - - // Command APDU for VERIFY command (page 32) - ResponseApdu response = communicate(commandFactory.createVerifyPw1ForOtherCommand(pin)); - if (!response.isSuccess()) { - throw new CardException("Bad PIN!", response.getSw()); - } - - mPw1ValidatedForDecrypt = true; - } - - /** - * Verifies the user's PW1 or PW3 with the appropriate mode. - */ - private void verifyAdminPin(Passphrase adminPin) throws IOException { - // Command APDU for VERIFY command (page 32) - ResponseApdu response = - communicate(commandFactory.createVerifyPw3Command(adminPin.toStringUnsafe().getBytes())); - if (!response.isSuccess()) { - throw new CardException("Bad PIN!", response.getSw()); - } - - 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 (lastResponse == null) { + throw new IllegalStateException(); + } - if (!response.isSuccess()) { - throw new CardException("Failed to put data.", response.getSw()); + return lastResponse; + } else { + throw new IOException("Command too long, and chaining unavailable"); } } - private void setKeyAttributes(Passphrase adminPin, KeyType keyType, byte[] data) throws IOException { - if (!mOpenPgpCapabilities.isAttributesChangable()) { + @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.isHasAesSm()) { 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); + secureMessaging = SCP11bSecureMessaging.establish(this, context, commandFactory); + } catch (SecureMessagingException e) { + secureMessaging = null; + Log.e(Constants.TAG, "failed to establish secure messaging", e); + } + } - setKeyAttributes(adminPin, slot, SecurityTokenUtils.attributesFromSecretKey(slot, secretKey, - mOpenPgpCapabilities.getFormatForKeyType(slot))); + 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()); + } + } - 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(); + 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()); + } + } - // 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."); - } + public void clearSecureMessaging() { + if (secureMessaging != null) { + secureMessaging.clearSession(); + } + secureMessaging = null; + } - keyBytes = SecurityTokenUtils.createRSAPrivKeyTemplate(crtSecretKey, slot, - (RSAKeyFormat) formatForKeyType); - break; + // endregion - case ECKeyFormatType: - if (!secretKey.isEC()) { - throw new IOException("Security Token not configured for EC key."); - } + // region pin management - 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()); + public void verifyPinForSignature() throws IOException { + if (isPw1ValidatedForSignature) { + return; + } + if (cachedPin == null) { + throw new IllegalStateException("Connection not initialized with Pin!"); } - CommandApdu apdu = commandFactory.createPutKeyCommand(keyBytes); - ResponseApdu response = communicate(apdu); + byte[] pin = cachedPin.toStringUnsafe().getBytes(); + CommandApdu verifyPw1ForSignatureCommand = commandFactory.createVerifyPw1ForSignatureCommand(pin); + ResponseApdu response = communicate(verifyPw1ForSignatureCommand); if (!response.isSuccess()) { - throw new CardException("Key export to Security Token failed", response.getSw()); + throw new CardException("Bad PIN!", response.getSw()); + } + + isPw1ValidatedForSignature = true; + } + + public void verifyPinForOther() throws IOException { + if (isPw1ValidatedForOther) { + return; + } + if (cachedPin == null) { + throw new IllegalStateException("Connection not initialized with Pin!"); + } + + byte[] pin = cachedPin.toStringUnsafe().getBytes(); + + CommandApdu verifyPw1ForOtherCommand = commandFactory.createVerifyPw1ForOtherCommand(pin); + ResponseApdu response = communicate(verifyPw1ForOtherCommand); + if (!response.isSuccess()) { + throw new CardException("Bad PIN!", response.getSw()); + } + + isPw1ValidatedForOther = true; + } + + public void verifyAdminPin(Passphrase adminPin) throws IOException { + if (isPw3Validated) { + return; + } + + CommandApdu verifyPw3Command = commandFactory.createVerifyPw3Command(adminPin.toStringUnsafe().getBytes()); + ResponseApdu response = communicate(verifyPw3Command); + if (!response.isSuccess()) { + throw new CardException("Bad PIN!", response.getSw()); + } + + isPw3Validated = true; + } + + public void invalidateSingleUsePw1() { + if (!openPgpCapabilities.isPw1ValidForMultipleSignatures()) { + isPw1ValidatedForSignature = 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 mOpenPgpCapabilities.getFingerprints(); + public void invalidatePw3() { + isPw3Validated = false; } - /** - * 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. - */ - private byte[] getPwStatusBytes() throws IOException { - return mOpenPgpCapabilities.getPwStatusBytes(); - } + // endregion - public byte[] getAid() throws IOException { - return mOpenPgpCapabilities.getAid(); - } - - public String getUrl() throws IOException { - byte[] data = getData(0x5F, 0x50); - return new String(data).trim(); - } - - public String getUserId() throws IOException { - return getHolderName(getData(0x00, 0x65)); - } - - private byte[] getData(int p1, int p2) throws IOException { + private byte[] 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()); @@ -636,390 +368,67 @@ public class SecurityTokenConnection { } - 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 String readUrl() throws IOException { + byte[] data = readData(0x5F, 0x50); + return new String(data).trim(); } - 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[] readUserId() throws IOException { + return readData(0x00, 0x65); } + public SecurityTokenInfo readTokenInfo() throws IOException { + byte[][] fingerprints = new byte[3][]; + fingerprints[0] = openPgpCapabilities.getFingerprintSign(); + fingerprints[1] = openPgpCapabilities.getFingerprintEncrypt(); + fingerprints[2] = openPgpCapabilities.getFingerprintAuth(); - 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; + byte[] aid = openPgpCapabilities.getAid(); + String userId = parseHolderName(readUserId()); + String url = readUrl(); + int pw1TriesLeft = openPgpCapabilities.getPw1TriesLeft(); + int pw3TriesLeft = openPgpCapabilities.getPw3TriesLeft(); + boolean hasLifeCycleManagement = cardCapabilities.hasLifeCycleManagement(); - 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; + TransportType transportType = transport.getTransportType(); + + return SecurityTokenInfo.create(transportType, tokenType, fingerprints, aid, userId, url, pw1TriesLeft, + pw3TriesLeft, hasLifeCycleManagement); } - /** - * 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 - * 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 ((mSecureMessaging != null) && mSecureMessaging.isEstablished()) { - try { - apdu = mSecureMessaging.encryptAndSign(apdu); - } catch (SecureMessagingException e) { - clearSecureMessaging(); - throw new IOException("secure messaging encrypt/sign failure : " + e.getMessage()); - } - } - - ResponseApdu lastResponse = null; - // Transmit - if (mCardCapabilities.hasExtended()) { - lastResponse = mTransport.transceive(apdu); - } else if (commandFactory.isSuitableForShortApdu(apdu)) { - CommandApdu shortApdu = commandFactory.createShortApdu(apdu); - lastResponse = mTransport.transceive(shortApdu); - } else if (mCardCapabilities.hasChaining()) { - List chainedApdus = commandFactory.createChainedApdus(apdu); - for (int i = 0, totalCommands = chainedApdus.size(); i < totalCommands; i++) { - CommandApdu chainedApdu = chainedApdus.get(i); - lastResponse = mTransport.transceive(chainedApdu); - - 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 = mTransport.transceive(getResponse); - result.write(lastResponse.getData()); - } - - result.write(lastResponse.getSw1()); - result.write(lastResponse.getSw2()); - - lastResponse = ResponseApdu.fromBytes(result.toByteArray()); - - if ((mSecureMessaging != null) && mSecureMessaging.isEstablished()) { - try { - lastResponse = mSecureMessaging.verifyAndDecrypt(lastResponse); - } catch (SecureMessagingException e) { - clearSecureMessaging(); - throw new IOException("secure messaging verify/decrypt failure : " + e.getMessage()); - } - } - - 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 (!mPw3Validated) { - 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(); - } - - /** - * 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. - * - * @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; - } - - // 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; - } 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() { return tokenType; } - public void clearSecureMessaging() { - if (mSecureMessaging != null) { - mSecureMessaging.clearSession(); + public OpenPgpCapabilities getOpenPgpCapabilities() { + return openPgpCapabilities; + } + + public OpenPgpCommandApduFactory getCommandFactory() { + return commandFactory; + } + + + 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 ""; } - mSecureMessaging = null; - } - - void setSecureMessaging(final SecureMessaging sm) { - clearSecureMessaging(); - mSecureMessaging = sm; - } - - public SecurityTokenInfo getTokenInfo() throws IOException { - byte[] rawFingerprints = 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]); - } - - byte[] aid = getAid(); - String userId = getUserId(); - String url = getUrl(); - byte[] pwInfo = getPwStatusBytes(); - boolean hasLifeCycleManagement = mCardCapabilities.hasLifeCycleManagement(); - - TransportType transportType = mTransport.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/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/operations/GenerateKeyTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/GenerateKeyTokenOp.java new file mode 100644 index 000000000..4844e62ee --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/GenerateKeyTokenOp.java @@ -0,0 +1,70 @@ +/* + * 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 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 GenerateKeyTokenOp { + private final SecurityTokenConnection connection; + + public static GenerateKeyTokenOp create(SecurityTokenConnection connection) { + return new GenerateKeyTokenOp(connection); + } + + private GenerateKeyTokenOp(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/operations/ModifyPinTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java new file mode 100644 index 000000000..7b0fa4f00 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ModifyPinTokenOp.java @@ -0,0 +1,95 @@ +/* + * 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 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 ModifyPinTokenOp { + private static final int MIN_PW3_LENGTH = 8; + + private final SecurityTokenConnection connection; + private final Passphrase adminPin; + + public static ModifyPinTokenOp create(SecurityTokenConnection connection, Passphrase adminPin) { + return new ModifyPinTokenOp(connection, adminPin); + } + + private ModifyPinTokenOp(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); + + int maxPw1Length = connection.getOpenPgpCapabilities().getPw3MaxLength(); + if (newPin.length < 6 || newPin.length > maxPw1Length) { + 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 { + int maxPw3Length = connection.getOpenPgpCapabilities().getPw3MaxLength(); + + if (newAdminPin.length < MIN_PW3_LENGTH || newAdminPin.length > maxPw3Length) { + 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/operations/PsoDecryptTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java new file mode 100644 index 000000000..3a2a8307d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOp.java @@ -0,0 +1,198 @@ +/* + * 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 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; +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 PsoDecryptTokenOp { + private final SecurityTokenConnection connection; + private final JcaKeyFingerprintCalculator fingerprintCalculator; + + public static PsoDecryptTokenOp create(SecurityTokenConnection connection) { + return new PsoDecryptTokenOp(connection, new JcaKeyFingerprintCalculator()); + } + + private PsoDecryptTokenOp(SecurityTokenConnection connection, + JcaKeyFingerprintCalculator jcaKeyFingerprintCalculator) { + this.connection = connection; + this.fingerprintCalculator = jcaKeyFingerprintCalculator; + } + + public byte[] verifyAndDecryptSessionKey(@NonNull byte[] encryptedSessionKeyMpi, CanonicalizedPublicKey publicKey) + throws IOException { + connection.verifyPinForOther(); + + KeyFormat kf = connection.getOpenPgpCapabilities().getEncryptKeyFormat(); + switch (kf.keyFormatType()) { + case RSAKeyFormatType: + return decryptSessionKeyRsa(encryptedSessionKeyMpi); + + case ECKeyFormatType: + return decryptSessionKeyEcdh(encryptedSessionKeyMpi, (ECKeyFormat) kf, publicKey); + + default: + throw new CardException("Unknown encryption key type!"); + } + } + + 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()); + } + + return response.getData(); + } + + private byte[] decryptSessionKeyEcdh(byte[] encryptedSessionKeyMpi, ECKeyFormat eckf, CanonicalizedPublicKey publicKey) + throws IOException { + int mpiLength = getMpiLength(encryptedSessionKeyMpi); + byte[] encryptedPoint = Arrays.copyOfRange(encryptedSessionKeyMpi, 2, mpiLength); + + X9ECParameters x9Params = NISTNamedCurves.getByOID(eckf.getCurveOID()); + ECPoint p = x9Params.getCurve().decodePoint(encryptedPoint); + if (!p.isValid()) { + throw new CardException("Invalid EC point!"); + } + + byte[] psoDecipherPayload = p.getEncoded(false); + + 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); + + 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); + + 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); + + 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()); + } + + /* 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. + */ + byte[] keyEncryptionKey = response.getData(); + + final byte[] keyEnc = new byte[encryptedSessionKeyMpi[mpiLength + 2]]; + + 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/securitytoken/operations/ResetAndWipeTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java new file mode 100644 index 000000000..0e8eff509 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOp.java @@ -0,0 +1,92 @@ +/* + * 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 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 ResetAndWipeTokenOp { + private static final byte[] INVALID_PIN = "XXXXXXXXXXX".getBytes(); + + private final SecurityTokenConnection connection; + + public static ResetAndWipeTokenOp create(SecurityTokenConnection connection) { + return new ResetAndWipeTokenOp(connection); + } + + private ResetAndWipeTokenOp(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 { + exhausePw1Tries(); + exhaustPw3Tries(); + + // secure messaging must be disabled before reactivation + connection.clearSecureMessaging(); + + // 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(); + connection.communicate(reactivate1); + + CommandApdu reactivate2 = connection.getCommandFactory().createReactivate2Command(); + ResponseApdu response2 = connection.communicate(reactivate2); + if (!response2.isSuccess()) { + throw new CardException("Reactivating failed!", response2.getSw()); + } + + 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/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java new file mode 100644 index 000000000..25b358c8c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOp.java @@ -0,0 +1,206 @@ +/* + * 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 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 java.util.Arrays; + +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 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 SecurityTokenChangeKeyTokenOp create(SecurityTokenConnection stConnection) { + return new SecurityTokenChangeKeyTokenOp(stConnection); + } + + private SecurityTokenChangeKeyTokenOp(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.getOpenPgpCapabilities().getKeyFingerprint(keyType) == null) { + return true; + } + + return keyMatchesFingerPrint(keyType, BLANK_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 new file mode 100644 index 000000000..4c7a237ae --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenPsoSignTokenOp.java @@ -0,0 +1,205 @@ +/* + * 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 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.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 SecurityTokenPsoSignTokenOp { + private final SecurityTokenConnection connection; + + public static SecurityTokenPsoSignTokenOp create(SecurityTokenConnection connection) { + return new SecurityTokenPsoSignTokenOp(connection); + } + + private SecurityTokenPsoSignTokenOp(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.getSignKeyFormat(); + + 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.invalidateSingleUsePw1(); + + 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.getAuthKeyFormat(); + + 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/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/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) { 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..aa6256185 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.operations.ModifyPinTokenOp; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenConnection; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; import org.sufficientlysecure.keychain.service.input.SecurityTokenChangePinParcel; @@ -141,9 +142,9 @@ 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); + 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 e962e8008..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,9 +39,13 @@ 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; +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; @@ -189,7 +193,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)); @@ -205,17 +209,18 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { throw new IOException("Couldn't find subkey for key to token operation."); } + PsoDecryptTokenOp psoDecryptTokenOp = PsoDecryptTokenOp.create(stConnection); for (int i = 0; i < mRequiredInput.mInputData.length; i++) { byte[] encryptedSessionKey = mRequiredInput.mInputData[i]; - byte[] decryptedSessionKey = stConnection - .decryptSessionKey(encryptedSessionKey, publicKeyRing.getPublicKey(tokenKeyId)); + byte[] decryptedSessionKey = psoDecryptTokenOp + .verifyAndDecryptSessionKey(encryptedSessionKey, publicKeyRing.getPublicKey(tokenKeyId)); mInputParcel = mInputParcel.withCryptoData(encryptedSessionKey, decryptedSessionKey); } break; } 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)); @@ -223,26 +228,28 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { mInputParcel = mInputParcel.withSignatureTime(mRequiredInput.mSignatureTime); + SecurityTokenPsoSignTokenOp psoSignUseCase = SecurityTokenPsoSignTokenOp.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; } 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)); } + SecurityTokenPsoSignTokenOp psoSignUseCase = SecurityTokenPsoSignTokenOp.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); } @@ -272,7 +279,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 { @@ -282,25 +289,22 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity { throw new IOException("Unable to get cached passphrase!"); } - stConnection.changeKey(key, passphrase, adminPin); + SecurityTokenChangeKeyTokenOp putKeyUseCase = SecurityTokenChangeKeyTokenOp.create(stConnection); + putKeyUseCase.changeKey(key, passphrase, adminPin); // TODO: Is this really used anywhere? 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))); + ModifyPinTokenOp.create(stConnection, adminPin).modifyPw1andPw3Pins(newPin, newAdminPin); SecurityTokenConnection.clearCachedConnections(); break; } case SECURITY_TOKEN_RESET_CARD: { - stConnection.resetAndWipeToken(); - mResultTokenInfo = stConnection.getTokenInfo(); + ResetAndWipeTokenOp.create(stConnection).resetAndWipeToken(); + 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/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/SecurityTokenConnectionCompatTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java new file mode 100644 index 000000000..fc368dc00 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnectionCompatTest.java @@ -0,0 +1,99 @@ +/* + * 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; + + +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 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..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; @@ -90,7 +85,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" + @@ -106,86 +101,11 @@ public class SecurityTokenConnectionTest { expect("00ca5f5000", "9000"); - securityTokenConnection.getTokenInfo(); - + securityTokenConnection.readTokenInfo(); 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"); - - - securityTokenConnection.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))); } @@ -199,8 +119,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 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/PsoDecryptTokenOpTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOpTest.java new file mode 100644 index 000000000..4911996d6 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/PsoDecryptTokenOpTest.java @@ -0,0 +1,85 @@ +/* + * 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; +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.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +@RunWith(KeychainTestRunner.class) +public class PsoDecryptTokenOpTest { + private static final byte[] RSA_ENC_SESSIONKEY_MPI = Hex.decode( + "07ff7b9ff36f70da1fe7a6b59168c24a7e5b48a938c4f970de46524a06ebf4a9175a9737cf2e6f30957110b31db7" + + "0e9a2992401b1d5e99389f976356f4e3a28ff537362e7ce14b81200e21d4f0e77d46bd89f3a54ca06062289148a5938748" + + "8ac01d30d2baf58e6b35e32434720473604a9f7d5083ca6d40e4a2dadedd68033a4d4bbdb06d075d6980c0c0ca19078dcd" + + "fb9d8cbcb34f28d0b968b6e09eda0e1d3ab6b251eb09f9fb9d9abfeaf9010001733b9015e9e4b6c9df61bbc76041f439d1" + + "273e41f5d0e8414a2b8d6d4c7e86f30b94cfba308b38de53d694a8ca15382301ace806c8237641b03525b3e3e8cbb017e2" + + "51265229bcbb0da5d5aeb4eafbad9779"); + private SecurityTokenConnection securityTokenConnection; + private OpenPgpCommandApduFactory commandFactory; + private PsoDecryptTokenOp 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 = PsoDecryptTokenOp.create(securityTokenConnection); + } + + @Test + public void testRsaDecrypt() throws Exception { + OpenPgpCapabilities openPgpCapabilities = OpenPgpCapabilities.fromBytes( + 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.verifyAndDecryptSessionKey(RSA_ENC_SESSIONKEY_MPI, null); + + assertArrayEquals(Hex.decode("01020304"), response); + } +} \ No newline at end of file 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..2068eb7f4 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/ResetAndWipeTokenOpTest.java @@ -0,0 +1,94 @@ +/* + * 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; +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 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 new file mode 100644 index 000000000..d57618308 --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/operations/SecurityTokenChangeKeyTokenOpTest.java @@ -0,0 +1,144 @@ +/* + * 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; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalMatchers; +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.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.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("WeakerAccess") +@RunWith(KeychainTestRunner.class) +public class SecurityTokenChangeKeyTokenOpTest { + SecurityTokenChangeKeyTokenOp 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 = SecurityTokenChangeKeyTokenOp.create(securityTokenConnection); + + } + + @Test + public void testPutKey() throws Exception { + OpenPgpCapabilities openPgpCapabilities = OpenPgpCapabilities.fromBytes( + 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(SecurityTokenChangeKeyTokenOpTest.class.getResourceAsStream(name)).next(); + } +} \ No newline at end of file