From e3b8cea04d43d9aafec544f56aa46ccf691a575d Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Mon, 1 Feb 2016 15:21:33 +0100 Subject: performance: cache session keys per compatible S2K configuration --- .../SessionKeySecretKeyDecryptorBuilder.java | 107 +++++++++++++++++++++ .../keychain/pgp/CanonicalizedSecretKey.java | 27 +++++- .../keychain/pgp/ComparableS2K.java | 91 ++++++++++++++++++ .../keychain/pgp/PgpDecryptVerifyOperation.java | 2 + .../keychain/util/Passphrase.java | 45 ++++++++- 5 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/SessionKeySecretKeyDecryptorBuilder.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/ComparableS2K.java (limited to 'OpenKeychain/src/main/java/org') diff --git a/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/SessionKeySecretKeyDecryptorBuilder.java b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/SessionKeySecretKeyDecryptorBuilder.java new file mode 100644 index 000000000..49282230f --- /dev/null +++ b/OpenKeychain/src/main/java/org/spongycastle/openpgp/operator/jcajce/SessionKeySecretKeyDecryptorBuilder.java @@ -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); + } + } + }; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java index 7394c07c3..7f2a00617 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java @@ -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) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/ComparableS2K.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/ComparableS2K.java new file mode 100644 index 000000000..b10f77739 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/ComparableS2K.java @@ -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 CREATOR = new Creator() { + @Override + public ComparableS2K createFromParcel(Parcel in) { + return new ComparableS2K(in); + } + + @Override + public ComparableS2K[] newArray(int size) { + return new ComparableS2K[size]; + } + }; + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java index ea7465209..79a7a8fe1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java @@ -610,6 +610,8 @@ public class PgpDecryptVerifyOperation extends BaseOperation 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 entry : mCachedSessionKeys.entrySet()) { + dest.writeParcelable(entry.getKey(), 0); + dest.writeByteArray(entry.getValue()); + } } public static final Creator CREATOR = new Creator() { -- cgit v1.2.3