aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java200
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java836
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyResult.java93
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java218
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpImportExport.java294
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java644
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java769
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncrypt.java607
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpToX509.java313
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/NoAsymmetricEncryptionException.java26
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralException.java29
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralMsgIdException.java35
12 files changed, 4064 insertions, 0 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java
new file mode 100644
index 000000000..c6c62d649
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.pgp;
+
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPObjectFactory;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureList;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+
+public class PgpConversionHelper {
+
+ /**
+ * Convert from byte[] to PGPKeyRing
+ *
+ * @param keysBytes
+ * @return
+ */
+ public static PGPKeyRing BytesToPGPKeyRing(byte[] keysBytes) {
+ PGPObjectFactory factory = new PGPObjectFactory(keysBytes);
+ PGPKeyRing keyRing = null;
+ try {
+ if ((keyRing = (PGPKeyRing) factory.nextObject()) == null) {
+ Log.e(Constants.TAG, "No keys given!");
+ }
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while converting to PGPKeyRing!", e);
+ }
+
+ return keyRing;
+ }
+
+ /**
+ * Convert from byte[] to ArrayList<PGPSecretKey>
+ *
+ * @param keysBytes
+ * @return
+ */
+ public static ArrayList<PGPSecretKey> BytesToPGPSecretKeyList(byte[] keysBytes) {
+ PGPObjectFactory factory = new PGPObjectFactory(keysBytes);
+ Object obj = null;
+ ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>();
+ try {
+ while ((obj = factory.nextObject()) != null) {
+ PGPSecretKey secKey = null;
+ if (obj instanceof PGPSecretKey) {
+ secKey = (PGPSecretKey) obj;
+ if (secKey == null) {
+ Log.e(Constants.TAG, "No keys given!");
+ }
+ keys.add(secKey);
+ } else if (obj instanceof PGPSecretKeyRing) { //master keys are sent as keyrings
+ PGPSecretKeyRing keyRing = null;
+ keyRing = (PGPSecretKeyRing) obj;
+ if (keyRing == null) {
+ Log.e(Constants.TAG, "No keys given!");
+ }
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSecretKey> itr = keyRing.getSecretKeys();
+ while (itr.hasNext()) {
+ keys.add(itr.next());
+ }
+ }
+ }
+ } catch (IOException e) {
+ }
+
+ return keys;
+ }
+
+ /**
+ * Convert from byte[] to PGPSecretKey
+ * <p/>
+ * Singles keys are encoded as keyRings with one single key in it by Bouncy Castle
+ *
+ * @param keyBytes
+ * @return
+ */
+ public static PGPSecretKey BytesToPGPSecretKey(byte[] keyBytes) {
+ PGPObjectFactory factory = new PGPObjectFactory(keyBytes);
+ Object obj = null;
+ try {
+ obj = factory.nextObject();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while converting to PGPSecretKey!", e);
+ }
+ PGPSecretKey secKey = null;
+ if (obj instanceof PGPSecretKey) {
+ if ((secKey = (PGPSecretKey) obj) == null) {
+ Log.e(Constants.TAG, "No keys given!");
+ }
+ } else if (obj instanceof PGPSecretKeyRing) { //master keys are sent as keyrings
+ PGPSecretKeyRing keyRing = null;
+ if ((keyRing = (PGPSecretKeyRing) obj) == null) {
+ Log.e(Constants.TAG, "No keys given!");
+ }
+ secKey = keyRing.getSecretKey();
+ }
+
+ return secKey;
+ }
+
+ /**
+ * Convert from byte[] to PGPSignature
+ *
+ * @param sigBytes
+ * @return
+ */
+ public static PGPSignature BytesToPGPSignature(byte[] sigBytes) {
+ PGPObjectFactory factory = new PGPObjectFactory(sigBytes);
+ PGPSignatureList signatures = null;
+ try {
+ if ((signatures = (PGPSignatureList) factory.nextObject()) == null || signatures.isEmpty()) {
+ Log.e(Constants.TAG, "No signatures given!");
+ return null;
+ }
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while converting to PGPSignature!", e);
+ return null;
+ }
+
+ return signatures.get(0);
+ }
+
+ /**
+ * Convert from ArrayList<PGPSecretKey> to byte[]
+ *
+ * @param keys
+ * @return
+ */
+ public static byte[] PGPSecretKeyArrayListToBytes(ArrayList<PGPSecretKey> keys) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ for (PGPSecretKey key : keys) {
+ try {
+ key.encode(os);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while converting ArrayList<PGPSecretKey> to byte[]!", e);
+ }
+ }
+
+ return os.toByteArray();
+ }
+
+ /**
+ * Convert from PGPSecretKey to byte[]
+ *
+ * @param key
+ * @return
+ */
+ public static byte[] PGPSecretKeyToBytes(PGPSecretKey key) {
+ try {
+ return key.getEncoded();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Encoding failed", e);
+
+ return null;
+ }
+ }
+
+ /**
+ * Convert from PGPSecretKeyRing to byte[]
+ *
+ * @param keyRing
+ * @return
+ */
+ public static byte[] PGPSecretKeyRingToBytes(PGPSecretKeyRing keyRing) {
+ try {
+ return keyRing.getEncoded();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Encoding failed", e);
+
+ return null;
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java
new file mode 100644
index 000000000..8a0bf99d7
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java
@@ -0,0 +1,836 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp;
+
+import android.content.Context;
+
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.spongycastle.bcpg.ArmoredInputStream;
+import org.spongycastle.bcpg.SignatureSubpacketTags;
+import org.spongycastle.openpgp.PGPCompressedData;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.spongycastle.openpgp.PGPEncryptedDataList;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPLiteralData;
+import org.spongycastle.openpgp.PGPObjectFactory;
+import org.spongycastle.openpgp.PGPOnePassSignature;
+import org.spongycastle.openpgp.PGPOnePassSignatureList;
+import org.spongycastle.openpgp.PGPPBEEncryptedData;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureList;
+import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
+import org.spongycastle.openpgp.PGPUtil;
+import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory;
+import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.SignatureException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * This class uses a Builder pattern!
+ */
+public class PgpDecryptVerify {
+ private Context mContext;
+ private InputData mData;
+ private OutputStream mOutStream;
+
+ private ProgressDialogUpdater mProgressDialogUpdater;
+ private boolean mAllowSymmetricDecryption;
+ private String mPassphrase;
+ private Set<Long> mAllowedKeyIds;
+
+ private PgpDecryptVerify(Builder builder) {
+ // private Constructor can only be called from Builder
+ this.mContext = builder.mContext;
+ this.mData = builder.mData;
+ this.mOutStream = builder.mOutStream;
+
+ this.mProgressDialogUpdater = builder.mProgressDialogUpdater;
+ this.mAllowSymmetricDecryption = builder.mAllowSymmetricDecryption;
+ this.mPassphrase = builder.mPassphrase;
+ this.mAllowedKeyIds = builder.mAllowedKeyIds;
+ }
+
+ public static class Builder {
+ // mandatory parameter
+ private Context mContext;
+ private InputData mData;
+ private OutputStream mOutStream;
+
+ // optional
+ private ProgressDialogUpdater mProgressDialogUpdater = null;
+ private boolean mAllowSymmetricDecryption = true;
+ private String mPassphrase = null;
+ private Set<Long> mAllowedKeyIds = null;
+
+ public Builder(Context context, InputData data, OutputStream outStream) {
+ this.mContext = context;
+ this.mData = data;
+ this.mOutStream = outStream;
+ }
+
+ public Builder progressDialogUpdater(ProgressDialogUpdater progressDialogUpdater) {
+ this.mProgressDialogUpdater = progressDialogUpdater;
+ return this;
+ }
+
+ public Builder allowSymmetricDecryption(boolean allowSymmetricDecryption) {
+ this.mAllowSymmetricDecryption = allowSymmetricDecryption;
+ return this;
+ }
+
+ public Builder passphrase(String passphrase) {
+ this.mPassphrase = passphrase;
+ return this;
+ }
+
+ /**
+ * Allow these key ids alone for decryption.
+ * This means only ciphertexts encrypted for one of these private key can be decrypted.
+ *
+ * @param allowedKeyIds
+ * @return
+ */
+ public Builder allowedKeyIds(Set<Long> allowedKeyIds) {
+ this.mAllowedKeyIds = allowedKeyIds;
+ return this;
+ }
+
+ public PgpDecryptVerify build() {
+ return new PgpDecryptVerify(this);
+ }
+ }
+
+ public void updateProgress(int message, int current, int total) {
+ if (mProgressDialogUpdater != null) {
+ mProgressDialogUpdater.setProgress(message, current, total);
+ }
+ }
+
+ public void updateProgress(int current, int total) {
+ if (mProgressDialogUpdater != null) {
+ mProgressDialogUpdater.setProgress(current, total);
+ }
+ }
+
+ /**
+ * Decrypts and/or verifies data based on parameters of class
+ *
+ * @return
+ * @throws IOException
+ * @throws PgpGeneralException
+ * @throws PGPException
+ * @throws SignatureException
+ */
+ public PgpDecryptVerifyResult execute()
+ throws IOException, PgpGeneralException, PGPException, SignatureException {
+ // automatically works with ascii armor input and binary
+ InputStream in = PGPUtil.getDecoderStream(mData.getInputStream());
+ if (in instanceof ArmoredInputStream) {
+ ArmoredInputStream aIn = (ArmoredInputStream) in;
+ // it is ascii armored
+ Log.d(Constants.TAG, "ASCII Armor Header Line: " + aIn.getArmorHeaderLine());
+
+ if (aIn.isClearText()) {
+ // a cleartext signature, verify it with the other method
+ return verifyCleartextSignature(aIn);
+ }
+ // else: ascii armored encryption! go on...
+ }
+
+ return decryptVerify(in);
+ }
+
+ /**
+ * Decrypt and/or verifies binary or ascii armored pgp
+ *
+ * @param in
+ * @return
+ * @throws IOException
+ * @throws PgpGeneralException
+ * @throws PGPException
+ * @throws SignatureException
+ */
+ private PgpDecryptVerifyResult decryptVerify(InputStream in)
+ throws IOException, PgpGeneralException, PGPException, SignatureException {
+ PgpDecryptVerifyResult returnData = new PgpDecryptVerifyResult();
+
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ PGPEncryptedDataList enc;
+ Object o = pgpF.nextObject();
+
+ int currentProgress = 0;
+ updateProgress(R.string.progress_reading_data, currentProgress, 100);
+
+ if (o instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) o;
+ } else {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+
+ if (enc == null) {
+ throw new PgpGeneralException(mContext.getString(R.string.error_invalid_data));
+ }
+
+ InputStream clear;
+ PGPEncryptedData encryptedData;
+
+ currentProgress += 5;
+
+ PGPPublicKeyEncryptedData encryptedDataAsymmetric = null;
+ PGPPBEEncryptedData encryptedDataSymmetric = null;
+ PGPSecretKey secretKey = null;
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ boolean symmetricPacketFound = false;
+ // find secret key
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPublicKeyEncryptedData) {
+ updateProgress(R.string.progress_finding_key, currentProgress, 100);
+
+ PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj;
+ long masterKeyId = ProviderHelper.getMasterKeyId(mContext,
+ KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(encData.getKeyID()))
+ );
+ PGPSecretKeyRing secretKeyRing = ProviderHelper.getPGPSecretKeyRing(mContext, masterKeyId);
+ if (secretKeyRing == null) {
+ throw new PgpGeneralException(mContext.getString(R.string.error_no_secret_key_found));
+ }
+ secretKey = secretKeyRing.getSecretKey(encData.getKeyID());
+ if (secretKey == null) {
+ throw new PgpGeneralException(mContext.getString(R.string.error_no_secret_key_found));
+ }
+ // secret key exists in database
+
+ // allow only a specific key for decryption?
+ if (mAllowedKeyIds != null) {
+ Log.d(Constants.TAG, "encData.getKeyID():" + encData.getKeyID());
+ Log.d(Constants.TAG, "allowedKeyIds: " + mAllowedKeyIds);
+ Log.d(Constants.TAG, "masterKeyId: " + masterKeyId);
+
+ if (!mAllowedKeyIds.contains(masterKeyId)) {
+ throw new PgpGeneralException(
+ mContext.getString(R.string.error_no_secret_key_found));
+ }
+ }
+
+ encryptedDataAsymmetric = encData;
+
+ // if no passphrase was explicitly set try to get it from the cache service
+ if (mPassphrase == null) {
+ // returns "" if key has no passphrase
+ mPassphrase =
+ PassphraseCacheService.getCachedPassphrase(mContext, masterKeyId);
+
+ // if passphrase was not cached, return here
+ // indicating that a passphrase is missing!
+ if (mPassphrase == null) {
+ returnData.setKeyIdPassphraseNeeded(masterKeyId);
+ returnData.setStatus(PgpDecryptVerifyResult.KEY_PASSHRASE_NEEDED);
+ return returnData;
+ }
+ }
+
+ // break out of while, only get first object here
+ // TODO???: There could be more pgp objects, which are not decrypted!
+ break;
+ } else if (mAllowSymmetricDecryption && obj instanceof PGPPBEEncryptedData) {
+ symmetricPacketFound = true;
+
+ encryptedDataSymmetric = (PGPPBEEncryptedData) obj;
+
+ // if no passphrase is given, return here
+ // indicating that a passphrase is missing!
+ if (mPassphrase == null) {
+ returnData.setStatus(PgpDecryptVerifyResult.SYMMETRIC_PASSHRASE_NEEDED);
+ return returnData;
+ }
+
+ // break out of while, only get first object here
+ // TODO???: There could be more pgp objects, which are not decrypted!
+ break;
+ }
+ }
+
+ if (symmetricPacketFound) {
+ updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
+
+ PGPDigestCalculatorProvider digestCalcProvider = new JcaPGPDigestCalculatorProviderBuilder()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build();
+ PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder(
+ digestCalcProvider).setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ mPassphrase.toCharArray());
+
+ clear = encryptedDataSymmetric.getDataStream(decryptorFactory);
+
+ encryptedData = encryptedDataSymmetric;
+ currentProgress += 5;
+ } else {
+ if (secretKey == null) {
+ throw new PgpGeneralException(mContext.getString(R.string.error_no_secret_key_found));
+ }
+
+ currentProgress += 5;
+ updateProgress(R.string.progress_extracting_key, currentProgress, 100);
+ PGPPrivateKey privateKey;
+ try {
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ mPassphrase.toCharArray());
+ privateKey = secretKey.extractPrivateKey(keyDecryptor);
+ } catch (PGPException e) {
+ throw new PGPException(mContext.getString(R.string.error_wrong_passphrase));
+ }
+ if (privateKey == null) {
+ throw new PgpGeneralException(
+ mContext.getString(R.string.error_could_not_extract_private_key));
+ }
+ currentProgress += 5;
+ updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
+
+ PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(privateKey);
+
+ clear = encryptedDataAsymmetric.getDataStream(decryptorFactory);
+
+ encryptedData = encryptedDataAsymmetric;
+ currentProgress += 5;
+ }
+
+ PGPObjectFactory plainFact = new PGPObjectFactory(clear);
+ Object dataChunk = plainFact.nextObject();
+ PGPOnePassSignature signature = null;
+ OpenPgpSignatureResult signatureResult = null;
+ PGPPublicKey signatureKey = null;
+ int signatureIndex = -1;
+
+ if (dataChunk instanceof PGPCompressedData) {
+ updateProgress(R.string.progress_decompressing_data, currentProgress, 100);
+
+ PGPObjectFactory fact = new PGPObjectFactory(
+ ((PGPCompressedData) dataChunk).getDataStream());
+ dataChunk = fact.nextObject();
+ plainFact = fact;
+ currentProgress += 10;
+ }
+
+ long signatureKeyId = 0;
+ if (dataChunk instanceof PGPOnePassSignatureList) {
+ updateProgress(R.string.progress_processing_signature, currentProgress, 100);
+
+ signatureResult = new OpenPgpSignatureResult();
+ PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk;
+ for (int i = 0; i < sigList.size(); ++i) {
+ signature = sigList.get(i);
+ signatureKey = ProviderHelper
+ .getPGPPublicKeyRing(mContext, signature.getKeyID()).getPublicKey();
+ if (signatureKeyId == 0) {
+ signatureKeyId = signature.getKeyID();
+ }
+ if (signatureKey == null) {
+ signature = null;
+ } else {
+ signatureIndex = i;
+ signatureKeyId = signature.getKeyID();
+ String userId = null;
+ PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingWithKeyId(
+ mContext, signatureKeyId);
+ if (signKeyRing != null) {
+ userId = PgpKeyHelper.getMainUserId(signKeyRing.getPublicKey());
+ }
+ signatureResult.setUserId(userId);
+ break;
+ }
+ }
+
+ signatureResult.setKeyId(signatureKeyId);
+
+ if (signature != null) {
+ JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider =
+ new JcaPGPContentVerifierBuilderProvider()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ signature.init(contentVerifierBuilderProvider, signatureKey);
+ } else {
+ signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY);
+ }
+
+ dataChunk = plainFact.nextObject();
+ currentProgress += 10;
+ }
+
+ if (dataChunk instanceof PGPSignatureList) {
+ dataChunk = plainFact.nextObject();
+ }
+
+ if (dataChunk instanceof PGPLiteralData) {
+ updateProgress(R.string.progress_decrypting, currentProgress, 100);
+
+ PGPLiteralData literalData = (PGPLiteralData) dataChunk;
+
+ byte[] buffer = new byte[1 << 16];
+ InputStream dataIn = literalData.getInputStream();
+
+ int startProgress = currentProgress;
+ int endProgress = 100;
+ if (signature != null) {
+ endProgress = 90;
+ } else if (encryptedData.isIntegrityProtected()) {
+ endProgress = 95;
+ }
+
+ int n;
+ // TODO: progress calculation is broken here! Try to rework it based on commented code!
+// int progress = 0;
+ long startPos = mData.getStreamPosition();
+ while ((n = dataIn.read(buffer)) > 0) {
+ mOutStream.write(buffer, 0, n);
+// progress += n;
+ if (signature != null) {
+ try {
+ signature.update(buffer, 0, n);
+ } catch (SignatureException e) {
+ signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_ERROR);
+ signature = null;
+ }
+ }
+ // TODO: dead code?!
+ // unknown size, but try to at least have a moving, slowing down progress bar
+// currentProgress = startProgress + (endProgress - startProgress) * progress
+// / (progress + 100000);
+ if (mData.getSize() - startPos == 0) {
+ currentProgress = endProgress;
+ } else {
+ currentProgress = (int) (startProgress + (endProgress - startProgress)
+ * (mData.getStreamPosition() - startPos) / (mData.getSize() - startPos));
+ }
+ updateProgress(currentProgress, 100);
+ }
+
+ if (signature != null) {
+ updateProgress(R.string.progress_verifying_signature, 90, 100);
+
+ PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject();
+ PGPSignature messageSignature = signatureList.get(signatureIndex);
+
+ // these are not cleartext signatures!
+ // TODO: what about binary signatures?
+ signatureResult.setSignatureOnly(false);
+
+ //Now check binding signatures
+ boolean validKeyBinding = verifyKeyBinding(mContext, messageSignature, signatureKey);
+ boolean validSignature = signature.verify(messageSignature);
+
+ // TODO: implement CERTIFIED!
+ if (validKeyBinding & validSignature) {
+ signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED);
+ }
+ }
+ }
+
+ if (encryptedData.isIntegrityProtected()) {
+ updateProgress(R.string.progress_verifying_integrity, 95, 100);
+
+ if (encryptedData.verify()) {
+ // passed
+ Log.d(Constants.TAG, "Integrity verification: success!");
+ } else {
+ // failed
+ Log.d(Constants.TAG, "Integrity verification: failed!");
+ throw new PgpGeneralException(mContext.getString(R.string.error_integrity_check_failed));
+ }
+ } else {
+ // no integrity check
+ Log.e(Constants.TAG, "Encrypted data was not integrity protected!");
+ // TODO: inform user?
+ }
+
+ updateProgress(R.string.progress_done, 100, 100);
+
+ returnData.setSignatureResult(signatureResult);
+ return returnData;
+ }
+
+ /**
+ * This method verifies cleartext signatures
+ * as defined in http://tools.ietf.org/html/rfc4880#section-7
+ * <p/>
+ * The method is heavily based on
+ * pg/src/main/java/org/spongycastle/openpgp/examples/ClearSignedFileProcessor.java
+ *
+ * @return
+ * @throws IOException
+ * @throws PgpGeneralException
+ * @throws PGPException
+ * @throws SignatureException
+ */
+ private PgpDecryptVerifyResult verifyCleartextSignature(ArmoredInputStream aIn)
+ throws IOException, PgpGeneralException, PGPException, SignatureException {
+ PgpDecryptVerifyResult returnData = new PgpDecryptVerifyResult();
+ OpenPgpSignatureResult signatureResult = new OpenPgpSignatureResult();
+ // cleartext signatures are never encrypted ;)
+ signatureResult.setSignatureOnly(true);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ updateProgress(R.string.progress_done, 0, 100);
+
+ ByteArrayOutputStream lineOut = new ByteArrayOutputStream();
+ int lookAhead = readInputLine(lineOut, aIn);
+ byte[] lineSep = getLineSeparator();
+
+ byte[] line = lineOut.toByteArray();
+ out.write(line, 0, getLengthWithoutSeparator(line));
+ out.write(lineSep);
+
+ while (lookAhead != -1 && aIn.isClearText()) {
+ lookAhead = readInputLine(lineOut, lookAhead, aIn);
+ line = lineOut.toByteArray();
+ out.write(line, 0, getLengthWithoutSeparator(line));
+ out.write(lineSep);
+ }
+
+ out.close();
+
+ byte[] clearText = out.toByteArray();
+ mOutStream.write(clearText);
+
+ updateProgress(R.string.progress_processing_signature, 60, 100);
+ PGPObjectFactory pgpFact = new PGPObjectFactory(aIn);
+
+ PGPSignatureList sigList = (PGPSignatureList) pgpFact.nextObject();
+ if (sigList == null) {
+ throw new PgpGeneralException(mContext.getString(R.string.error_corrupt_data));
+ }
+ PGPSignature signature = null;
+ long signatureKeyId = 0;
+ PGPPublicKey signatureKey = null;
+ for (int i = 0; i < sigList.size(); ++i) {
+
+ signature = sigList.get(i);
+ signatureKeyId = signature.getKeyID();
+
+ // find data about this subkey
+ HashMap<String, Object> data = ProviderHelper.getGenericData(mContext,
+ KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(signature.getKeyID())),
+ new String[] { KeyRings.MASTER_KEY_ID, KeyRings.USER_ID },
+ new int[] { ProviderHelper.FIELD_TYPE_INTEGER, ProviderHelper.FIELD_TYPE_STRING });
+ // any luck? otherwise, try next.
+ if(data.get(KeyRings.MASTER_KEY_ID) == null) {
+ signature = null;
+ // do NOT reset signatureKeyId, that one is shown when no known one is found!
+ continue;
+ }
+
+ // this one can't fail now (yay database constraints)
+ signatureKey = ProviderHelper.getPGPPublicKeyRing(mContext, (Long) data.get(KeyRings.MASTER_KEY_ID)).getPublicKey();
+ signatureResult.setUserId((String) data.get(KeyRings.USER_ID));
+
+ break;
+ }
+
+ signatureResult.setKeyId(signatureKeyId);
+
+ if (signature == null) {
+ signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY);
+ returnData.setSignatureResult(signatureResult);
+
+ updateProgress(R.string.progress_done, 100, 100);
+ return returnData;
+ }
+
+ JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider =
+ new JcaPGPContentVerifierBuilderProvider()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ signature.init(contentVerifierBuilderProvider, signatureKey);
+
+ InputStream sigIn = new BufferedInputStream(new ByteArrayInputStream(clearText));
+
+ lookAhead = readInputLine(lineOut, sigIn);
+
+ processLine(signature, lineOut.toByteArray());
+
+ if (lookAhead != -1) {
+ do {
+ lookAhead = readInputLine(lineOut, lookAhead, sigIn);
+
+ signature.update((byte) '\r');
+ signature.update((byte) '\n');
+
+ processLine(signature, lineOut.toByteArray());
+ } while (lookAhead != -1);
+ }
+
+ //Now check binding signatures
+ boolean validKeyBinding = verifyKeyBinding(mContext, signature, signatureKey);
+ boolean validSignature = signature.verify();
+
+ if (validSignature & validKeyBinding) {
+ signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED);
+ }
+
+ // TODO: what about SIGNATURE_SUCCESS_CERTIFIED and SIGNATURE_ERROR????
+
+ returnData.setSignatureResult(signatureResult);
+
+ updateProgress(R.string.progress_done, 100, 100);
+ return returnData;
+ }
+
+ private static boolean verifyKeyBinding(Context context,
+ PGPSignature signature, PGPPublicKey signatureKey) {
+ long signatureKeyId = signature.getKeyID();
+ boolean validKeyBinding = false;
+
+ PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingWithKeyId(context,
+ signatureKeyId);
+ PGPPublicKey mKey = null;
+ if (signKeyRing != null) {
+ mKey = signKeyRing.getPublicKey();
+ }
+
+ if (signature.getKeyID() != mKey.getKeyID()) {
+ validKeyBinding = verifyKeyBinding(mKey, signatureKey);
+ } else { //if the key used to make the signature was the master key, no need to check binding sigs
+ validKeyBinding = true;
+ }
+ return validKeyBinding;
+ }
+
+ private static boolean verifyKeyBinding(PGPPublicKey masterPublicKey, PGPPublicKey signingPublicKey) {
+ boolean validSubkeyBinding = false;
+ boolean validTempSubkeyBinding = false;
+ boolean validPrimaryKeyBinding = false;
+
+ JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider =
+ new JcaPGPContentVerifierBuilderProvider()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ Iterator<PGPSignature> itr = signingPublicKey.getSignatures();
+
+ while (itr.hasNext()) { //what does gpg do if the subkey binding is wrong?
+ //gpg has an invalid subkey binding error on key import I think, but doesn't shout
+ //about keys without subkey signing. Can't get it to import a slightly broken one
+ //either, so we will err on bad subkey binding here.
+ PGPSignature sig = itr.next();
+ if (sig.getKeyID() == masterPublicKey.getKeyID() &&
+ sig.getSignatureType() == PGPSignature.SUBKEY_BINDING) {
+ //check and if ok, check primary key binding.
+ try {
+ sig.init(contentVerifierBuilderProvider, masterPublicKey);
+ validTempSubkeyBinding = sig.verifyCertification(masterPublicKey, signingPublicKey);
+ } catch (PGPException e) {
+ continue;
+ } catch (SignatureException e) {
+ continue;
+ }
+
+ if (validTempSubkeyBinding) {
+ validSubkeyBinding = true;
+ }
+ if (validTempSubkeyBinding) {
+ validPrimaryKeyBinding = verifyPrimaryKeyBinding(sig.getUnhashedSubPackets(),
+ masterPublicKey, signingPublicKey);
+ if (validPrimaryKeyBinding) {
+ break;
+ }
+ validPrimaryKeyBinding = verifyPrimaryKeyBinding(sig.getHashedSubPackets(),
+ masterPublicKey, signingPublicKey);
+ if (validPrimaryKeyBinding) {
+ break;
+ }
+ }
+ }
+ }
+ return (validSubkeyBinding & validPrimaryKeyBinding);
+ }
+
+ private static boolean verifyPrimaryKeyBinding(PGPSignatureSubpacketVector pkts,
+ PGPPublicKey masterPublicKey,
+ PGPPublicKey signingPublicKey) {
+ boolean validPrimaryKeyBinding = false;
+ JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider =
+ new JcaPGPContentVerifierBuilderProvider()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ PGPSignatureList eSigList;
+
+ if (pkts.hasSubpacket(SignatureSubpacketTags.EMBEDDED_SIGNATURE)) {
+ try {
+ eSigList = pkts.getEmbeddedSignatures();
+ } catch (IOException e) {
+ return false;
+ } catch (PGPException e) {
+ return false;
+ }
+ for (int j = 0; j < eSigList.size(); ++j) {
+ PGPSignature emSig = eSigList.get(j);
+ if (emSig.getSignatureType() == PGPSignature.PRIMARYKEY_BINDING) {
+ try {
+ emSig.init(contentVerifierBuilderProvider, signingPublicKey);
+ validPrimaryKeyBinding = emSig.verifyCertification(masterPublicKey, signingPublicKey);
+ if (validPrimaryKeyBinding) {
+ break;
+ }
+ } catch (PGPException e) {
+ continue;
+ } catch (SignatureException e) {
+ continue;
+ }
+ }
+ }
+ }
+
+ return validPrimaryKeyBinding;
+ }
+
+ /**
+ * Mostly taken from ClearSignedFileProcessor in Bouncy Castle
+ *
+ * @param sig
+ * @param line
+ * @throws SignatureException
+ */
+ private static void processLine(PGPSignature sig, byte[] line)
+ throws SignatureException {
+ int length = getLengthWithoutWhiteSpace(line);
+ if (length > 0) {
+ sig.update(line, 0, length);
+ }
+ }
+
+ private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn)
+ throws IOException {
+ bOut.reset();
+
+ int lookAhead = -1;
+ int ch;
+
+ while ((ch = fIn.read()) >= 0) {
+ bOut.write(ch);
+ if (ch == '\r' || ch == '\n') {
+ lookAhead = readPassedEOL(bOut, ch, fIn);
+ break;
+ }
+ }
+
+ return lookAhead;
+ }
+
+ private static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn)
+ throws IOException {
+ bOut.reset();
+
+ int ch = lookAhead;
+
+ do {
+ bOut.write(ch);
+ if (ch == '\r' || ch == '\n') {
+ lookAhead = readPassedEOL(bOut, ch, fIn);
+ break;
+ }
+ } while ((ch = fIn.read()) >= 0);
+
+ if (ch < 0) {
+ lookAhead = -1;
+ }
+
+ return lookAhead;
+ }
+
+ private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn)
+ throws IOException {
+ int lookAhead = fIn.read();
+
+ if (lastCh == '\r' && lookAhead == '\n') {
+ bOut.write(lookAhead);
+ lookAhead = fIn.read();
+ }
+
+ return lookAhead;
+ }
+
+ private static int getLengthWithoutSeparator(byte[] line) {
+ int end = line.length - 1;
+
+ while (end >= 0 && isLineEnding(line[end])) {
+ end--;
+ }
+
+ return end + 1;
+ }
+
+ private static boolean isLineEnding(byte b) {
+ return b == '\r' || b == '\n';
+ }
+
+ private static int getLengthWithoutWhiteSpace(byte[] line) {
+ int end = line.length - 1;
+
+ while (end >= 0 && isWhiteSpace(line[end])) {
+ end--;
+ }
+
+ return end + 1;
+ }
+
+ private static boolean isWhiteSpace(byte b) {
+ return b == '\r' || b == '\n' || b == '\t' || b == ' ';
+ }
+
+ private static byte[] getLineSeparator() {
+ String nl = System.getProperty("line.separator");
+ byte[] nlBytes = new byte[nl.length()];
+
+ for (int i = 0; i != nlBytes.length; i++) {
+ nlBytes[i] = (byte) nl.charAt(i);
+ }
+
+ return nlBytes;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyResult.java
new file mode 100644
index 000000000..ad240e834
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyResult.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.pgp;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.openintents.openpgp.OpenPgpSignatureResult;
+
+public class PgpDecryptVerifyResult implements Parcelable {
+ public static final int SUCCESS = 1;
+ public static final int KEY_PASSHRASE_NEEDED = 2;
+ public static final int SYMMETRIC_PASSHRASE_NEEDED = 3;
+
+ int mStatus;
+ long mKeyIdPassphraseNeeded;
+
+ OpenPgpSignatureResult mSignatureResult;
+
+ public int getStatus() {
+ return mStatus;
+ }
+
+ public void setStatus(int mStatus) {
+ this.mStatus = mStatus;
+ }
+
+ public long getKeyIdPassphraseNeeded() {
+ return mKeyIdPassphraseNeeded;
+ }
+
+ public void setKeyIdPassphraseNeeded(long mKeyIdPassphraseNeeded) {
+ this.mKeyIdPassphraseNeeded = mKeyIdPassphraseNeeded;
+ }
+
+ public OpenPgpSignatureResult getSignatureResult() {
+ return mSignatureResult;
+ }
+
+ public void setSignatureResult(OpenPgpSignatureResult signatureResult) {
+ this.mSignatureResult = signatureResult;
+ }
+
+ public PgpDecryptVerifyResult() {
+
+ }
+
+ public PgpDecryptVerifyResult(PgpDecryptVerifyResult b) {
+ this.mStatus = b.mStatus;
+ this.mKeyIdPassphraseNeeded = b.mKeyIdPassphraseNeeded;
+ this.mSignatureResult = b.mSignatureResult;
+ }
+
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStatus);
+ dest.writeLong(mKeyIdPassphraseNeeded);
+ dest.writeParcelable(mSignatureResult, 0);
+ }
+
+ public static final Creator<PgpDecryptVerifyResult> CREATOR = new Creator<PgpDecryptVerifyResult>() {
+ public PgpDecryptVerifyResult createFromParcel(final Parcel source) {
+ PgpDecryptVerifyResult vr = new PgpDecryptVerifyResult();
+ vr.mStatus = source.readInt();
+ vr.mKeyIdPassphraseNeeded = source.readLong();
+ vr.mSignatureResult = source.readParcelable(OpenPgpSignatureResult.class.getClassLoader());
+ return vr;
+ }
+
+ public PgpDecryptVerifyResult[] newArray(final int size) {
+ return new PgpDecryptVerifyResult[size];
+ }
+ };
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java
new file mode 100644
index 000000000..f884b1776
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+
+import org.spongycastle.openpgp.PGPEncryptedDataList;
+import org.spongycastle.openpgp.PGPObjectFactory;
+import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPUtil;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.exception.NoAsymmetricEncryptionException;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.security.SecureRandom;
+import java.util.Iterator;
+import java.util.regex.Pattern;
+
+public class PgpHelper {
+
+ public static final Pattern PGP_MESSAGE = Pattern.compile(
+ ".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL);
+
+ public static final Pattern PGP_CLEARTEXT_SIGNATURE = Pattern
+ .compile(".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----" +
+ "BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
+ Pattern.DOTALL);
+
+ public static final Pattern PGP_PUBLIC_KEY = Pattern.compile(
+ ".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----).*",
+ Pattern.DOTALL);
+
+ public static String getVersion(Context context) {
+ String version = null;
+ try {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(Constants.PACKAGE_NAME, 0);
+ version = pi.versionName;
+ return version;
+ } catch (NameNotFoundException e) {
+ Log.e(Constants.TAG, "Version could not be retrieved!", e);
+ return "0.0.0";
+ }
+ }
+
+ public static String getFullVersion(Context context) {
+ return "OpenPGP Keychain v" + getVersion(context);
+ }
+
+ public static long getDecryptionKeyId(Context context, InputStream inputStream)
+ throws PgpGeneralException, NoAsymmetricEncryptionException, IOException {
+ InputStream in = PGPUtil.getDecoderStream(inputStream);
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ PGPEncryptedDataList enc;
+ Object o = pgpF.nextObject();
+
+ // the first object might be a PGP marker packet.
+ if (o instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) o;
+ } else {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+
+ if (enc == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_invalid_data));
+ }
+
+ // TODO: currently we always only look at the first known key
+ // find the secret key
+ PGPSecretKey secretKey = null;
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ boolean gotAsymmetricEncryption = false;
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPublicKeyEncryptedData) {
+ gotAsymmetricEncryption = true;
+ PGPPublicKeyEncryptedData pbe = (PGPPublicKeyEncryptedData) obj;
+ secretKey = ProviderHelper.getPGPSecretKeyRing(context, pbe.getKeyID()).getSecretKey();
+ if (secretKey != null) {
+ break;
+ }
+ }
+ }
+
+ if (!gotAsymmetricEncryption) {
+ throw new NoAsymmetricEncryptionException();
+ }
+
+ if (secretKey == null) {
+ return Id.key.none;
+ }
+
+ return secretKey.getKeyID();
+ }
+
+ public static int getStreamContent(Context context, InputStream inStream) throws IOException {
+ InputStream in = PGPUtil.getDecoderStream(inStream);
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ Object object = pgpF.nextObject();
+ while (object != null) {
+ if (object instanceof PGPPublicKeyRing || object instanceof PGPSecretKeyRing) {
+ return Id.content.keys;
+ } else if (object instanceof PGPEncryptedDataList) {
+ return Id.content.encrypted_data;
+ }
+ object = pgpF.nextObject();
+ }
+
+ return Id.content.unknown;
+ }
+
+ /**
+ * Generate a random filename
+ *
+ * @param length
+ * @return
+ */
+ public static String generateRandomFilename(int length) {
+ SecureRandom random = new SecureRandom();
+
+ byte bytes[] = new byte[length];
+ random.nextBytes(bytes);
+ String result = "";
+ for (int i = 0; i < length; ++i) {
+ int v = (bytes[i] + 256) % 64;
+ if (v < 10) {
+ result += (char) ('0' + v);
+ } else if (v < 36) {
+ result += (char) ('A' + v - 10);
+ } else if (v < 62) {
+ result += (char) ('a' + v - 36);
+ } else if (v == 62) {
+ result += '_';
+ } else if (v == 63) {
+ result += '.';
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Go once through stream to get length of stream. The length is later used to display progress
+ * when encrypting/decrypting
+ *
+ * @param in
+ * @return
+ * @throws IOException
+ */
+ public static long getLengthOfStream(InputStream in) throws IOException {
+ long size = 0;
+ long n = 0;
+ byte dummy[] = new byte[0x10000];
+ while ((n = in.read(dummy)) > 0) {
+ size += n;
+ }
+ return size;
+ }
+
+ /**
+ * Deletes file securely by overwriting it with random data before deleting it.
+ * <p/>
+ * TODO: Does this really help on flash storage?
+ *
+ * @param context
+ * @param progress
+ * @param file
+ * @throws IOException
+ */
+ public static void deleteFileSecurely(Context context, ProgressDialogUpdater progress, File file)
+ throws IOException {
+ long length = file.length();
+ SecureRandom random = new SecureRandom();
+ RandomAccessFile raf = new RandomAccessFile(file, "rws");
+ raf.seek(0);
+ raf.getFilePointer();
+ byte[] data = new byte[1 << 16];
+ int pos = 0;
+ String msg = context.getString(R.string.progress_deleting_securely, file.getName());
+ while (pos < length) {
+ if (progress != null) {
+ progress.setProgress(msg, (int) (100 * pos / length), 100);
+ }
+ random.nextBytes(data);
+ raf.write(data);
+ pos += data.length;
+ }
+ raf.close();
+ file.delete();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpImportExport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpImportExport.java
new file mode 100644
index 000000000..d03f3ccc2
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpImportExport.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Environment;
+
+import org.spongycastle.bcpg.ArmoredOutputStream;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
+import org.sufficientlysecure.keychain.util.HkpKeyServer;
+import org.sufficientlysecure.keychain.util.IterableIterator;
+import org.sufficientlysecure.keychain.util.KeyServer.AddKeyException;
+import org.sufficientlysecure.keychain.util.KeychainServiceListener;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PgpImportExport {
+
+ private Context mContext;
+ private ProgressDialogUpdater mProgress;
+
+ private KeychainServiceListener mKeychainServiceListener;
+
+ public PgpImportExport(Context context, ProgressDialogUpdater progress) {
+ super();
+ this.mContext = context;
+ this.mProgress = progress;
+ }
+
+ public PgpImportExport(Context context,
+ ProgressDialogUpdater progress, KeychainServiceListener keychainListener) {
+ super();
+ this.mContext = context;
+ this.mProgress = progress;
+ this.mKeychainServiceListener = keychainListener;
+ }
+
+ public void updateProgress(int message, int current, int total) {
+ if (mProgress != null) {
+ mProgress.setProgress(message, current, total);
+ }
+ }
+
+ public void updateProgress(String message, int current, int total) {
+ if (mProgress != null) {
+ mProgress.setProgress(message, current, total);
+ }
+ }
+
+ public void updateProgress(int current, int total) {
+ if (mProgress != null) {
+ mProgress.setProgress(current, total);
+ }
+ }
+
+ public boolean uploadKeyRingToServer(HkpKeyServer server, PGPPublicKeyRing keyring) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ArmoredOutputStream aos = null;
+ try {
+ aos = new ArmoredOutputStream(bos);
+ aos.write(keyring.getEncoded());
+ aos.close();
+
+ String armoredKey = bos.toString("UTF-8");
+ server.add(armoredKey);
+
+ return true;
+ } catch (IOException e) {
+ return false;
+ } catch (AddKeyException e) {
+ // TODO: tell the user?
+ return false;
+ } finally {
+ try {
+ if (aos != null) { aos.close(); }
+ if (bos != null) { bos.close(); }
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ /**
+ * Imports keys from given data. If keyIds is given only those are imported
+ */
+ public Bundle importKeyRings(List<ImportKeysListEntry> entries)
+ throws PgpGeneralException, PGPException, IOException {
+ Bundle returnData = new Bundle();
+
+ updateProgress(R.string.progress_importing, 0, 100);
+
+ int newKeys = 0;
+ int oldKeys = 0;
+ int badKeys = 0;
+
+ int position = 0;
+ try {
+ for (ImportKeysListEntry entry : entries) {
+ Object obj = PgpConversionHelper.BytesToPGPKeyRing(entry.getBytes());
+
+ if (obj instanceof PGPKeyRing) {
+ PGPKeyRing keyring = (PGPKeyRing) obj;
+
+ int status = storeKeyRingInCache(keyring);
+
+ if (status == Id.return_value.error) {
+ throw new PgpGeneralException(
+ mContext.getString(R.string.error_saving_keys));
+ }
+
+ // update the counts to display to the user at the end
+ if (status == Id.return_value.updated) {
+ ++oldKeys;
+ } else if (status == Id.return_value.ok) {
+ ++newKeys;
+ } else if (status == Id.return_value.bad) {
+ ++badKeys;
+ }
+ } else {
+ Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!");
+ }
+
+ position++;
+ updateProgress(position / entries.size() * 100, 100);
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Exception on parsing key file!", e);
+ }
+
+ returnData.putInt(KeychainIntentService.RESULT_IMPORT_ADDED, newKeys);
+ returnData.putInt(KeychainIntentService.RESULT_IMPORT_UPDATED, oldKeys);
+ returnData.putInt(KeychainIntentService.RESULT_IMPORT_BAD, badKeys);
+
+ return returnData;
+ }
+
+ public Bundle exportKeyRings(ArrayList<Long> publicKeyRingMasterIds,
+ ArrayList<Long> secretKeyRingMasterIds,
+ OutputStream outStream) throws PgpGeneralException,
+ PGPException, IOException {
+ Bundle returnData = new Bundle();
+
+ int masterKeyIdsSize = publicKeyRingMasterIds.size() + secretKeyRingMasterIds.size();
+ int progress = 0;
+
+ updateProgress(
+ mContext.getResources().getQuantityString(R.plurals.progress_exporting_key,
+ masterKeyIdsSize), 0, 100);
+
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new PgpGeneralException(
+ mContext.getString(R.string.error_external_storage_not_ready));
+ }
+ // For each public masterKey id
+ for (long pubKeyMasterId : publicKeyRingMasterIds) {
+ progress++;
+ // Create an output stream
+ ArmoredOutputStream arOutStream = new ArmoredOutputStream(outStream);
+ arOutStream.setHeader("Version", PgpHelper.getFullVersion(mContext));
+
+ updateProgress(progress * 100 / masterKeyIdsSize, 100);
+ PGPPublicKeyRing publicKeyRing =
+ ProviderHelper.getPGPPublicKeyRing(mContext, pubKeyMasterId);
+
+ if (publicKeyRing != null) {
+ publicKeyRing.encode(arOutStream);
+ }
+
+ if (mKeychainServiceListener.hasServiceStopped()) {
+ arOutStream.close();
+ return null;
+ }
+
+ arOutStream.close();
+ }
+
+ // For each secret masterKey id
+ for (long secretKeyMasterId : secretKeyRingMasterIds) {
+ progress++;
+ // Create an output stream
+ ArmoredOutputStream arOutStream = new ArmoredOutputStream(outStream);
+ arOutStream.setHeader("Version", PgpHelper.getFullVersion(mContext));
+
+ updateProgress(progress * 100 / masterKeyIdsSize, 100);
+ PGPSecretKeyRing secretKeyRing =
+ ProviderHelper.getPGPSecretKeyRing(mContext, secretKeyMasterId);
+
+ if (secretKeyRing != null) {
+ secretKeyRing.encode(arOutStream);
+ }
+ if (mKeychainServiceListener.hasServiceStopped()) {
+ arOutStream.close();
+ return null;
+ }
+
+ arOutStream.close();
+ }
+
+ returnData.putInt(KeychainIntentService.RESULT_EXPORT, masterKeyIdsSize);
+
+ updateProgress(R.string.progress_done, 100, 100);
+
+ return returnData;
+ }
+
+ /**
+ * TODO: implement Id.return_value.updated as status when key already existed
+ */
+ @SuppressWarnings("unchecked")
+ public int storeKeyRingInCache(PGPKeyRing keyring) {
+ int status = Integer.MIN_VALUE; // out of bounds value (Id.return_value.*)
+ try {
+ if (keyring instanceof PGPSecretKeyRing) {
+ PGPSecretKeyRing secretKeyRing = (PGPSecretKeyRing) keyring;
+ boolean save = true;
+
+ for (PGPSecretKey testSecretKey : new IterableIterator<PGPSecretKey>(
+ secretKeyRing.getSecretKeys())) {
+ if (!testSecretKey.isMasterKey()) {
+ if (testSecretKey.isPrivateKeyEmpty()) {
+ // this is bad, something is very wrong...
+ save = false;
+ status = Id.return_value.bad;
+ }
+ }
+ }
+
+ if (save) {
+ // TODO: preserve certifications
+ // (http://osdir.com/ml/encryption.bouncy-castle.devel/2007-01/msg00054.html ?)
+ PGPPublicKeyRing newPubRing = null;
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(
+ secretKeyRing.getPublicKeys())) {
+ if (newPubRing == null) {
+ newPubRing = new PGPPublicKeyRing(key.getEncoded(),
+ new JcaKeyFingerprintCalculator());
+ }
+ newPubRing = PGPPublicKeyRing.insertPublicKey(newPubRing, key);
+ }
+ if (newPubRing != null) {
+ ProviderHelper.saveKeyRing(mContext, newPubRing);
+ }
+ ProviderHelper.saveKeyRing(mContext, secretKeyRing);
+ // TODO: remove status returns, use exceptions!
+ status = Id.return_value.ok;
+ }
+ } else if (keyring instanceof PGPPublicKeyRing) {
+ PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
+ ProviderHelper.saveKeyRing(mContext, publicKeyRing);
+ // TODO: remove status returns, use exceptions!
+ status = Id.return_value.ok;
+ }
+ } catch (IOException e) {
+ status = Id.return_value.error;
+ }
+
+ return status;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java
new file mode 100644
index 000000000..4c786f555
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
+
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
+import org.spongycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.IterableIterator;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.security.DigestException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.Vector;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PgpKeyHelper {
+
+ private static final Pattern USER_ID_PATTERN = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$");
+
+ public static Date getCreationDate(PGPPublicKey key) {
+ return key.getCreationTime();
+ }
+
+ public static Date getCreationDate(PGPSecretKey key) {
+ return key.getPublicKey().getCreationTime();
+ }
+
+ @SuppressWarnings("unchecked")
+ public static PGPSecretKey getKeyNum(PGPSecretKeyRing keyRing, long num) {
+ long cnt = 0;
+ if (keyRing == null) {
+ return null;
+ }
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ if (cnt == num) {
+ return key;
+ }
+ cnt++;
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Vector<PGPPublicKey> getEncryptKeys(PGPPublicKeyRing keyRing) {
+ Vector<PGPPublicKey> encryptKeys = new Vector<PGPPublicKey>();
+
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ if (isEncryptionKey(key)) {
+ encryptKeys.add(key);
+ }
+ }
+
+ return encryptKeys;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Vector<PGPSecretKey> getSigningKeys(PGPSecretKeyRing keyRing) {
+ Vector<PGPSecretKey> signingKeys = new Vector<PGPSecretKey>();
+
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ if (isSigningKey(key)) {
+ signingKeys.add(key);
+ }
+ }
+
+ return signingKeys;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Vector<PGPSecretKey> getCertificationKeys(PGPSecretKeyRing keyRing) {
+ Vector<PGPSecretKey> signingKeys = new Vector<PGPSecretKey>();
+
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ if (isCertificationKey(key)) {
+ signingKeys.add(key);
+ }
+ }
+
+ return signingKeys;
+ }
+
+ public static Vector<PGPPublicKey> getUsableEncryptKeys(PGPPublicKeyRing keyRing) {
+ Vector<PGPPublicKey> usableKeys = new Vector<PGPPublicKey>();
+ Vector<PGPPublicKey> encryptKeys = getEncryptKeys(keyRing);
+ PGPPublicKey masterKey = null;
+ for (int i = 0; i < encryptKeys.size(); ++i) {
+ PGPPublicKey key = encryptKeys.get(i);
+ if (!isExpired(key) && !key.isRevoked()) {
+ if (key.isMasterKey()) {
+ masterKey = key;
+ } else {
+ usableKeys.add(key);
+ }
+ }
+ }
+ if (masterKey != null) {
+ usableKeys.add(masterKey);
+ }
+ return usableKeys;
+ }
+
+ public static boolean isExpired(PGPPublicKey key) {
+ Date creationDate = getCreationDate(key);
+ Date expiryDate = getExpiryDate(key);
+ Date now = new Date();
+ if (now.compareTo(creationDate) >= 0
+ && (expiryDate == null || now.compareTo(expiryDate) <= 0)) {
+ return false;
+ }
+ return true;
+ }
+
+ public static Vector<PGPSecretKey> getUsableCertificationKeys(PGPSecretKeyRing keyRing) {
+ Vector<PGPSecretKey> usableKeys = new Vector<PGPSecretKey>();
+ Vector<PGPSecretKey> signingKeys = getCertificationKeys(keyRing);
+ PGPSecretKey masterKey = null;
+ for (int i = 0; i < signingKeys.size(); ++i) {
+ PGPSecretKey key = signingKeys.get(i);
+ if (key.isMasterKey()) {
+ masterKey = key;
+ } else {
+ usableKeys.add(key);
+ }
+ }
+ if (masterKey != null) {
+ usableKeys.add(masterKey);
+ }
+ return usableKeys;
+ }
+
+ public static Vector<PGPSecretKey> getUsableSigningKeys(PGPSecretKeyRing keyRing) {
+ Vector<PGPSecretKey> usableKeys = new Vector<PGPSecretKey>();
+ Vector<PGPSecretKey> signingKeys = getSigningKeys(keyRing);
+ PGPSecretKey masterKey = null;
+ for (int i = 0; i < signingKeys.size(); ++i) {
+ PGPSecretKey key = signingKeys.get(i);
+ if (key.isMasterKey()) {
+ masterKey = key;
+ } else {
+ usableKeys.add(key);
+ }
+ }
+ if (masterKey != null) {
+ usableKeys.add(masterKey);
+ }
+ return usableKeys;
+ }
+
+ public static Date getExpiryDate(PGPPublicKey key) {
+ Date creationDate = getCreationDate(key);
+ if (key.getValidDays() == 0) {
+ // no expiry
+ return null;
+ }
+ Calendar calendar = GregorianCalendar.getInstance();
+ calendar.setTime(creationDate);
+ calendar.add(Calendar.DATE, key.getValidDays());
+
+ return calendar.getTime();
+ }
+
+ public static Date getExpiryDate(PGPSecretKey key) {
+ return getExpiryDate(key.getPublicKey());
+ }
+
+ public static PGPPublicKey getEncryptPublicKey(Context context, long masterKeyId) {
+ PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRing(context, masterKeyId);
+ if (keyRing == null) {
+ Log.e(Constants.TAG, "keyRing is null!");
+ return null;
+ }
+ Vector<PGPPublicKey> encryptKeys = getUsableEncryptKeys(keyRing);
+ if (encryptKeys.size() == 0) {
+ Log.e(Constants.TAG, "encryptKeys is null!");
+ return null;
+ }
+ return encryptKeys.get(0);
+ }
+
+ public static PGPSecretKey getCertificationKey(Context context, long masterKeyId) {
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRing(context, masterKeyId);
+ if (keyRing == null) {
+ return null;
+ }
+ Vector<PGPSecretKey> signingKeys = getUsableCertificationKeys(keyRing);
+ if (signingKeys.size() == 0) {
+ return null;
+ }
+ return signingKeys.get(0);
+ }
+
+ public static PGPSecretKey getSigningKey(Context context, long masterKeyId) {
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRing(context, masterKeyId);
+ if (keyRing == null) {
+ return null;
+ }
+ Vector<PGPSecretKey> signingKeys = getUsableSigningKeys(keyRing);
+ if (signingKeys.size() == 0) {
+ return null;
+ }
+ return signingKeys.get(0);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static String getMainUserId(PGPPublicKey key) {
+ for (String userId : new IterableIterator<String>(key.getUserIDs())) {
+ return userId;
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static String getMainUserId(PGPSecretKey key) {
+ for (String userId : new IterableIterator<String>(key.getUserIDs())) {
+ return userId;
+ }
+ return null;
+ }
+
+ public static String getMainUserIdSafe(Context context, PGPPublicKey key) {
+ String userId = getMainUserId(key);
+ if (userId == null || userId.equals("")) {
+ userId = context.getString(R.string.user_id_no_name);
+ }
+ return userId;
+ }
+
+ public static String getMainUserIdSafe(Context context, PGPSecretKey key) {
+ String userId = getMainUserId(key);
+ if (userId == null || userId.equals("")) {
+ userId = context.getString(R.string.user_id_no_name);
+ }
+ return userId;
+ }
+
+ public static int getKeyUsage(PGPSecretKey key) {
+ return getKeyUsage(key.getPublicKey());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static int getKeyUsage(PGPPublicKey key) {
+ int usage = 0;
+ if (key.getVersion() >= 4) {
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
+ continue;
+ }
+
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+ if (hashed != null) {
+ usage |= hashed.getKeyFlags();
+ }
+
+ PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
+ if (unhashed != null) {
+ usage |= unhashed.getKeyFlags();
+ }
+ }
+ }
+ return usage;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static boolean isEncryptionKey(PGPPublicKey key) {
+ if (!key.isEncryptionKey()) {
+ return false;
+ }
+
+ if (key.getVersion() <= 3) {
+ // this must be true now
+ return key.isEncryptionKey();
+ }
+
+ // special cases
+ if (key.getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT) {
+ return true;
+ }
+
+ if (key.getAlgorithm() == PGPPublicKey.RSA_ENCRYPT) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
+ continue;
+ }
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if (hashed != null
+ && (hashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
+ return true;
+ }
+
+ PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
+
+ if (unhashed != null
+ && (unhashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isEncryptionKey(PGPSecretKey key) {
+ return isEncryptionKey(key.getPublicKey());
+ }
+
+ @SuppressWarnings("unchecked")
+ public static boolean isSigningKey(PGPPublicKey key) {
+ if (key.getVersion() <= 3) {
+ return true;
+ }
+
+ // special case
+ if (key.getAlgorithm() == PGPPublicKey.RSA_SIGN) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
+ continue;
+ }
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if (hashed != null && (hashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
+ return true;
+ }
+
+ PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
+
+ if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static boolean isSigningKey(PGPSecretKey key) {
+ return isSigningKey(key.getPublicKey());
+ }
+
+ @SuppressWarnings("unchecked")
+ public static boolean isCertificationKey(PGPPublicKey key) {
+ if (key.getVersion() <= 3) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
+ continue;
+ }
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if (hashed != null && (hashed.getKeyFlags() & KeyFlags.CERTIFY_OTHER) != 0) {
+ return true;
+ }
+
+ PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
+
+ if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.CERTIFY_OTHER) != 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static boolean isAuthenticationKey(PGPSecretKey key) {
+ return isAuthenticationKey(key.getPublicKey());
+ }
+
+ @SuppressWarnings("unchecked")
+ public static boolean isAuthenticationKey(PGPPublicKey key) {
+ if (key.getVersion() <= 3) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
+ continue;
+ }
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if (hashed != null && (hashed.getKeyFlags() & KeyFlags.AUTHENTICATION) != 0) {
+ return true;
+ }
+
+ PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
+
+ if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.AUTHENTICATION) != 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static boolean isCertificationKey(PGPSecretKey key) {
+ return isCertificationKey(key.getPublicKey());
+ }
+
+ public static String getAlgorithmInfo(PGPPublicKey key) {
+ return getAlgorithmInfo(key.getAlgorithm(), key.getBitStrength());
+ }
+
+ public static String getAlgorithmInfo(PGPSecretKey key) {
+ return getAlgorithmInfo(key.getPublicKey());
+ }
+
+ public static String getAlgorithmInfo(int algorithm, int keySize) {
+ String algorithmStr;
+
+ switch (algorithm) {
+ case PGPPublicKey.RSA_ENCRYPT:
+ case PGPPublicKey.RSA_GENERAL:
+ case PGPPublicKey.RSA_SIGN: {
+ algorithmStr = "RSA";
+ break;
+ }
+ case PGPPublicKey.DSA: {
+ algorithmStr = "DSA";
+ break;
+ }
+
+ case PGPPublicKey.ELGAMAL_ENCRYPT:
+ case PGPPublicKey.ELGAMAL_GENERAL: {
+ algorithmStr = "ElGamal";
+ break;
+ }
+
+ default: {
+ algorithmStr = "Unknown";
+ break;
+ }
+ }
+ if(keySize > 0)
+ return algorithmStr + ", " + keySize + " bit";
+ else
+ return algorithmStr;
+ }
+
+ /**
+ * Converts fingerprint to hex (optional: with whitespaces after 4 characters)
+ * <p/>
+ * Fingerprint is shown using lowercase characters. Studies have shown that humans can
+ * better differentiate between numbers and letters when letters are lowercase.
+ *
+ * @param fingerprint
+ * @return
+ */
+ public static String convertFingerprintToHex(byte[] fingerprint) {
+ String hexString = Hex.toHexString(fingerprint);
+
+ return hexString;
+ }
+
+ /**
+ * Convert key id from long to 64 bit hex string
+ * <p/>
+ * V4: "The Key ID is the low-order 64 bits of the fingerprint"
+ * <p/>
+ * see http://tools.ietf.org/html/rfc4880#section-12.2
+ *
+ * @param keyId
+ * @return
+ */
+ public static String convertKeyIdToHex(long keyId) {
+ long upper = keyId >> 32;
+ if (upper == 0) {
+ // this is a short key id
+ return convertKeyIdToHexShort(keyId);
+ }
+ return "0x" + convertKeyIdToHex32bit(keyId >> 32) + convertKeyIdToHex32bit(keyId);
+ }
+
+ public static String convertKeyIdToHexShort(long keyId) {
+ return "0x" + convertKeyIdToHex32bit(keyId);
+ }
+
+ private static String convertKeyIdToHex32bit(long keyId) {
+ String hexString = Long.toHexString(keyId & 0xffffffffL).toLowerCase(Locale.US);
+ while (hexString.length() < 8) {
+ hexString = "0" + hexString;
+ }
+ return hexString;
+ }
+
+
+ public static SpannableStringBuilder colorizeFingerprint(String fingerprint) {
+ // split by 4 characters
+ fingerprint = fingerprint.replaceAll("(.{4})(?!$)", "$1 ");
+
+ // add line breaks to have a consistent "image" that can be recognized
+ char[] chars = fingerprint.toCharArray();
+ chars[24] = '\n';
+ fingerprint = String.valueOf(chars);
+
+ SpannableStringBuilder sb = new SpannableStringBuilder(fingerprint);
+ try {
+ // for each 4 characters of the fingerprint + 1 space
+ for (int i = 0; i < fingerprint.length(); i += 5) {
+ int spanEnd = Math.min(i + 4, fingerprint.length());
+ String fourChars = fingerprint.substring(i, spanEnd);
+
+ int raw = Integer.parseInt(fourChars, 16);
+ byte[] bytes = {(byte) ((raw >> 8) & 0xff - 128), (byte) (raw & 0xff - 128)};
+ int[] color = getRgbForData(bytes);
+ int r = color[0];
+ int g = color[1];
+ int b = color[2];
+
+ // we cannot change black by multiplication, so adjust it to an almost-black grey,
+ // which will then be brightened to the minimal brightness level
+ if (r == 0 && g == 0 && b == 0) {
+ r = 1;
+ g = 1;
+ b = 1;
+ }
+
+ // Convert rgb to brightness
+ double brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+
+ // If a color is too dark to be seen on black,
+ // then brighten it up to a minimal brightness.
+ if (brightness < 80) {
+ double factor = 80.0 / brightness;
+ r = Math.min(255, (int) (r * factor));
+ g = Math.min(255, (int) (g * factor));
+ b = Math.min(255, (int) (b * factor));
+
+ // If it is too light, then darken it to a respective maximal brightness.
+ } else if (brightness > 180) {
+ double factor = 180.0 / brightness;
+ r = (int) (r * factor);
+ g = (int) (g * factor);
+ b = (int) (b * factor);
+ }
+
+ // Create a foreground color with the 3 digest integers as RGB
+ // and then converting that int to hex to use as a color
+ sb.setSpan(new ForegroundColorSpan(Color.rgb(r, g, b)),
+ i, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Colorization failed", e);
+ // if anything goes wrong, then just display the fingerprint without colour,
+ // instead of partially correct colour or wrong colours
+ return new SpannableStringBuilder(fingerprint);
+ }
+
+ return sb;
+ }
+
+ /**
+ * Converts the given bytes to a unique RGB color using SHA1 algorithm
+ *
+ * @param bytes
+ * @return an integer array containing 3 numeric color representations (Red, Green, Black)
+ * @throws java.security.NoSuchAlgorithmException
+ * @throws java.security.DigestException
+ */
+ private static int[] getRgbForData(byte[] bytes) throws NoSuchAlgorithmException, DigestException {
+ MessageDigest md = MessageDigest.getInstance("SHA1");
+
+ md.update(bytes);
+ byte[] digest = md.digest();
+
+ int[] result = {((int) digest[0] + 256) % 256,
+ ((int) digest[1] + 256) % 256,
+ ((int) digest[2] + 256) % 256};
+ return result;
+ }
+
+ /**
+ * Splits userId string into naming part, email part, and comment part
+ *
+ * @param userId
+ * @return array with naming (0), email (1), comment (2)
+ */
+ public static String[] splitUserId(String userId) {
+ String[] result = new String[]{null, null, null};
+
+ if (userId == null || userId.equals("")) {
+ return result;
+ }
+
+ /*
+ * User ID matching:
+ * http://fiddle.re/t4p6f
+ *
+ * test cases:
+ * "Max Mustermann (this is a comment) <max@example.com>"
+ * "Max Mustermann <max@example.com>"
+ * "Max Mustermann (this is a comment)"
+ * "Max Mustermann [this is nothing]"
+ */
+ Matcher matcher = USER_ID_PATTERN.matcher(userId);
+ if (matcher.matches()) {
+ result[0] = matcher.group(1);
+ result[1] = matcher.group(3);
+ result[2] = matcher.group(2);
+ }
+
+ return result;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java
new file mode 100644
index 000000000..48b959738
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java
@@ -0,0 +1,769 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp;
+
+import android.util.Pair;
+
+import org.spongycastle.bcpg.CompressionAlgorithmTags;
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.spec.ElGamalParameterSpec;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPKeyPair;
+import org.spongycastle.openpgp.PGPKeyRingGenerator;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
+import org.spongycastle.openpgp.PGPUtil;
+import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.spongycastle.openpgp.operator.PBESecretKeyEncryptor;
+import org.spongycastle.openpgp.operator.PGPContentSignerBuilder;
+import org.spongycastle.openpgp.operator.PGPDigestCalculator;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralMsgIdException;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.util.IterableIterator;
+import org.sufficientlysecure.keychain.util.Primes;
+import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.SignatureException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TimeZone;
+
+/** This class is the single place where ALL operations that actually modify a PGP public or secret
+ * key take place.
+ *
+ * Note that no android specific stuff should be done here, ie no imports from com.android.
+ *
+ * All operations support progress reporting to a ProgressDialogUpdater passed on initialization.
+ * This indicator may be null.
+ *
+ */
+public class PgpKeyOperation {
+ private ProgressDialogUpdater mProgress;
+
+ private static final int[] PREFERRED_SYMMETRIC_ALGORITHMS = new int[]{
+ SymmetricKeyAlgorithmTags.AES_256, SymmetricKeyAlgorithmTags.AES_192,
+ SymmetricKeyAlgorithmTags.AES_128, SymmetricKeyAlgorithmTags.CAST5,
+ SymmetricKeyAlgorithmTags.TRIPLE_DES};
+ private static final int[] PREFERRED_HASH_ALGORITHMS = new int[]{HashAlgorithmTags.SHA1,
+ HashAlgorithmTags.SHA256, HashAlgorithmTags.RIPEMD160};
+ private static final int[] PREFERRED_COMPRESSION_ALGORITHMS = new int[]{
+ CompressionAlgorithmTags.ZLIB, CompressionAlgorithmTags.BZIP2,
+ CompressionAlgorithmTags.ZIP};
+
+ public PgpKeyOperation(ProgressDialogUpdater progress) {
+ super();
+ this.mProgress = progress;
+ }
+
+ void updateProgress(int message, int current, int total) {
+ if (mProgress != null) {
+ mProgress.setProgress(message, current, total);
+ }
+ }
+
+ void updateProgress(int current, int total) {
+ if (mProgress != null) {
+ mProgress.setProgress(current, total);
+ }
+ }
+
+ /**
+ * Creates new secret key.
+ *
+ * @param algorithmChoice
+ * @param keySize
+ * @param passphrase
+ * @param isMasterKey
+ * @return A newly created PGPSecretKey
+ * @throws NoSuchAlgorithmException
+ * @throws PGPException
+ * @throws NoSuchProviderException
+ * @throws PgpGeneralMsgIdException
+ * @throws InvalidAlgorithmParameterException
+ */
+
+ // TODO: key flags?
+ public PGPSecretKey createKey(int algorithmChoice, int keySize, String passphrase,
+ boolean isMasterKey)
+ throws NoSuchAlgorithmException, PGPException, NoSuchProviderException,
+ PgpGeneralMsgIdException, InvalidAlgorithmParameterException {
+
+ if (keySize < 512) {
+ throw new PgpGeneralMsgIdException(R.string.error_key_size_minimum512bit);
+ }
+
+ if (passphrase == null) {
+ passphrase = "";
+ }
+
+ int algorithm;
+ KeyPairGenerator keyGen;
+
+ switch (algorithmChoice) {
+ case Id.choice.algorithm.dsa: {
+ keyGen = KeyPairGenerator.getInstance("DSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ keyGen.initialize(keySize, new SecureRandom());
+ algorithm = PGPPublicKey.DSA;
+ break;
+ }
+
+ case Id.choice.algorithm.elgamal: {
+ if (isMasterKey) {
+ throw new PgpGeneralMsgIdException(R.string.error_master_key_must_not_be_el_gamal);
+ }
+ keyGen = KeyPairGenerator.getInstance("ElGamal", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ BigInteger p = Primes.getBestPrime(keySize);
+ BigInteger g = new BigInteger("2");
+
+ ElGamalParameterSpec elParams = new ElGamalParameterSpec(p, g);
+
+ keyGen.initialize(elParams);
+ algorithm = PGPPublicKey.ELGAMAL_ENCRYPT;
+ break;
+ }
+
+ case Id.choice.algorithm.rsa: {
+ keyGen = KeyPairGenerator.getInstance("RSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ keyGen.initialize(keySize, new SecureRandom());
+
+ algorithm = PGPPublicKey.RSA_GENERAL;
+ break;
+ }
+
+ default: {
+ throw new PgpGeneralMsgIdException(R.string.error_unknown_algorithm_choice);
+ }
+ }
+
+ // build new key pair
+ PGPKeyPair keyPair = new JcaPGPKeyPair(algorithm, keyGen.generateKeyPair(), new Date());
+
+ // define hashing and signing algos
+ PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
+ HashAlgorithmTags.SHA1);
+
+ // Build key encrypter and decrypter based on passphrase
+ PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
+ PGPEncryptedData.CAST5, sha1Calc)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
+
+ return new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(),
+ sha1Calc, isMasterKey, keyEncryptor);
+ }
+
+ public PGPSecretKeyRing changeSecretKeyPassphrase(PGPSecretKeyRing keyRing, String oldPassphrase,
+ String newPassphrase)
+ throws IOException, PGPException, NoSuchProviderException {
+
+ updateProgress(R.string.progress_building_key, 0, 100);
+ if (oldPassphrase == null) {
+ oldPassphrase = "";
+ }
+ if (newPassphrase == null) {
+ newPassphrase = "";
+ }
+
+ PGPSecretKeyRing newKeyRing = PGPSecretKeyRing.copyWithNewPassword(
+ keyRing,
+ new JcePBESecretKeyDecryptorBuilder(new JcaPGPDigestCalculatorProviderBuilder()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build()).setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassphrase.toCharArray()),
+ new JcePBESecretKeyEncryptorBuilder(keyRing.getSecretKey()
+ .getKeyEncryptionAlgorithm()).build(newPassphrase.toCharArray()));
+
+ return newKeyRing;
+
+ }
+
+ private Pair<PGPSecretKeyRing, PGPPublicKeyRing> buildNewSecretKey(
+ ArrayList<String> userIds, ArrayList<PGPSecretKey> keys,
+ ArrayList<GregorianCalendar> keysExpiryDates,
+ ArrayList<Integer> keysUsages,
+ String newPassphrase, String oldPassphrase)
+ throws PgpGeneralMsgIdException, PGPException, SignatureException, IOException {
+
+ int usageId = keysUsages.get(0);
+ boolean canSign;
+ String mainUserId = userIds.get(0);
+
+ PGPSecretKey masterKey = keys.get(0);
+
+ // this removes all userIds and certifications previously attached to the masterPublicKey
+ PGPPublicKey masterPublicKey = masterKey.getPublicKey();
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassphrase.toCharArray());
+ PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor);
+
+ updateProgress(R.string.progress_certifying_master_key, 20, 100);
+
+ for (String userId : userIds) {
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+
+ sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
+
+ PGPSignature certification = sGen.generateCertification(userId, masterPublicKey);
+ masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, certification);
+ }
+
+ PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
+
+ PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
+
+ hashedPacketsGen.setKeyFlags(true, usageId);
+
+ hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS);
+ hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
+ hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
+
+ if (keysExpiryDates.get(0) != null) {
+ GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ creationDate.setTime(masterPublicKey.getCreationTime());
+ GregorianCalendar expiryDate = keysExpiryDates.get(0);
+ //note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c
+ //here we purposefully ignore partial days in each date - long type has no fractional part!
+ long numDays = (expiryDate.getTimeInMillis() / 86400000) -
+ (creationDate.getTimeInMillis() / 86400000);
+ if (numDays <= 0) {
+ throw new PgpGeneralMsgIdException(R.string.error_expiry_must_come_after_creation);
+ }
+ hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400);
+ } else {
+ hashedPacketsGen.setKeyExpirationTime(false, 0);
+ // do this explicitly, although since we're rebuilding,
+ // this happens anyway
+ }
+
+ updateProgress(R.string.progress_building_master_key, 30, 100);
+
+ // define hashing and signing algos
+ PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
+ HashAlgorithmTags.SHA1);
+ PGPContentSignerBuilder certificationSignerBuilder = new JcaPGPContentSignerBuilder(
+ masterKeyPair.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
+
+ // Build key encrypter based on passphrase
+ PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
+ PGPEncryptedData.CAST5, sha1Calc)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ newPassphrase.toCharArray());
+
+ PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION,
+ masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(),
+ unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor);
+
+ updateProgress(R.string.progress_adding_sub_keys, 40, 100);
+
+ for (int i = 1; i < keys.size(); ++i) {
+ updateProgress(40 + 40 * (i - 1) / (keys.size() - 1), 100);
+
+ PGPSecretKey subKey = keys.get(i);
+ PGPPublicKey subPublicKey = subKey.getPublicKey();
+
+ PBESecretKeyDecryptor keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ oldPassphrase.toCharArray());
+ PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(keyDecryptor2);
+
+ // TODO: now used without algorithm and creation time?! (APG 1)
+ PGPKeyPair subKeyPair = new PGPKeyPair(subPublicKey, subPrivateKey);
+
+ hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
+
+ usageId = keysUsages.get(i);
+ canSign = (usageId & KeyFlags.SIGN_DATA) > 0; //todo - separate function for this
+ if (canSign) {
+ Date todayDate = new Date(); //both sig times the same
+ // cross-certify signing keys
+ hashedPacketsGen.setSignatureCreationTime(false, todayDate); //set outer creation time
+ PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ subHashedPacketsGen.setSignatureCreationTime(false, todayDate); //set inner creation time
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ subPublicKey.getAlgorithm(), PGPUtil.SHA1)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+ sGen.init(PGPSignature.PRIMARYKEY_BINDING, subPrivateKey);
+ sGen.setHashedSubpackets(subHashedPacketsGen.generate());
+ PGPSignature certification = sGen.generateCertification(masterPublicKey,
+ subPublicKey);
+ unhashedPacketsGen.setEmbeddedSignature(false, certification);
+ }
+ hashedPacketsGen.setKeyFlags(false, usageId);
+
+ if (keysExpiryDates.get(i) != null) {
+ GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ creationDate.setTime(subPublicKey.getCreationTime());
+ GregorianCalendar expiryDate = keysExpiryDates.get(i);
+ //note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c
+ //here we purposefully ignore partial days in each date - long type has no fractional part!
+ long numDays = (expiryDate.getTimeInMillis() / 86400000) -
+ (creationDate.getTimeInMillis() / 86400000);
+ if (numDays <= 0) {
+ throw new PgpGeneralMsgIdException(R.string.error_expiry_must_come_after_creation);
+ }
+ hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400);
+ } else {
+ hashedPacketsGen.setKeyExpirationTime(false, 0);
+ // do this explicitly, although since we're rebuilding,
+ // this happens anyway
+ }
+
+ keyGen.addSubKey(subKeyPair, hashedPacketsGen.generate(), unhashedPacketsGen.generate());
+ }
+
+ PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing();
+ PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing();
+
+ return new Pair<PGPSecretKeyRing, PGPPublicKeyRing>(secretKeyRing, publicKeyRing);
+
+ }
+
+ public Pair<PGPSecretKeyRing, PGPPublicKeyRing> buildSecretKey(PGPSecretKeyRing mKR,
+ PGPPublicKeyRing pKR,
+ SaveKeyringParcel saveParcel)
+ throws PgpGeneralMsgIdException, PGPException, SignatureException, IOException {
+
+ updateProgress(R.string.progress_building_key, 0, 100);
+ PGPSecretKey masterKey = saveParcel.keys.get(0);
+
+ if (saveParcel.oldPassphrase == null) {
+ saveParcel.oldPassphrase = "";
+ }
+ if (saveParcel.newPassphrase == null) {
+ saveParcel.newPassphrase = "";
+ }
+
+ if (mKR == null) {
+ return buildNewSecretKey(saveParcel.userIDs, saveParcel.keys, saveParcel.keysExpiryDates,
+ saveParcel.keysUsages, saveParcel.newPassphrase, saveParcel.oldPassphrase); //new Keyring
+ }
+
+ /*
+ IDs - NB This might not need to happen later, if we change the way the primary ID is chosen
+ remove deleted ids
+ if the primary ID changed we need to:
+ remove all of the IDs from the keyring, saving their certifications
+ add them all in again, updating certs of IDs which have changed
+ else
+ remove changed IDs and add in with new certs
+
+ if the master key changed, we need to remove the primary ID certification, so we can add
+ the new one when it is generated, and they don't conflict
+
+ Keys
+ remove deleted keys
+ if a key is modified, re-sign it
+ do we need to remove and add in?
+
+ Todo
+ identify more things which need to be preserved - e.g. trust levels?
+ user attributes
+ */
+
+ if (saveParcel.deletedKeys != null) {
+ for (PGPSecretKey dKey : saveParcel.deletedKeys) {
+ mKR = PGPSecretKeyRing.removeSecretKey(mKR, dKey);
+ }
+ }
+
+ masterKey = mKR.getSecretKey();
+ PGPPublicKey masterPublicKey = masterKey.getPublicKey();
+
+ int usageId = saveParcel.keysUsages.get(0);
+ boolean canSign;
+ String mainUserId = saveParcel.userIDs.get(0);
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(saveParcel.oldPassphrase.toCharArray());
+ PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor);
+
+ updateProgress(R.string.progress_certifying_master_key, 20, 100);
+
+ boolean anyIDChanged = false;
+ for (String delID : saveParcel.deletedIDs) {
+ anyIDChanged = true;
+ masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, delID);
+ }
+
+ int userIDIndex = 0;
+
+ PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
+
+ hashedPacketsGen.setKeyFlags(true, usageId);
+
+ hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS);
+ hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
+ hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
+
+ if (saveParcel.keysExpiryDates.get(0) != null) {
+ GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ creationDate.setTime(masterPublicKey.getCreationTime());
+ GregorianCalendar expiryDate = saveParcel.keysExpiryDates.get(0);
+ //note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c
+ //here we purposefully ignore partial days in each date - long type has no fractional part!
+ long numDays = (expiryDate.getTimeInMillis() / 86400000) -
+ (creationDate.getTimeInMillis() / 86400000);
+ if (numDays <= 0) {
+ throw new PgpGeneralMsgIdException(R.string.error_expiry_must_come_after_creation);
+ }
+ hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400);
+ } else {
+ hashedPacketsGen.setKeyExpirationTime(false, 0);
+ // do this explicitly, although since we're rebuilding,
+ // this happens anyway
+ }
+
+ if (saveParcel.primaryIDChanged ||
+ !saveParcel.originalIDs.get(0).equals(saveParcel.userIDs.get(0))) {
+ anyIDChanged = true;
+ ArrayList<Pair<String, PGPSignature>> sigList = new ArrayList<Pair<String, PGPSignature>>();
+ for (String userId : saveParcel.userIDs) {
+ String origID = saveParcel.originalIDs.get(userIDIndex);
+ if (origID.equals(userId) && !saveParcel.newIDs[userIDIndex] &&
+ !userId.equals(saveParcel.originalPrimaryID) && userIDIndex != 0) {
+ Iterator<PGPSignature> origSigs = masterPublicKey.getSignaturesForID(origID);
+ // TODO: make sure this iterator only has signatures we are interested in
+ while (origSigs.hasNext()) {
+ PGPSignature origSig = origSigs.next();
+ sigList.add(new Pair<String, PGPSignature>(origID, origSig));
+ }
+ } else {
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+
+ sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
+ if (userIDIndex == 0) {
+ sGen.setHashedSubpackets(hashedPacketsGen.generate());
+ sGen.setUnhashedSubpackets(unhashedPacketsGen.generate());
+ }
+ PGPSignature certification = sGen.generateCertification(userId, masterPublicKey);
+ sigList.add(new Pair<String, PGPSignature>(userId, certification));
+ }
+ if (!saveParcel.newIDs[userIDIndex]) {
+ masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, origID);
+ }
+ userIDIndex++;
+ }
+ for (Pair<String, PGPSignature> toAdd : sigList) {
+ masterPublicKey =
+ PGPPublicKey.addCertification(masterPublicKey, toAdd.first, toAdd.second);
+ }
+ } else {
+ for (String userId : saveParcel.userIDs) {
+ String origID = saveParcel.originalIDs.get(userIDIndex);
+ if (!origID.equals(userId) || saveParcel.newIDs[userIDIndex]) {
+ anyIDChanged = true;
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+
+ sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
+ if (userIDIndex == 0) {
+ sGen.setHashedSubpackets(hashedPacketsGen.generate());
+ sGen.setUnhashedSubpackets(unhashedPacketsGen.generate());
+ }
+ PGPSignature certification = sGen.generateCertification(userId, masterPublicKey);
+ if (!saveParcel.newIDs[userIDIndex]) {
+ masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, origID);
+ }
+ masterPublicKey =
+ PGPPublicKey.addCertification(masterPublicKey, userId, certification);
+ }
+ userIDIndex++;
+ }
+ }
+
+ ArrayList<Pair<String, PGPSignature>> sigList = new ArrayList<Pair<String, PGPSignature>>();
+ if (saveParcel.moddedKeys[0]) {
+ userIDIndex = 0;
+ for (String userId : saveParcel.userIDs) {
+ String origID = saveParcel.originalIDs.get(userIDIndex);
+ if (!(origID.equals(saveParcel.originalPrimaryID) && !saveParcel.primaryIDChanged)) {
+ Iterator<PGPSignature> sigs = masterPublicKey.getSignaturesForID(userId);
+ // TODO: make sure this iterator only has signatures we are interested in
+ while (sigs.hasNext()) {
+ PGPSignature sig = sigs.next();
+ sigList.add(new Pair<String, PGPSignature>(userId, sig));
+ }
+ }
+ masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, userId);
+ userIDIndex++;
+ }
+ anyIDChanged = true;
+ }
+
+ //update the keyring with the new ID information
+ if (anyIDChanged) {
+ pKR = PGPPublicKeyRing.insertPublicKey(pKR, masterPublicKey);
+ mKR = PGPSecretKeyRing.replacePublicKeys(mKR, pKR);
+ }
+
+ PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
+
+ updateProgress(R.string.progress_building_master_key, 30, 100);
+
+ // define hashing and signing algos
+ PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
+ HashAlgorithmTags.SHA1);
+ PGPContentSignerBuilder certificationSignerBuilder = new JcaPGPContentSignerBuilder(
+ masterKeyPair.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
+
+ // Build key encryptor based on old passphrase, as some keys may be unchanged
+ PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
+ PGPEncryptedData.CAST5, sha1Calc)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ saveParcel.oldPassphrase.toCharArray());
+
+ //this generates one more signature than necessary...
+ PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION,
+ masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(),
+ unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor);
+
+ for (int i = 1; i < saveParcel.keys.size(); ++i) {
+ updateProgress(40 + 50 * i / saveParcel.keys.size(), 100);
+ if (saveParcel.moddedKeys[i]) {
+ PGPSecretKey subKey = saveParcel.keys.get(i);
+ PGPPublicKey subPublicKey = subKey.getPublicKey();
+
+ PBESecretKeyDecryptor keyDecryptor2;
+ if (saveParcel.newKeys[i]) {
+ keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ "".toCharArray());
+ } else {
+ keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ saveParcel.oldPassphrase.toCharArray());
+ }
+ PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(keyDecryptor2);
+ PGPKeyPair subKeyPair = new PGPKeyPair(subPublicKey, subPrivateKey);
+
+ hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
+
+ usageId = saveParcel.keysUsages.get(i);
+ canSign = (usageId & KeyFlags.SIGN_DATA) > 0; //todo - separate function for this
+ if (canSign) {
+ Date todayDate = new Date(); //both sig times the same
+ // cross-certify signing keys
+ hashedPacketsGen.setSignatureCreationTime(false, todayDate); //set outer creation time
+ PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ subHashedPacketsGen.setSignatureCreationTime(false, todayDate); //set inner creation time
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ subPublicKey.getAlgorithm(), PGPUtil.SHA1)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+ sGen.init(PGPSignature.PRIMARYKEY_BINDING, subPrivateKey);
+ sGen.setHashedSubpackets(subHashedPacketsGen.generate());
+ PGPSignature certification = sGen.generateCertification(masterPublicKey,
+ subPublicKey);
+ unhashedPacketsGen.setEmbeddedSignature(false, certification);
+ }
+ hashedPacketsGen.setKeyFlags(false, usageId);
+
+ if (saveParcel.keysExpiryDates.get(i) != null) {
+ GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ creationDate.setTime(subPublicKey.getCreationTime());
+ GregorianCalendar expiryDate = saveParcel.keysExpiryDates.get(i);
+ // note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c
+ // here we purposefully ignore partial days in each date - long type has
+ // no fractional part!
+ long numDays = (expiryDate.getTimeInMillis() / 86400000) -
+ (creationDate.getTimeInMillis() / 86400000);
+ if (numDays <= 0) {
+ throw new PgpGeneralMsgIdException(R.string.error_expiry_must_come_after_creation);
+ }
+ hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400);
+ } else {
+ hashedPacketsGen.setKeyExpirationTime(false, 0);
+ // do this explicitly, although since we're rebuilding,
+ // this happens anyway
+ }
+
+ keyGen.addSubKey(subKeyPair, hashedPacketsGen.generate(), unhashedPacketsGen.generate());
+ // certifications will be discarded if the key is changed, because I think, for a start,
+ // they will be invalid. Binding certs are regenerated anyway, and other certs which
+ // need to be kept are on IDs and attributes
+ // TODO: don't let revoked keys be edited, other than removed - changing one would
+ // result in the revocation being wrong?
+ }
+ }
+
+ PGPSecretKeyRing updatedSecretKeyRing = keyGen.generateSecretKeyRing();
+ //finally, update the keyrings
+ Iterator<PGPSecretKey> itr = updatedSecretKeyRing.getSecretKeys();
+ while (itr.hasNext()) {
+ PGPSecretKey theNextKey = itr.next();
+ if ((theNextKey.isMasterKey() && saveParcel.moddedKeys[0]) || !theNextKey.isMasterKey()) {
+ mKR = PGPSecretKeyRing.insertSecretKey(mKR, theNextKey);
+ pKR = PGPPublicKeyRing.insertPublicKey(pKR, theNextKey.getPublicKey());
+ }
+ }
+
+ //replace lost IDs
+ if (saveParcel.moddedKeys[0]) {
+ masterPublicKey = mKR.getPublicKey();
+ for (Pair<String, PGPSignature> toAdd : sigList) {
+ masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, toAdd.first, toAdd.second);
+ }
+ pKR = PGPPublicKeyRing.insertPublicKey(pKR, masterPublicKey);
+ mKR = PGPSecretKeyRing.replacePublicKeys(mKR, pKR);
+ }
+
+ // Build key encryptor based on new passphrase
+ PBESecretKeyEncryptor keyEncryptorNew = new JcePBESecretKeyEncryptorBuilder(
+ PGPEncryptedData.CAST5, sha1Calc)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ saveParcel.newPassphrase.toCharArray());
+
+ //update the passphrase
+ mKR = PGPSecretKeyRing.copyWithNewPassword(mKR, keyDecryptor, keyEncryptorNew);
+
+ /* additional handy debug info
+
+ Log.d(Constants.TAG, " ------- in private key -------");
+
+ for(String uid : new IterableIterator<String>(secretKeyRing.getPublicKey().getUserIDs())) {
+ for(PGPSignature sig : new IterableIterator<PGPSignature>(
+ secretKeyRing.getPublicKey().getSignaturesForID(uid))) {
+ Log.d(Constants.TAG, "sig: " +
+ PgpKeyHelper.convertKeyIdToHex(sig.getKeyID()) + " for " + uid);
+ }
+
+ }
+
+ Log.d(Constants.TAG, " ------- in public key -------");
+
+ for(String uid : new IterableIterator<String>(publicKeyRing.getPublicKey().getUserIDs())) {
+ for(PGPSignature sig : new IterableIterator<PGPSignature>(
+ publicKeyRing.getPublicKey().getSignaturesForID(uid))) {
+ Log.d(Constants.TAG, "sig: " +
+ PgpKeyHelper.convertKeyIdToHex(sig.getKeyID()) + " for " + uid);
+ }
+ }
+
+ */
+
+ return new Pair<PGPSecretKeyRing, PGPPublicKeyRing>(mKR, pKR);
+
+ }
+
+ /**
+ * Certify the given pubkeyid with the given masterkeyid.
+ *
+ * @param certificationKey Certifying key
+ * @param publicKey public key to certify
+ * @param userIds User IDs to certify, must not be null or empty
+ * @param passphrase Passphrase of the secret key
+ * @return A keyring with added certifications
+ */
+ public PGPPublicKey certifyKey(PGPSecretKey certificationKey, PGPPublicKey publicKey,
+ List<String> userIds, String passphrase)
+ throws PgpGeneralMsgIdException, NoSuchAlgorithmException, NoSuchProviderException,
+ PGPException, SignatureException {
+
+ // create a signatureGenerator from the supplied masterKeyId and passphrase
+ PGPSignatureGenerator signatureGenerator; {
+
+ if (certificationKey == null) {
+ throw new PgpGeneralMsgIdException(R.string.error_signature_failed);
+ }
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
+ PGPPrivateKey signaturePrivateKey = certificationKey.extractPrivateKey(keyDecryptor);
+ if (signaturePrivateKey == null) {
+ throw new PgpGeneralMsgIdException(R.string.error_could_not_extract_private_key);
+ }
+
+ // TODO: SHA256 fixed?
+ JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(
+ certificationKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
+ signatureGenerator.init(PGPSignature.DEFAULT_CERTIFICATION, signaturePrivateKey);
+ }
+
+ { // supply signatureGenerator with a SubpacketVector
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ PGPSignatureSubpacketVector packetVector = spGen.generate();
+ signatureGenerator.setHashedSubpackets(packetVector);
+ }
+
+ // fetch public key ring, add the certification and return it
+ for (String userId : new IterableIterator<String>(userIds.iterator())) {
+ PGPSignature sig = signatureGenerator.generateCertification(userId, publicKey);
+ publicKey = PGPPublicKey.addCertification(publicKey, userId, sig);
+ }
+
+ return publicKey;
+ }
+
+ /** Simple static subclass that stores two values.
+ *
+ * This is only used to return a pair of values in one function above. We specifically don't use
+ * com.android.Pair to keep this class free from android dependencies.
+ */
+ public static class Pair<K, V> {
+ public final K first;
+ public final V second;
+ public Pair(K first, V second) {
+ this.first = first;
+ this.second = second;
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncrypt.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncrypt.java
new file mode 100644
index 000000000..a864a165d
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncrypt.java
@@ -0,0 +1,607 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp;
+
+import android.content.Context;
+
+import org.spongycastle.bcpg.ArmoredOutputStream;
+import org.spongycastle.bcpg.BCPGOutputStream;
+import org.spongycastle.openpgp.PGPCompressedDataGenerator;
+import org.spongycastle.openpgp.PGPEncryptedDataGenerator;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPLiteralData;
+import org.spongycastle.openpgp.PGPLiteralDataGenerator;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
+import org.spongycastle.openpgp.PGPV3SignatureGenerator;
+import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.util.Date;
+
+/**
+ * This class uses a Builder pattern!
+ */
+public class PgpSignEncrypt {
+ private Context mContext;
+ private InputData mData;
+ private OutputStream mOutStream;
+
+ private ProgressDialogUpdater mProgress;
+ private boolean mEnableAsciiArmorOutput;
+ private int mCompressionId;
+ private long[] mEncryptionKeyIds;
+ private String mSymmetricPassphrase;
+ private int mSymmetricEncryptionAlgorithm;
+ private long mSignatureKeyId;
+ private int mSignatureHashAlgorithm;
+ private boolean mSignatureForceV3;
+ private String mSignaturePassphrase;
+
+ private PgpSignEncrypt(Builder builder) {
+ // private Constructor can only be called from Builder
+ this.mContext = builder.mContext;
+ this.mData = builder.mData;
+ this.mOutStream = builder.mOutStream;
+
+ this.mProgress = builder.mProgress;
+ this.mEnableAsciiArmorOutput = builder.mEnableAsciiArmorOutput;
+ this.mCompressionId = builder.mCompressionId;
+ this.mEncryptionKeyIds = builder.mEncryptionKeyIds;
+ this.mSymmetricPassphrase = builder.mSymmetricPassphrase;
+ this.mSymmetricEncryptionAlgorithm = builder.mSymmetricEncryptionAlgorithm;
+ this.mSignatureKeyId = builder.mSignatureKeyId;
+ this.mSignatureHashAlgorithm = builder.mSignatureHashAlgorithm;
+ this.mSignatureForceV3 = builder.mSignatureForceV3;
+ this.mSignaturePassphrase = builder.mSignaturePassphrase;
+ }
+
+ public static class Builder {
+ // mandatory parameter
+ private Context mContext;
+ private InputData mData;
+ private OutputStream mOutStream;
+
+ // optional
+ private ProgressDialogUpdater mProgress = null;
+ private boolean mEnableAsciiArmorOutput = false;
+ private int mCompressionId = Id.choice.compression.none;
+ private long[] mEncryptionKeyIds = null;
+ private String mSymmetricPassphrase = null;
+ private int mSymmetricEncryptionAlgorithm = 0;
+ private long mSignatureKeyId = Id.key.none;
+ private int mSignatureHashAlgorithm = 0;
+ private boolean mSignatureForceV3 = false;
+ private String mSignaturePassphrase = null;
+
+ public Builder(Context context, InputData data, OutputStream outStream) {
+ this.mContext = context;
+ this.mData = data;
+ this.mOutStream = outStream;
+ }
+
+ public Builder progress(ProgressDialogUpdater progress) {
+ this.mProgress = progress;
+ return this;
+ }
+
+ public Builder enableAsciiArmorOutput(boolean enableAsciiArmorOutput) {
+ this.mEnableAsciiArmorOutput = enableAsciiArmorOutput;
+ return this;
+ }
+
+ public Builder compressionId(int compressionId) {
+ this.mCompressionId = compressionId;
+ return this;
+ }
+
+ public Builder encryptionKeyIds(long[] encryptionKeyIds) {
+ this.mEncryptionKeyIds = encryptionKeyIds;
+ return this;
+ }
+
+ public Builder symmetricPassphrase(String symmetricPassphrase) {
+ this.mSymmetricPassphrase = symmetricPassphrase;
+ return this;
+ }
+
+ public Builder symmetricEncryptionAlgorithm(int symmetricEncryptionAlgorithm) {
+ this.mSymmetricEncryptionAlgorithm = symmetricEncryptionAlgorithm;
+ return this;
+ }
+
+ public Builder signatureKeyId(long signatureKeyId) {
+ this.mSignatureKeyId = signatureKeyId;
+ return this;
+ }
+
+ public Builder signatureHashAlgorithm(int signatureHashAlgorithm) {
+ this.mSignatureHashAlgorithm = signatureHashAlgorithm;
+ return this;
+ }
+
+ public Builder signatureForceV3(boolean signatureForceV3) {
+ this.mSignatureForceV3 = signatureForceV3;
+ return this;
+ }
+
+ public Builder signaturePassphrase(String signaturePassphrase) {
+ this.mSignaturePassphrase = signaturePassphrase;
+ return this;
+ }
+
+ public PgpSignEncrypt build() {
+ return new PgpSignEncrypt(this);
+ }
+ }
+
+ public void updateProgress(int message, int current, int total) {
+ if (mProgress != null) {
+ mProgress.setProgress(message, current, total);
+ }
+ }
+
+ public void updateProgress(int current, int total) {
+ if (mProgress != null) {
+ mProgress.setProgress(current, total);
+ }
+ }
+
+ /**
+ * Signs and/or encrypts data based on parameters of class
+ *
+ * @throws IOException
+ * @throws PgpGeneralException
+ * @throws PGPException
+ * @throws NoSuchProviderException
+ * @throws NoSuchAlgorithmException
+ * @throws SignatureException
+ */
+ public void execute()
+ throws IOException, PgpGeneralException, PGPException, NoSuchProviderException,
+ NoSuchAlgorithmException, SignatureException {
+
+ boolean enableSignature = mSignatureKeyId != Id.key.none;
+ boolean enableEncryption = ((mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0)
+ || mSymmetricPassphrase != null);
+ boolean enableCompression = (enableEncryption && mCompressionId != Id.choice.compression.none);
+
+ Log.d(Constants.TAG, "enableSignature:" + enableSignature
+ + "\nenableEncryption:" + enableEncryption
+ + "\nenableCompression:" + enableCompression
+ + "\nenableAsciiArmorOutput:" + mEnableAsciiArmorOutput);
+
+ int signatureType;
+ if (mEnableAsciiArmorOutput && enableSignature && !enableEncryption && !enableCompression) {
+ // for sign-only ascii text
+ signatureType = PGPSignature.CANONICAL_TEXT_DOCUMENT;
+ } else {
+ signatureType = PGPSignature.BINARY_DOCUMENT;
+ }
+
+ ArmoredOutputStream armorOut = null;
+ OutputStream out;
+ if (mEnableAsciiArmorOutput) {
+ armorOut = new ArmoredOutputStream(mOutStream);
+ armorOut.setHeader("Version", PgpHelper.getFullVersion(mContext));
+ out = armorOut;
+ } else {
+ out = mOutStream;
+ }
+
+ /* Get keys for signature generation for later usage */
+ PGPSecretKey signingKey = null;
+ PGPSecretKeyRing signingKeyRing = null;
+ PGPPrivateKey signaturePrivateKey = null;
+ if (enableSignature) {
+ signingKeyRing = ProviderHelper.getPGPSecretKeyRingWithKeyId(mContext, mSignatureKeyId);
+ signingKey = PgpKeyHelper.getSigningKey(mContext, mSignatureKeyId);
+ if (signingKey == null) {
+ throw new PgpGeneralException(mContext.getString(R.string.error_signature_failed));
+ }
+
+ if (mSignaturePassphrase == null) {
+ throw new PgpGeneralException(
+ mContext.getString(R.string.error_no_signature_passphrase));
+ }
+
+ updateProgress(R.string.progress_extracting_signature_key, 0, 100);
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mSignaturePassphrase.toCharArray());
+ signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor);
+ if (signaturePrivateKey == null) {
+ throw new PgpGeneralException(
+ mContext.getString(R.string.error_could_not_extract_private_key));
+ }
+ }
+ updateProgress(R.string.progress_preparing_streams, 5, 100);
+
+ /* Initialize PGPEncryptedDataGenerator for later usage */
+ PGPEncryptedDataGenerator cPk = null;
+ if (enableEncryption) {
+ // has Integrity packet enabled!
+ JcePGPDataEncryptorBuilder encryptorBuilder =
+ new JcePGPDataEncryptorBuilder(mSymmetricEncryptionAlgorithm)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME)
+ .setWithIntegrityPacket(true);
+
+ cPk = new PGPEncryptedDataGenerator(encryptorBuilder);
+
+ if (mSymmetricPassphrase != null) {
+ // Symmetric encryption
+ Log.d(Constants.TAG, "encryptionKeyIds length is 0 -> symmetric encryption");
+
+ JcePBEKeyEncryptionMethodGenerator symmetricEncryptionGenerator =
+ new JcePBEKeyEncryptionMethodGenerator(mSymmetricPassphrase.toCharArray());
+ cPk.addMethod(symmetricEncryptionGenerator);
+ } else {
+ // Asymmetric encryption
+ for (long id : mEncryptionKeyIds) {
+ PGPPublicKey key = PgpKeyHelper.getEncryptPublicKey(mContext, id);
+ if (key != null) {
+ JcePublicKeyKeyEncryptionMethodGenerator pubKeyEncryptionGenerator =
+ new JcePublicKeyKeyEncryptionMethodGenerator(key);
+ cPk.addMethod(pubKeyEncryptionGenerator);
+ }
+ }
+ }
+ }
+
+ /* Initialize signature generator object for later usage */
+ PGPSignatureGenerator signatureGenerator = null;
+ PGPV3SignatureGenerator signatureV3Generator = null;
+ if (enableSignature) {
+ updateProgress(R.string.progress_preparing_signature, 10, 100);
+
+ // content signer based on signing key algorithm and chosen hash algorithm
+ JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(
+ signingKey.getPublicKey().getAlgorithm(), mSignatureHashAlgorithm)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ if (mSignatureForceV3) {
+ signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder);
+ signatureV3Generator.init(signatureType, signaturePrivateKey);
+ } else {
+ signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
+ signatureGenerator.init(signatureType, signaturePrivateKey);
+
+ String userId = PgpKeyHelper.getMainUserId(signingKeyRing.getSecretKey());
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+ }
+ }
+
+ PGPCompressedDataGenerator compressGen = null;
+ OutputStream pOut;
+ OutputStream encryptionOut = null;
+ BCPGOutputStream bcpgOut;
+ if (enableEncryption) {
+ /* actual encryption */
+
+ encryptionOut = cPk.open(out, new byte[1 << 16]);
+
+ if (enableCompression) {
+ compressGen = new PGPCompressedDataGenerator(mCompressionId);
+ bcpgOut = new BCPGOutputStream(compressGen.open(encryptionOut));
+ } else {
+ bcpgOut = new BCPGOutputStream(encryptionOut);
+ }
+
+ if (enableSignature) {
+ if (mSignatureForceV3) {
+ signatureV3Generator.generateOnePassVersion(false).encode(bcpgOut);
+ } else {
+ signatureGenerator.generateOnePassVersion(false).encode(bcpgOut);
+ }
+ }
+
+ PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator();
+ // file name not needed, so empty string
+ pOut = literalGen.open(bcpgOut, PGPLiteralData.BINARY, "", new Date(),
+ new byte[1 << 16]);
+ updateProgress(R.string.progress_encrypting, 20, 100);
+
+ long progress = 0;
+ int n;
+ byte[] buffer = new byte[1 << 16];
+ InputStream in = mData.getInputStream();
+ while ((n = in.read(buffer)) > 0) {
+ pOut.write(buffer, 0, n);
+
+ // update signature buffer if signature is requested
+ if (enableSignature) {
+ if (mSignatureForceV3) {
+ signatureV3Generator.update(buffer, 0, n);
+ } else {
+ signatureGenerator.update(buffer, 0, n);
+ }
+ }
+
+ progress += n;
+ if (mData.getSize() != 0) {
+ updateProgress((int) (20 + (95 - 20) * progress / mData.getSize()), 100);
+ }
+ }
+
+ literalGen.close();
+ } else if (mEnableAsciiArmorOutput && enableSignature && !enableEncryption && !enableCompression) {
+ /* sign-only of ascii text */
+
+ updateProgress(R.string.progress_signing, 40, 100);
+
+ // write directly on armor output stream
+ armorOut.beginClearText(mSignatureHashAlgorithm);
+
+ InputStream in = mData.getInputStream();
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+
+ final byte[] newline = "\r\n".getBytes("UTF-8");
+
+ if (mSignatureForceV3) {
+ processLine(reader.readLine(), armorOut, signatureV3Generator);
+ } else {
+ processLine(reader.readLine(), armorOut, signatureGenerator);
+ }
+
+ while (true) {
+ String line = reader.readLine();
+
+ if (line == null) {
+ armorOut.write(newline);
+ break;
+ }
+
+ armorOut.write(newline);
+
+ // update signature buffer with input line
+ if (mSignatureForceV3) {
+ signatureV3Generator.update(newline);
+ processLine(line, armorOut, signatureV3Generator);
+ } else {
+ signatureGenerator.update(newline);
+ processLine(line, armorOut, signatureGenerator);
+ }
+ }
+
+ armorOut.endClearText();
+
+ pOut = new BCPGOutputStream(armorOut);
+ } else {
+ // TODO: implement sign-only for files!
+ pOut = null;
+ Log.e(Constants.TAG, "not supported!");
+ }
+
+ if (enableSignature) {
+ updateProgress(R.string.progress_generating_signature, 95, 100);
+ if (mSignatureForceV3) {
+ signatureV3Generator.generate().encode(pOut);
+ } else {
+ signatureGenerator.generate().encode(pOut);
+ }
+ }
+
+ // closing outputs
+ // NOTE: closing needs to be done in the correct order!
+ // TODO: closing bcpgOut and pOut???
+ if (enableEncryption) {
+ if (enableCompression) {
+ compressGen.close();
+ }
+
+ encryptionOut.close();
+ }
+ if (mEnableAsciiArmorOutput) {
+ armorOut.close();
+ }
+
+ out.close();
+ mOutStream.close();
+
+ updateProgress(R.string.progress_done, 100, 100);
+ }
+
+ // TODO: merge this into execute method!
+ // TODO: allow binary input for this class
+ public void generateSignature()
+ throws PgpGeneralException, PGPException, IOException, NoSuchAlgorithmException,
+ SignatureException {
+
+ OutputStream out;
+ if (mEnableAsciiArmorOutput) {
+ // Ascii Armor (Radix-64)
+ ArmoredOutputStream armorOut = new ArmoredOutputStream(mOutStream);
+ armorOut.setHeader("Version", PgpHelper.getFullVersion(mContext));
+ out = armorOut;
+ } else {
+ out = mOutStream;
+ }
+
+ if (mSignatureKeyId == 0) {
+ throw new PgpGeneralException(mContext.getString(R.string.error_no_signature_key));
+ }
+
+ PGPSecretKeyRing signingKeyRing =
+ ProviderHelper.getPGPSecretKeyRingWithKeyId(mContext, mSignatureKeyId);
+ PGPSecretKey signingKey = PgpKeyHelper.getSigningKey(mContext, mSignatureKeyId);
+ if (signingKey == null) {
+ throw new PgpGeneralException(mContext.getString(R.string.error_signature_failed));
+ }
+
+ if (mSignaturePassphrase == null) {
+ throw new PgpGeneralException(mContext.getString(R.string.error_no_signature_passphrase));
+ }
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mSignaturePassphrase.toCharArray());
+ PGPPrivateKey signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor);
+ if (signaturePrivateKey == null) {
+ throw new PgpGeneralException(
+ mContext.getString(R.string.error_could_not_extract_private_key));
+ }
+ updateProgress(R.string.progress_preparing_streams, 0, 100);
+
+ updateProgress(R.string.progress_preparing_signature, 30, 100);
+
+ int type = PGPSignature.CANONICAL_TEXT_DOCUMENT;
+// if (binary) {
+// type = PGPSignature.BINARY_DOCUMENT;
+// }
+
+ // content signer based on signing key algorithm and chosen hash algorithm
+ JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(signingKey
+ .getPublicKey().getAlgorithm(), mSignatureHashAlgorithm)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ PGPSignatureGenerator signatureGenerator = null;
+ PGPV3SignatureGenerator signatureV3Generator = null;
+ if (mSignatureForceV3) {
+ signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder);
+ signatureV3Generator.init(type, signaturePrivateKey);
+ } else {
+ signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
+ signatureGenerator.init(type, signaturePrivateKey);
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ String userId = PgpKeyHelper.getMainUserId(signingKeyRing.getSecretKey());
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+ }
+
+ updateProgress(R.string.progress_signing, 40, 100);
+
+ InputStream inStream = mData.getInputStream();
+// if (binary) {
+// byte[] buffer = new byte[1 << 16];
+// int n = 0;
+// while ((n = inStream.read(buffer)) > 0) {
+// if (signatureForceV3) {
+// signatureV3Generator.update(buffer, 0, n);
+// } else {
+// signatureGenerator.update(buffer, 0, n);
+// }
+// }
+// } else {
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(inStream));
+ final byte[] newline = "\r\n".getBytes("UTF-8");
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (mSignatureForceV3) {
+ processLine(line, null, signatureV3Generator);
+ signatureV3Generator.update(newline);
+ } else {
+ processLine(line, null, signatureGenerator);
+ signatureGenerator.update(newline);
+ }
+ }
+// }
+
+ BCPGOutputStream bOut = new BCPGOutputStream(out);
+ if (mSignatureForceV3) {
+ signatureV3Generator.generate().encode(bOut);
+ } else {
+ signatureGenerator.generate().encode(bOut);
+ }
+ out.close();
+ mOutStream.close();
+
+ updateProgress(R.string.progress_done, 100, 100);
+ }
+
+
+ private static void processLine(final String pLine, final ArmoredOutputStream pArmoredOutput,
+ final PGPSignatureGenerator pSignatureGenerator)
+ throws IOException, SignatureException {
+
+ if (pLine == null) {
+ return;
+ }
+
+ final char[] chars = pLine.toCharArray();
+ int len = chars.length;
+
+ while (len > 0) {
+ if (!Character.isWhitespace(chars[len - 1])) {
+ break;
+ }
+ len--;
+ }
+
+ final byte[] data = pLine.substring(0, len).getBytes("UTF-8");
+
+ if (pArmoredOutput != null) {
+ pArmoredOutput.write(data);
+ }
+ pSignatureGenerator.update(data);
+ }
+
+ private static void processLine(final String pLine, final ArmoredOutputStream pArmoredOutput,
+ final PGPV3SignatureGenerator pSignatureGenerator)
+ throws IOException, SignatureException {
+
+ if (pLine == null) {
+ return;
+ }
+
+ final char[] chars = pLine.toCharArray();
+ int len = chars.length;
+
+ while (len > 0) {
+ if (!Character.isWhitespace(chars[len - 1])) {
+ break;
+ }
+ len--;
+ }
+
+ final byte[] data = pLine.substring(0, len).getBytes("UTF-8");
+
+ if (pArmoredOutput != null) {
+ pArmoredOutput.write(data);
+ }
+ pSignatureGenerator.update(data);
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpToX509.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpToX509.java
new file mode 100644
index 000000000..5bb1665b6
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpToX509.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp;
+
+import org.spongycastle.asn1.DERObjectIdentifier;
+import org.spongycastle.asn1.x509.AuthorityKeyIdentifier;
+import org.spongycastle.asn1.x509.BasicConstraints;
+import org.spongycastle.asn1.x509.GeneralName;
+import org.spongycastle.asn1.x509.GeneralNames;
+import org.spongycastle.asn1.x509.SubjectKeyIdentifier;
+import org.spongycastle.asn1.x509.X509Extensions;
+import org.spongycastle.asn1.x509.X509Name;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.x509.X509V3CertificateGenerator;
+import org.spongycastle.x509.extension.AuthorityKeyIdentifierStructure;
+import org.spongycastle.x509.extension.SubjectKeyIdentifierStructure;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Vector;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+public class PgpToX509 {
+ public static final String DN_COMMON_PART_O = "OpenPGP to X.509 Bridge";
+ public static final String DN_COMMON_PART_OU = "OpenPGP Keychain cert";
+
+ /**
+ * Creates a self-signed certificate from a public and private key. The (critical) key-usage
+ * extension is set up with: digital signature, non-repudiation, key-encipherment, key-agreement
+ * and certificate-signing. The (non-critical) Netscape extension is set up with: SSL client and
+ * S/MIME. A URI subjectAltName may also be set up.
+ *
+ * @param pubKey public key
+ * @param privKey private key
+ * @param subject subject (and issuer) DN for this certificate, RFC 2253 format preferred.
+ * @param startDate date from which the certificate will be valid (defaults to current date and time
+ * if null)
+ * @param endDate date until which the certificate will be valid (defaults to current date and time
+ * if null) *
+ * @param subjAltNameURI URI to be placed in subjectAltName
+ * @return self-signed certificate
+ * @throws InvalidKeyException
+ * @throws SignatureException
+ * @throws NoSuchAlgorithmException
+ * @throws IllegalStateException
+ * @throws NoSuchProviderException
+ * @throws CertificateException
+ * @throws Exception
+ * @author Bruno Harbulot
+ */
+ public static X509Certificate createSelfSignedCert(
+ PublicKey pubKey, PrivateKey privKey, X509Name subject, Date startDate, Date endDate,
+ String subjAltNameURI)
+ throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException,
+ SignatureException, CertificateException, NoSuchProviderException {
+
+ X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
+
+ certGenerator.reset();
+ /*
+ * Sets up the subject distinguished name. Since it's a self-signed certificate, issuer and
+ * subject are the same.
+ */
+ certGenerator.setIssuerDN(subject);
+ certGenerator.setSubjectDN(subject);
+
+ /*
+ * Sets up the validity dates.
+ */
+ if (startDate == null) {
+ startDate = new Date(System.currentTimeMillis());
+ }
+ certGenerator.setNotBefore(startDate);
+ if (endDate == null) {
+ endDate = new Date(startDate.getTime() + (365L * 24L * 60L * 60L * 1000L));
+ Log.d(Constants.TAG, "end date is=" + DateFormat.getDateInstance().format(endDate));
+ }
+
+ certGenerator.setNotAfter(endDate);
+
+ /*
+ * The serial-number of this certificate is 1. It makes sense because it's self-signed.
+ */
+ certGenerator.setSerialNumber(BigInteger.ONE);
+ /*
+ * Sets the public-key to embed in this certificate.
+ */
+ certGenerator.setPublicKey(pubKey);
+ /*
+ * Sets the signature algorithm.
+ */
+ String pubKeyAlgorithm = pubKey.getAlgorithm();
+ if (pubKeyAlgorithm.equals("DSA")) {
+ certGenerator.setSignatureAlgorithm("SHA1WithDSA");
+ } else if (pubKeyAlgorithm.equals("RSA")) {
+ certGenerator.setSignatureAlgorithm("SHA1WithRSAEncryption");
+ } else {
+ RuntimeException re = new RuntimeException("Algorithm not recognised: "
+ + pubKeyAlgorithm);
+ Log.e(Constants.TAG, re.getMessage(), re);
+ throw re;
+ }
+
+ /*
+ * Adds the Basic Constraint (CA: true) extension.
+ */
+ certGenerator.addExtension(X509Extensions.BasicConstraints, true,
+ new BasicConstraints(true));
+
+ /*
+ * Adds the subject key identifier extension.
+ */
+ SubjectKeyIdentifier subjectKeyIdentifier = new SubjectKeyIdentifierStructure(pubKey);
+ certGenerator
+ .addExtension(X509Extensions.SubjectKeyIdentifier, false, subjectKeyIdentifier);
+
+ /*
+ * Adds the authority key identifier extension.
+ */
+ AuthorityKeyIdentifier authorityKeyIdentifier = new AuthorityKeyIdentifierStructure(pubKey);
+ certGenerator.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
+ authorityKeyIdentifier);
+
+ /*
+ * Adds the subject alternative-name extension.
+ */
+ if (subjAltNameURI != null) {
+ GeneralNames subjectAltNames = new GeneralNames(new GeneralName(
+ GeneralName.uniformResourceIdentifier, subjAltNameURI));
+ certGenerator.addExtension(X509Extensions.SubjectAlternativeName, false,
+ subjectAltNames);
+ }
+
+ /*
+ * Creates and sign this certificate with the private key corresponding to the public key of
+ * the certificate (hence the name "self-signed certificate").
+ */
+ X509Certificate cert = certGenerator.generate(privKey);
+
+ /*
+ * Checks that this certificate has indeed been correctly signed.
+ */
+ cert.verify(pubKey);
+
+ return cert;
+ }
+
+ /**
+ * Creates a self-signed certificate from a PGP Secret Key.
+ *
+ * @param pgpSecKey PGP Secret Key (from which one can extract the public and private
+ * keys and other attributes).
+ * @param pgpPrivKey PGP Private Key corresponding to the Secret Key (password callbacks
+ * should be done before calling this method)
+ * @param subjAltNameURI optional URI to embed in the subject alternative-name
+ * @return self-signed certificate
+ * @throws PGPException
+ * @throws NoSuchProviderException
+ * @throws InvalidKeyException
+ * @throws NoSuchAlgorithmException
+ * @throws SignatureException
+ * @throws CertificateException
+ * @author Bruno Harbulot
+ */
+ public static X509Certificate createSelfSignedCert(
+ PGPSecretKey pgpSecKey, PGPPrivateKey pgpPrivKey, String subjAltNameURI)
+ throws PGPException, NoSuchProviderException, InvalidKeyException, NoSuchAlgorithmException,
+ SignatureException, CertificateException {
+ // get public key from secret key
+ PGPPublicKey pgpPubKey = pgpSecKey.getPublicKey();
+
+ // LOGGER.info("Key ID: " + Long.toHexString(pgpPubKey.getKeyID() & 0xffffffffL));
+
+ /*
+ * The X.509 Name to be the subject DN is prepared. The CN is extracted from the Secret Key
+ * user ID.
+ */
+ Vector<DERObjectIdentifier> x509NameOids = new Vector<DERObjectIdentifier>();
+ Vector<String> x509NameValues = new Vector<String>();
+
+ x509NameOids.add(X509Name.O);
+ x509NameValues.add(DN_COMMON_PART_O);
+
+ x509NameOids.add(X509Name.OU);
+ x509NameValues.add(DN_COMMON_PART_OU);
+
+ for (@SuppressWarnings("unchecked")
+ Iterator<Object> it = (Iterator<Object>) pgpSecKey.getUserIDs(); it.hasNext(); ) {
+ Object attrib = it.next();
+ x509NameOids.add(X509Name.CN);
+ x509NameValues.add("CryptoCall");
+ // x509NameValues.add(attrib.toString());
+ }
+
+ /*
+ * Currently unused.
+ */
+ Log.d(Constants.TAG, "User attributes: ");
+ for (@SuppressWarnings("unchecked")
+ Iterator<Object> it = (Iterator<Object>) pgpSecKey.getUserAttributes(); it.hasNext(); ) {
+ Object attrib = it.next();
+ Log.d(Constants.TAG, " - " + attrib + " -- " + attrib.getClass());
+ }
+
+ X509Name x509name = new X509Name(x509NameOids, x509NameValues);
+
+ Log.d(Constants.TAG, "Subject DN: " + x509name);
+
+ /*
+ * To check the signature from the certificate on the recipient side, the creation time
+ * needs to be embedded in the certificate. It seems natural to make this creation time be
+ * the "not-before" date of the X.509 certificate. Unlimited PGP keys have a validity of 0
+ * second. In this case, the "not-after" date will be the same as the not-before date. This
+ * is something that needs to be checked by the service receiving this certificate.
+ */
+ Date creationTime = pgpPubKey.getCreationTime();
+ Log.d(Constants.TAG,
+ "pgp pub key creation time=" + DateFormat.getDateInstance().format(creationTime));
+ Log.d(Constants.TAG, "pgp valid seconds=" + pgpPubKey.getValidSeconds());
+ Date validTo = null;
+ if (pgpPubKey.getValidSeconds() > 0) {
+ validTo = new Date(creationTime.getTime() + 1000L * pgpPubKey.getValidSeconds());
+ }
+
+ X509Certificate selfSignedCert = createSelfSignedCert(
+ pgpPubKey.getKey(Constants.BOUNCY_CASTLE_PROVIDER_NAME), pgpPrivKey.getKey(),
+ x509name, creationTime, validTo, subjAltNameURI);
+
+ return selfSignedCert;
+ }
+
+ /**
+ * This is a password callback handler that will fill in a password automatically. Useful to
+ * configure passwords in advance, but should be used with caution depending on how much you
+ * allow passwords to be stored within your application.
+ *
+ * @author Bruno Harbulot.
+ */
+ public static final class PredefinedPasswordCallbackHandler implements CallbackHandler {
+
+ private char[] mPassword;
+ private String mPrompt;
+
+ public PredefinedPasswordCallbackHandler(String password) {
+ this(password == null ? null : password.toCharArray(), null);
+ }
+
+ public PredefinedPasswordCallbackHandler(char[] password) {
+ this(password, null);
+ }
+
+ public PredefinedPasswordCallbackHandler(String password, String prompt) {
+ this(password == null ? null : password.toCharArray(), prompt);
+ }
+
+ public PredefinedPasswordCallbackHandler(char[] password, String prompt) {
+ this.mPassword = password;
+ this.mPrompt = prompt;
+ }
+
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (Callback callback : callbacks) {
+ if (callback instanceof PasswordCallback) {
+ PasswordCallback pwCallback = (PasswordCallback) callback;
+ if ((this.mPrompt == null) || (this.mPrompt.equals(pwCallback.getPrompt()))) {
+ pwCallback.setPassword(this.mPassword);
+ }
+ } else {
+ throw new UnsupportedCallbackException(callback, "Unrecognised callback.");
+ }
+ }
+ }
+
+ protected final Object clone() throws CloneNotSupportedException {
+ throw new CloneNotSupportedException();
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/NoAsymmetricEncryptionException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/NoAsymmetricEncryptionException.java
new file mode 100644
index 000000000..23c4bbbd9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/NoAsymmetricEncryptionException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp.exception;
+
+public class NoAsymmetricEncryptionException extends Exception {
+ static final long serialVersionUID = 0xf812773343L;
+
+ public NoAsymmetricEncryptionException() {
+ super();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralException.java
new file mode 100644
index 000000000..418445367
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp.exception;
+
+public class PgpGeneralException extends Exception {
+ static final long serialVersionUID = 0xf812773342L;
+
+ public PgpGeneralException(String message) {
+ super(message);
+ }
+ public PgpGeneralException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralMsgIdException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralMsgIdException.java
new file mode 100644
index 000000000..caa7842db
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralMsgIdException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.pgp.exception;
+
+import android.content.Context;
+
+public class PgpGeneralMsgIdException extends Exception {
+ static final long serialVersionUID = 0xf812773343L;
+
+ private final int mMessageId;
+
+ public PgpGeneralMsgIdException(int messageId) {
+ super("msg[" + messageId + "]");
+ mMessageId = messageId;
+ }
+
+ public PgpGeneralException getContextualized(Context context) {
+ return new PgpGeneralException(context.getString(mMessageId), this);
+ }
+}