Merge pull request #2596 from hagau/password_kdf
Implement password KDF
This commit is contained in:
commit
5214a09ce8
5 changed files with 313 additions and 4 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue