diff options
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp')
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); + } +} |