Merge pull request #2596 from hagau/password_kdf

Implement password KDF
This commit is contained in:
Vincent Breitmoser 2021-02-03 22:49:46 +01:00 committed by GitHub
commit 5214a09ce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 313 additions and 4 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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]);
}

View File

@ -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());

View File

@ -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);
}
}