performance: cache session keys per compatible S2K configuration

This commit is contained in:
Vincent Breitmoser
2016-02-01 15:21:33 +01:00
parent cbf6f15d91
commit e3b8cea04d
5 changed files with 266 additions and 6 deletions

View File

@@ -0,0 +1,107 @@
package org.spongycastle.openpgp.operator.jcajce;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Provider;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.spec.IvParameterSpec;
import org.spongycastle.bcpg.S2K;
import org.spongycastle.jcajce.util.DefaultJcaJceHelper;
import org.spongycastle.jcajce.util.NamedJcaJceHelper;
import org.spongycastle.jcajce.util.ProviderJcaJceHelper;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider;
public class SessionKeySecretKeyDecryptorBuilder
{
private OperatorHelper helper = new OperatorHelper(new DefaultJcaJceHelper());
private PGPDigestCalculatorProvider calculatorProvider;
private JcaPGPDigestCalculatorProviderBuilder calculatorProviderBuilder;
public SessionKeySecretKeyDecryptorBuilder()
{
this.calculatorProviderBuilder = new JcaPGPDigestCalculatorProviderBuilder();
}
public SessionKeySecretKeyDecryptorBuilder(PGPDigestCalculatorProvider calculatorProvider)
{
this.calculatorProvider = calculatorProvider;
}
public SessionKeySecretKeyDecryptorBuilder setProvider(Provider provider)
{
this.helper = new OperatorHelper(new ProviderJcaJceHelper(provider));
if (calculatorProviderBuilder != null)
{
calculatorProviderBuilder.setProvider(provider);
}
return this;
}
public SessionKeySecretKeyDecryptorBuilder setProvider(String providerName)
{
this.helper = new OperatorHelper(new NamedJcaJceHelper(providerName));
if (calculatorProviderBuilder != null)
{
calculatorProviderBuilder.setProvider(providerName);
}
return this;
}
public PBESecretKeyDecryptor build(final byte[] sessionKey)
throws PGPException
{
if (calculatorProvider == null)
{
calculatorProvider = calculatorProviderBuilder.build();
}
return new PBESecretKeyDecryptor(null, calculatorProvider)
{
@Override
public byte[] makeKeyFromPassPhrase(int keyAlgorithm, S2K s2k) throws PGPException {
return sessionKey;
}
public byte[] recoverKeyData(int encAlgorithm, byte[] key, byte[] iv, byte[] keyData, int keyOff, int keyLen)
throws PGPException
{
try
{
Cipher c = helper.createCipher(PGPUtil.getSymmetricCipherName(encAlgorithm) + "/CFB/NoPadding");
c.init(Cipher.DECRYPT_MODE, PGPUtil.makeSymmetricKey(encAlgorithm, key), new IvParameterSpec(iv));
return c.doFinal(keyData, keyOff, keyLen);
}
catch (IllegalBlockSizeException e)
{
throw new PGPException("illegal block size: " + e.getMessage(), e);
}
catch (BadPaddingException e)
{
throw new PGPException("bad padding: " + e.getMessage(), e);
}
catch (InvalidAlgorithmParameterException e)
{
throw new PGPException("invalid parameter: " + e.getMessage(), e);
}
catch (InvalidKeyException e)
{
throw new PGPException("invalid key: " + e.getMessage(), e);
}
}
};
}
}

View File

