diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KdfCalculator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KdfCalculator.java new file mode 100644 index 000000000..81dc65f8b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KdfCalculator.java @@ -0,0 +1,56 @@ +package org.sufficientlysecure.keychain.securitytoken; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; + +import java.util.Arrays; + +// References: +// [0] RFC 4880 `OpenPGP Message Format` +class KdfCalculator { + public static class KdfCalculatorArguments { + public KdfParameters.HashType digestAlgorithm; + public byte[] salt; + public int iterations; + } + + public static byte[] calculateKdf(KdfCalculatorArguments kdfCalculatorArguments, byte[] pin) { + Digest digester; + switch (kdfCalculatorArguments.digestAlgorithm) { + case SHA256: + digester = new SHA256Digest(); + break; + case SHA512: + digester = new SHA512Digest(); + break; + default: + throw new RuntimeException("Unknown hash algorithm!"); + } + byte[] salt = kdfCalculatorArguments.salt; + int iterations = kdfCalculatorArguments.iterations; + + // prepare input to hash function + byte[] data = new byte[salt.length + pin.length]; + System.arraycopy(salt, 0, data, 0, salt.length); + System.arraycopy(pin, 0, data, salt.length, pin.length); + + // hash data repeatedly + // the iteration count is actually the number of octets to be hashed + // see 3.7.1.2 of [0] + int q = iterations / data.length; + int r = iterations % data.length; + for (int i = 0; i < q; i++) { + digester.update(data, 0, data.length); + } + digester.update(data, 0, r); + + byte[] digest = new byte[digester.getDigestSize()]; + digester.doFinal(digest, 0); + + // delete secrets from memory + Arrays.fill(data, (byte) 0); + + return digest; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KdfParameters.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KdfParameters.java new file mode 100644 index 000000000..bbf65225a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KdfParameters.java @@ -0,0 +1,134 @@ +package org.sufficientlysecure.keychain.securitytoken; + +import com.google.auto.value.AutoValue; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; + +import java.io.IOException; +import java.nio.ByteBuffer; + +@SuppressWarnings("unused") // just expose all included data +@AutoValue +public abstract class KdfParameters { + + public enum HashType { + SHA256 + , SHA512 + } + public enum PasswordType { + PW1 + , PW2 + , PW3 + } + + public abstract HashType getDigestAlgorithm(); + public abstract int getIterations(); + public abstract byte[] getSaltPw1(); + public abstract byte[] getSaltPw2(); + public abstract byte[] getSaltPw3(); + public abstract byte[] getHashUser(); + public abstract byte[] getHashAdmin(); + public abstract boolean isHasUsesKdf(); + + + public static KdfParameters fromKdfDo(byte[] kdfDo) throws IOException { + // parse elements of KDF-DO + Iso7816TLV[] tlvs = Iso7816TLV.readList(kdfDo, false); + return new AutoValue_KdfParameters.Builder().parseKdfTLVs(tlvs).build(); + } + + public KdfCalculator.KdfCalculatorArguments forType(PasswordType passwordType) { + byte[] salt = null; + // select salt based on the specified password type + switch (passwordType) { + case PW1: + salt = getSaltPw1(); + break; + case PW2: + salt = getSaltPw2(); + break; + case PW3: + salt = getSaltPw3(); + break; + } + KdfCalculator.KdfCalculatorArguments arguments = new KdfCalculator.KdfCalculatorArguments(); + arguments.digestAlgorithm = getDigestAlgorithm(); + arguments.salt = salt; + arguments.iterations = getIterations(); + return arguments; + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder digestAlgorithm(HashType digestAlgorithm); + abstract Builder iterations(int iterations); + abstract Builder saltPw1(byte[] saltPw1); + abstract Builder saltPw2(byte[] saltPw1); + abstract Builder saltPw3(byte[] saltPw1); + abstract Builder hashUser(byte[] hashUser); + abstract Builder hashAdmin(byte[] hashAdmin); + + abstract Builder hasUsesKdf(boolean hasUsesKdf); + + abstract KdfParameters build(); + + public Builder() { + hasUsesKdf(false); + } + + Builder parseKdfTLVs(Iso7816TLV[] tlvs) throws IOException { + for (Iso7816TLV tlv : tlvs) { + switch (tlv.mT) { + case 0x81: + switch (tlv.mV[0]) { + case (byte)0x00: + // no KDF, plain password + hasUsesKdf(false); + case (byte)0x03: + // using KDF + hasUsesKdf(true); + break; + default: + throw new CardException("Unknown KDF algorithm!"); + } + break; + case 0x82: + // hash algorithm + switch (tlv.mV[0]) { + case (byte)0x08: // SHA256 + digestAlgorithm(HashType.SHA256); + break; + case (byte)0x0a: // SHA512 + digestAlgorithm(HashType.SHA512); + break; + default: + throw new CardException("Unknown hash algorithm!"); + } + break; + case 0x83: + // iteration count + ByteBuffer buf = ByteBuffer.wrap(tlv.mV); + iterations(buf.getInt()); + break; + case 0x84: + saltPw1(tlv.mV); + break; + case 0x85: + saltPw2(tlv.mV); + break; + case 0x86: + saltPw3(tlv.mV); + break; + case 0x87: + hashUser(tlv.mV); + break; + case 0x88: + hashAdmin(tlv.mV); + break; + } + } + return this; + } + } +} 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 74067dfba..84f2b578e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/OpenPgpCapabilities.java @@ -26,12 +26,20 @@ import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; +/** + * References: + * [0] `Functional Specification of the OpenPGP application on ISO Smart Card Operating Systems` + * version 3.4.1 + * https://gnupg.org/ftp/specs/OpenPGP-smart-card-application-3.4.1.pdf + */ @SuppressWarnings("unused") // just expose all included data @AutoValue public abstract class OpenPgpCapabilities { + // Extended Capabilites flag bit offsets are defined on page 32 of [0] 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 final static int MASK_KDF_DO = 1; private static final int MAX_PW1_LENGTH_INDEX = 1; private static final int MAX_PW3_LENGTH_INDEX = 3; @@ -63,6 +71,7 @@ public abstract class OpenPgpCapabilities { abstract boolean isHasSM(); abstract boolean isHasAesSm(); abstract boolean isHasScp11bSm(); + abstract boolean isHasKdf(); @Nullable abstract Integer getMaxCmdLen(); @@ -135,6 +144,7 @@ public abstract class OpenPgpCapabilities { abstract Builder hasSM(boolean hasSm); abstract Builder hasAesSm(boolean hasAesSm); abstract Builder hasScp11bSm(boolean hasScp11bSm); + abstract Builder hasKdf(boolean hasKdf); abstract Builder maxCmdLen(Integer maxCommandLen); abstract Builder maxRspLen(Integer MaxResponseLen); @@ -147,6 +157,7 @@ public abstract class OpenPgpCapabilities { hasSM(false); hasAesSm(false); hasScp11bSm(false); + hasKdf(false); } Builder updateWithTLV(Iso7816TLV[] tlvs) { @@ -243,6 +254,8 @@ public abstract class OpenPgpCapabilities { hasScp11bSm(smType == 3); } + hasKdf((v[0] & MASK_KDF_DO) == 1); + maxCmdLen((v[6] << 8) + v[7]); maxRspLen((v[8] << 8) + v[9]); } 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 4215af30c..c56b8a8e8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenConnection.java @@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.securitytoken; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; import java.util.List; import android.content.Context; @@ -37,7 +38,12 @@ import timber.log.Timber; /** * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant * devices. - * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf + * For the full specs, see [0] + * + * References: + * [0] `Functional Specification of the OpenPGP application on ISO Smart Card Operating Systems` + * version 3.4.1 + * https://gnupg.org/ftp/specs/OpenPGP-smart-card-application-3.4.1.pdf */ public class SecurityTokenConnection { private static final int APDU_SW1_RESPONSE_AVAILABLE = 0x61; @@ -55,6 +61,7 @@ public class SecurityTokenConnection { private TokenType tokenType; private CardCapabilities cardCapabilities; private OpenPgpCapabilities openPgpCapabilities; + private KdfParameters kdfParameters; private SecureMessaging secureMessaging; @@ -304,6 +311,44 @@ public class SecurityTokenConnection { // region pin management + private byte[] calculateKdfIfNecessary(byte[] pin, KdfParameters.PasswordType type) throws IOException { + if (!this.openPgpCapabilities.isHasKdf()) { + // KDF-DO is not supported by token + return pin; + } + KdfParameters kdfParameters = retrieveKdfDo(); + if (kdfParameters == null || !kdfParameters.isHasUsesKdf()) { + return pin; + } else { + return KdfCalculator.calculateKdf(kdfParameters.forType(type), pin); + } + } + + private KdfParameters retrieveKdfDo() throws IOException { + if (this.kdfParameters != null) { + return this.kdfParameters; + } + + // query token for KDF-DO + // see page 18 of [0] + CommandApdu getKdfDoCommand = commandFactory.createGetDataCommand(0x00, 0xf9); + ResponseApdu kdfDoResponse = communicate(getKdfDoCommand); + if (!kdfDoResponse.isSuccess()) { + throw new CardException("Couldn't get KDF.DO!", kdfDoResponse.getSw()); + } + byte[] kdfDo = kdfDoResponse.getData(); + + // empty KDF-DO means plain UTF-8 password is being used + // see page 19 of [0] + if (kdfDo.length == 0) { + return null; + } + + this.kdfParameters = KdfParameters.fromKdfDo(kdfDo); + + return this.kdfParameters; + } + public void verifyPinForSignature() throws IOException { if (isPw1ValidatedForSignature) { return; @@ -314,7 +359,14 @@ public class SecurityTokenConnection { byte[] pin = cachedPin.toStringUnsafe().getBytes(); - CommandApdu verifyPw1ForSignatureCommand = commandFactory.createVerifyPw1ForSignatureCommand(pin); + byte[] transformedPin = this.calculateKdfIfNecessary(pin, KdfParameters.PasswordType.PW1); + + CommandApdu verifyPw1ForSignatureCommand = commandFactory.createVerifyPw1ForSignatureCommand(transformedPin); + + // delete secrets from memory + Arrays.fill(pin, (byte) 0); + Arrays.fill(transformedPin, (byte) 0); + ResponseApdu response = communicate(verifyPw1ForSignatureCommand); if (!response.isSuccess()) { throw new CardException("Bad PIN!", response.getSw()); @@ -333,7 +385,14 @@ public class SecurityTokenConnection { byte[] pin = cachedPin.toStringUnsafe().getBytes(); - CommandApdu verifyPw1ForOtherCommand = commandFactory.createVerifyPw1ForOtherCommand(pin); + byte[] transformedPin = this.calculateKdfIfNecessary(pin, KdfParameters.PasswordType.PW1); + + CommandApdu verifyPw1ForOtherCommand = commandFactory.createVerifyPw1ForOtherCommand(transformedPin); + + // delete secrets from memory + Arrays.fill(pin, (byte) 0); + Arrays.fill(transformedPin, (byte) 0); + ResponseApdu response = communicate(verifyPw1ForOtherCommand); if (!response.isSuccess()) { throw new CardException("Bad PIN!", response.getSw()); @@ -347,7 +406,16 @@ public class SecurityTokenConnection { return; } - CommandApdu verifyPw3Command = commandFactory.createVerifyPw3Command(adminPin.toStringUnsafe().getBytes()); + byte[] pin = adminPin.toStringUnsafe().getBytes(); + byte[] transformedPin = this.calculateKdfIfNecessary(pin, KdfParameters.PasswordType.PW3); + + CommandApdu verifyPw3Command = commandFactory.createVerifyPw3Command(transformedPin); + + // delete secrets from memory + Arrays.fill(pin, (byte) 0); + Arrays.fill(transformedPin, (byte) 0); + adminPin.removeFromMemory(); + ResponseApdu response = communicate(verifyPw3Command); if (!response.isSuccess()) { throw new CardException("Bad PIN!", response.getSw()); diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/KdfCalculatorTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/KdfCalculatorTest.java new file mode 100644 index 000000000..bd2658ead --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/KdfCalculatorTest.java @@ -0,0 +1,38 @@ +package org.sufficientlysecure.keychain.securitytoken; + +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowLog; +import org.sufficientlysecure.keychain.KeychainTestRunner; + +@RunWith(KeychainTestRunner.class) +public class KdfCalculatorTest { + + @Before + public void setUp() throws Exception { + ShadowLog.stream = System.out; + } + + @Test + public void testCalculateKdf() { + KdfCalculator.KdfCalculatorArguments arguments = new KdfCalculator.KdfCalculatorArguments(); + arguments.digestAlgorithm = KdfParameters.HashType.SHA256; + arguments.salt = Hex.decode("3031323334353637"); + arguments.iterations = 100000; + + byte[] pin = Hex.decode("313233343536"); + byte[] expected = Hex.decode( + "773784A602B6C81E3F092F4D7D00E17CC822D88F7360FCF2D2EF2D9D901F44B6"); + + byte[] result = KdfCalculator.calculateKdf(arguments, pin); + + Assert.assertArrayEquals( + "Result of iterated & salted S2K KDF not equal to test vector" + , result + , expected); + } +}