@@ -19,6 +19,7 @@
package org.sufficientlysecure.keychain.pgp;
import org.spongycastle.bcpg.S2K;
import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.spongycastle.openpgp.PGPException;
import org.spongycastle.openpgp.PGPPrivateKey;
import org.spongycastle.openpgp.PGPSecretKey;
@@ -33,6 +34,7 @@ import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyConverter;
import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder;
import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder;
import org.spongycastle.openpgp.operator.jcajce.SessionKeySecretKeyDecryptorBuilder;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
@@ -145,13 +147,12 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey {
// Otherwise, it's just a regular ol' passphrase
return SecretKeyType.PASSPHRASE;
}
}
/**
* Returns true on right passphrase
*/
public boolean unlock(Passphrase passphrase) throws PgpGeneralException {
public boolean unlock(final Passphrase passphrase) throws PgpGeneralException {
// handle keys on OpenPGP cards like they were unlocked
S2K s2k = mSecretKey.getS2K();
if (s2k != null
@@ -163,8 +164,26 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey {
// try to extract keys using the passphrase
try {
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.getCharArray());
int keyEncryptionAlgorithm = mSecretKey.getKeyEncryptionAlgorithm();
if (keyEncryptionAlgorithm == SymmetricKeyAlgorithmTags.NULL) {
mPrivateKey = mSecretKey.extractPrivateKey(null);
mPrivateKeyState = PRIVATE_KEY_STATE_UNLOCKED;
return true;
}
byte[] sessionKey;
sessionKey = passphrase.getCachedSessionKeyForAlgorithm(keyEncryptionAlgorithm, s2k);
if (sessionKey == null) {
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.getCharArray());
// this operation is EXPENSIVE, so we cache its result in the passed Passphrase object!
sessionKey = keyDecryptor.makeKeyFromPassPhrase(keyEncryptionAlgorithm, s2k);
passphrase.addCachedSessionKey(keyEncryptionAlgorithm, s2k, sessionKey);
}
PBESecretKeyDecryptor keyDecryptor = new SessionKeySecretKeyDecryptorBuilder()
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(sessionKey);
mPrivateKey = mSecretKey.extractPrivateKey(keyDecryptor);
mPrivateKeyState = PRIVATE_KEY_STATE_UNLOCKED;
} catch (PGPException e) {

View File

@@ -0,0 +1,91 @@
package org.sufficientlysecure.keychain.pgp;
import java.util.Arrays;
import android.os.Parcel;
import android.os.Parcelable;
import org.spongycastle.bcpg.S2K;
public class ComparableS2K implements Parcelable {
int encryptionAlgorithm;
int s2kType;
int s2kHashAlgo;
long s2kItCount;
byte[] s2kIV;
Integer cachedHashCode;
public ComparableS2K(int encryptionAlgorithm, S2K s2k) {
this.encryptionAlgorithm = encryptionAlgorithm;
this.s2kType = s2k.getType();
this.s2kHashAlgo = s2k.getHashAlgorithm();
this.s2kItCount = s2k.getIterationCount();
this.s2kIV = s2k.getIV();
}
protected ComparableS2K(Parcel in) {
encryptionAlgorithm = in.readInt();
s2kType = in.readInt();
s2kHashAlgo = in.readInt();
s2kItCount = in.readLong();
s2kIV = in.createByteArray();
}
@Override
public int hashCode() {
if (cachedHashCode == null) {
cachedHashCode = encryptionAlgorithm;
cachedHashCode *= 31 * s2kType;
cachedHashCode *= 31 * s2kHashAlgo;
cachedHashCode *= (int) (31 * s2kItCount);
cachedHashCode *= 31 * Arrays.hashCode(s2kIV);
}
return cachedHashCode;
}
@Override
public boolean equals(Object o) {
boolean isComparableS2K = o instanceof ComparableS2K;
if (!isComparableS2K) {
return false;
}
ComparableS2K other = (ComparableS2K) o;
return encryptionAlgorithm == other.encryptionAlgorithm
&& s2kType == other.s2kType
&& s2kHashAlgo == other.s2kHashAlgo
&& s2kItCount == other.s2kItCount
&& Arrays.equals(s2kIV, other.s2kIV);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(encryptionAlgorithm);
dest.writeInt(s2kType);
dest.writeInt(s2kHashAlgo);
dest.writeLong(s2kItCount);
dest.writeByteArray(s2kIV);
}
public static final Creator<ComparableS2K> CREATOR = new Creator<ComparableS2K>() {
@Override
public ComparableS2K createFromParcel(Parcel in) {
return new ComparableS2K(in);
}
@Override
public ComparableS2K[] newArray(int size) {
return new ComparableS2K[size];
}
};
}

View File

@@ -610,6 +610,8 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
if (secretEncryptionKey.getSecretKeyType() == SecretKeyType.DIVERT_TO_CARD) {
passphrase = null;
} else if (secretKeyType == SecretKeyType.PASSPHRASE_EMPTY) {
passphrase = new Passphrase("");
} else if (cryptoInput.hasPassphrase()) {
passphrase = cryptoInput.getPassphrase();
} else {

View File

@@ -22,9 +22,14 @@ import android.os.Parcelable;
import android.text.Editable;
import android.widget.EditText;
import org.spongycastle.bcpg.S2K;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.ComparableS2K;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map.Entry;
/**
* Passwords should not be stored as Strings in memory.
@@ -38,6 +43,7 @@ import java.util.Arrays;
*/
public class Passphrase implements Parcelable {
private char[] mPassphrase;
HashMap<ComparableS2K, byte[]> mCachedSessionKeys;
/**
* According to http://stackoverflow.com/a/15844273 EditText is not using String internally
@@ -87,8 +93,18 @@ public class Passphrase implements Parcelable {
return mPassphrase.length;
}
public char charAt(int index) {
return mPassphrase[index];
public byte[] getCachedSessionKeyForAlgorithm(int keyEncryptionAlgorithm, S2K s2k) {
if (mCachedSessionKeys == null) {
return null;
}
return mCachedSessionKeys.get(new ComparableS2K(keyEncryptionAlgorithm, s2k));
}
public void addCachedSessionKey(int keyEncryptionAlgorithm, S2K s2k, byte[] sessionKey) {
if (mCachedSessionKeys == null) {
mCachedSessionKeys = new HashMap<>();
}
mCachedSessionKeys.put(new ComparableS2K(keyEncryptionAlgorithm, s2k), sessionKey);
}
/**
@@ -98,6 +114,12 @@ public class Passphrase implements Parcelable {
if (mPassphrase != null) {
Arrays.fill(mPassphrase, ' ');
}
if (mCachedSessionKeys == null) {
return;
}
for (byte[] cachedSessionKey : mCachedSessionKeys.values()) {
Arrays.fill(cachedSessionKey, (byte) 0);
}
}
@Override
@@ -144,10 +166,29 @@ public class Passphrase implements Parcelable {
private Passphrase(Parcel source) {
mPassphrase = source.createCharArray();
int size = source.readInt();
if (size == 0) {
return;
}
mCachedSessionKeys = new HashMap<>(size);
for (int i = 0; i < size; i++) {
ComparableS2K cachedS2K = source.readParcelable(getClass().getClassLoader());
byte[] cachedSessionKey = source.createByteArray();
mCachedSessionKeys.put(cachedS2K, cachedSessionKey);
}
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeCharArray(mPassphrase);
if (mCachedSessionKeys == null || mCachedSessionKeys.isEmpty()) {
dest.writeInt(0);
return;
}
dest.writeInt(mCachedSessionKeys.size());
for (Entry<ComparableS2K,byte[]> entry : mCachedSessionKeys.entrySet()) {
dest.writeParcelable(entry.getKey(), 0);
dest.writeByteArray(entry.getValue());
}
}
public static final Creator<Passphrase> CREATOR = new Creator<Passphrase>() {