PgpKeyOperation: factor out all android dependencies

Conflicts:
	OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java
	OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java
This commit is contained in:
Vincent Breitmoser 2014-03-22 13:04:31 +01:00
parent ebb7f55921
commit 6f36b72eee
5 changed files with 236 additions and 117 deletions

View File

@ -17,20 +17,6 @@
package org.sufficientlysecure.keychain.pgp;
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.TimeZone;
import android.content.Context;
import org.spongycastle.bcpg.CompressionAlgorithmTags;
import org.spongycastle.bcpg.HashAlgorithmTags;
import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
@ -46,18 +32,11 @@ import org.spongycastle.openpgp.operator.jcajce.*;
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.pgp.exception.PgpGeneralMsgIdException;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.util.Primes;
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
import android.content.Context;
import android.util.Pair;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Primes;
import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
import java.io.IOException;
import java.math.BigInteger;
@ -69,9 +48,17 @@ 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 final Context mContext;
private final ProgressDialogUpdater mProgress;
private ProgressDialogUpdater mProgress;
private static final int[] PREFERRED_SYMMETRIC_ALGORITHMS = new int[]{
SymmetricKeyAlgorithmTags.AES_256, SymmetricKeyAlgorithmTags.AES_192,
@ -83,9 +70,8 @@ public class PgpKeyOperation {
CompressionAlgorithmTags.ZLIB, CompressionAlgorithmTags.BZIP2,
CompressionAlgorithmTags.ZIP};
public PgpKeyOperation(Context context, ProgressDialogUpdater progress) {
public PgpKeyOperation(ProgressDialogUpdater progress) {
super();
this.mContext = context;
this.mProgress = progress;
}
@ -101,13 +87,29 @@ public class PgpKeyOperation {
}
}
/**
* 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,
PgpGeneralException, InvalidAlgorithmParameterException {
PgpGeneralMsgIdException, InvalidAlgorithmParameterException {
if (keySize < 512) {
throw new PgpGeneralException(mContext.getString(R.string.error_key_size_minimum512bit));
throw new PgpGeneralMsgIdException(R.string.error_key_size_minimum512bit);
}
if (passphrase == null) {
@ -127,8 +129,7 @@ public class PgpKeyOperation {
case Id.choice.algorithm.elgamal: {
if (isMasterKey) {
throw new PgpGeneralException(
mContext.getString(R.string.error_master_key_must_not_be_el_gamal));
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);
@ -150,8 +151,7 @@ public class PgpKeyOperation {
}
default: {
throw new PgpGeneralException(
mContext.getString(R.string.error_unknown_algorithm_choice));
throw new PgpGeneralMsgIdException(R.string.error_unknown_algorithm_choice);
}
}
@ -171,8 +171,9 @@ public class PgpKeyOperation {
sha1Calc, isMasterKey, keyEncryptor);
}
public void changeSecretKeyPassphrase(PGPSecretKeyRing keyRing, String oldPassPhrase,
String newPassPhrase) throws IOException, PGPException {
public PGPSecretKeyRing changeSecretKeyPassphrase(PGPSecretKeyRing keyRing, String oldPassPhrase,
String newPassPhrase) throws IOException, PGPException,
NoSuchProviderException {
updateProgress(R.string.progress_building_key, 0, 100);
if (oldPassPhrase == null) {
@ -190,16 +191,16 @@ public class PgpKeyOperation {
new JcePBESecretKeyEncryptorBuilder(keyRing.getSecretKey()
.getKeyEncryptionAlgorithm()).build(newPassPhrase.toCharArray()));
updateProgress(R.string.progress_saving_key_ring, 50, 100);
ProviderHelper.saveKeyRing(mContext, newKeyRing);
updateProgress(R.string.progress_done, 100, 100);
return newKeyRing;
}
private void buildNewSecretKey(ArrayList<String> userIds, ArrayList<PGPSecretKey> keys, ArrayList<GregorianCalendar> keysExpiryDates, ArrayList<Integer> keysUsages, String newPassPhrase, String oldPassPhrase) throws PgpGeneralException,
PGPException, SignatureException, IOException {
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;
@ -210,8 +211,6 @@ public class PgpKeyOperation {
// this removes all userIds and certifications previously attached to the masterPublicKey
PGPPublicKey masterPublicKey = masterKey.getPublicKey();
PGPSecretKeyRing mKR = ProviderHelper.getPGPSecretKeyRingByKeyId(mContext, masterKey.getKeyID());
PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray());
PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor);
@ -249,7 +248,7 @@ public class PgpKeyOperation {
//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 PgpGeneralException(mContext.getString(R.string.error_expiry_must_come_after_creation));
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,
@ -319,9 +318,11 @@ public class PgpKeyOperation {
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 PgpGeneralException(mContext.getString(R.string.error_expiry_must_come_after_creation));
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,
@ -334,23 +335,18 @@ public class PgpKeyOperation {
PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing();
PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing();
updateProgress(R.string.progress_saving_key_ring, 90, 100);
return new Pair<PGPSecretKeyRing,PGPPublicKeyRing>(secretKeyRing, publicKeyRing);
ProviderHelper.saveKeyRing(mContext, secretKeyRing);
ProviderHelper.saveKeyRing(mContext, publicKeyRing);
updateProgress(R.string.progress_done, 100, 100);
}
public void buildSecretKey (SaveKeyringParcel saveParcel)throws PgpGeneralException,
PGPException, SignatureException, IOException {
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);
PGPSecretKeyRing mKR = ProviderHelper.getPGPSecretKeyRingByKeyId(mContext, masterKey.getKeyID());
PGPPublicKeyRing pKR = ProviderHelper.getPGPPublicKeyRingByKeyId(mContext, masterKey.getKeyID());
if (saveParcel.oldPassPhrase == null) {
saveParcel.oldPassPhrase = "";
}
@ -359,9 +355,8 @@ public class PgpKeyOperation {
}
if (mKR == null) {
buildNewSecretKey(saveParcel.userIDs, saveParcel.keys, saveParcel.keysExpiryDates,
return buildNewSecretKey(saveParcel.userIDs, saveParcel.keys, saveParcel.keysExpiryDates,
saveParcel.keysUsages, saveParcel.newPassPhrase, saveParcel.oldPassPhrase); //new Keyring
return;
}
/*
@ -430,7 +425,7 @@ public class PgpKeyOperation {
//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 PgpGeneralException(mContext.getString(R.string.error_expiry_must_come_after_creation));
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,
@ -592,7 +587,7 @@ public class PgpKeyOperation {
//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 PgpGeneralException(mContext.getString(R.string.error_expiry_must_come_after_creation));
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,
@ -637,7 +632,6 @@ public class PgpKeyOperation {
//update the passphrase
mKR = PGPSecretKeyRing.copyWithNewPassword(mKR, keyDecryptor, keyEncryptorNew);
updateProgress(R.string.progress_saving_key_ring, 90, 100);
/* additional handy debug info
@ -660,34 +654,35 @@ public class PgpKeyOperation {
*/
return new Pair<PGPSecretKeyRing,PGPPublicKeyRing>(mKR, pKR);
ProviderHelper.saveKeyRing(mContext, mKR);
ProviderHelper.saveKeyRing(mContext, pKR);
updateProgress(R.string.progress_done, 100, 100);
}
public PGPPublicKeyRing certifyKey(long masterKeyId, long pubKeyId, List<String> userIds, String passphrase)
throws PgpGeneralException, NoSuchAlgorithmException, NoSuchProviderException,
/**
* 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 {
if (passphrase == null) {
throw new PgpGeneralException("Unable to obtain passphrase");
} else {
// create a signatureGenerator from the supplied masterKeyId and passphrase
PGPSignatureGenerator signatureGenerator; {
PGPSecretKey certificationKey = PgpKeyHelper.getCertificationKey(mContext, masterKeyId);
if (certificationKey == null) {
throw new PgpGeneralException(mContext.getString(R.string.error_signature_failed));
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 PgpGeneralException(
mContext.getString(R.string.error_could_not_extract_private_key));
throw new PgpGeneralMsgIdException(R.string.error_could_not_extract_private_key);
}
// TODO: SHA256 fixed?
@ -706,16 +701,25 @@ public class PgpKeyOperation {
}
// fetch public key ring, add the certification and return it
PGPPublicKeyRing pubring = ProviderHelper
.getPGPPublicKeyRingByKeyId(mContext, pubKeyId);
PGPPublicKey signedKey = pubring.getPublicKey(pubKeyId);
for(String userId : new IterableIterator<String>(userIds.iterator())) {
PGPSignature sig = signatureGenerator.generateCertification(userId, signedKey);
signedKey = PGPPublicKey.addCertification(signedKey, userId, sig);
PGPSignature sig = signatureGenerator.generateCertification(userId, publicKey);
publicKey = PGPPublicKey.addCertification(publicKey, userId, sig);
}
pubring = PGPPublicKeyRing.insertPublicKey(pubring, signedKey);
return pubring;
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;
}
}
}

View File

@ -23,4 +23,7 @@ public class PgpGeneralException extends Exception {
public PgpGeneralException(String message) {
super(message);
}
public PgpGeneralException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -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 msgId;
public PgpGeneralMsgIdException(int msgId) {
super("msg[" + msgId + "]");
this.msgId = msgId;
}
public PgpGeneralException getContextualized(Context context) {
return new PgpGeneralException(context.getString(msgId), this);
}
}

View File

@ -35,6 +35,8 @@ import org.sufficientlysecure.keychain.helper.OtherHelper;
import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.pgp.*;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralMsgIdException;
import org.sufficientlysecure.keychain.provider.KeychainContract.DataStream;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import org.sufficientlysecure.keychain.util.*;
@ -473,14 +475,26 @@ public class KeychainIntentService extends IntentService
long masterKeyId = saveParams.keys.get(0).getKeyID();
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
/* Operation */
if (!canSign) {
keyOperations.changeSecretKeyPassphrase(
ProviderHelper.getPGPSecretKeyRingByKeyId(this, masterKeyId),
PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler(this, 0, 50, 100));
PGPSecretKeyRing keyRing = ProviderHelper
.getPGPSecretKeyRingByKeyId(this, masterKeyId);
keyRing = keyOperations.changeSecretKeyPassphrase(keyRing,
oldPassPhrase, newPassPhrase);
setProgress(R.string.progress_saving_key_ring, 50, 100);
ProviderHelper.saveKeyRing(this, keyRing);
setProgress(R.string.progress_done, 100, 100);
} else {
keyOperations.buildSecretKey(saveParams);
PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler(this, 0, 90, 100));
PGPSecretKeyRing privkey = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this, masterKeyId);
PGPPublicKeyRing pubkey = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(this, masterKeyId);
PgpKeyOperation.Pair<PGPSecretKeyRing,PGPPublicKeyRing> pair =
keyOperations.buildSecretKey(privkey, pubkey, saveParams);
setProgress(R.string.progress_saving_key_ring, 90, 100);
ProviderHelper.saveKeyRing(this, pair.first);
ProviderHelper.saveKeyRing(this, pair.second);
setProgress(R.string.progress_done, 100, 100);
}
PassphraseCacheService.addCachedPassphrase(this, masterKeyId, newPassPhrase);
@ -498,7 +512,7 @@ public class KeychainIntentService extends IntentService
boolean masterKey = data.getBoolean(GENERATE_KEY_MASTER_KEY);
/* Operation */
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler(this, 0, 100, 100));
PGPSecretKey newKey = keyOperations.createKey(algorithm, keysize,
passphrase, masterKey);
@ -529,7 +543,7 @@ public class KeychainIntentService extends IntentService
getQuantityString(R.plurals.progress_generating, keysTotal),
keysCreated,
keysTotal);
PgpKeyOperation keyOperations = new PgpKeyOperation(this, this);
PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler(this, 0, 100, 100));
PGPSecretKey masterKey = keyOperations.createKey(Id.choice.algorithm.rsa,
4096, passphrase, true);
@ -771,14 +785,23 @@ public class KeychainIntentService extends IntentService
/* Operation */
String signaturePassPhrase = PassphraseCacheService.getCachedPassphrase(this,
masterKeyId);
if (signaturePassPhrase == null) {
throw new PgpGeneralException("Unable to obtain passphrase");
}
PgpKeyOperation keyOperation = new PgpKeyOperation(this, this);
PGPPublicKeyRing signedPubKeyRing = keyOperation.certifyKey(masterKeyId, pubKeyId,
PgpKeyOperation keyOperation = new PgpKeyOperation(new ProgressScaler(this, 0, 100, 100));
PGPPublicKeyRing publicRing = ProviderHelper
.getPGPPublicKeyRingByKeyId(this, pubKeyId);
PGPPublicKey publicKey = publicRing.getPublicKey(pubKeyId);
PGPSecretKey certificationKey = PgpKeyHelper.getCertificationKey(this,
masterKeyId);
publicKey = keyOperation.certifyKey(certificationKey, publicKey,
userIds, signaturePassPhrase);
publicRing = PGPPublicKeyRing.insertPublicKey(publicRing, publicKey);
// store the signed key in our local cache
PgpImportExport pgpImportExport = new PgpImportExport(this, null);
int retval = pgpImportExport.storeKeyRingInCache(signedPubKeyRing);
int retval = pgpImportExport.storeKeyRingInCache(publicRing);
if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
throw new PgpGeneralException("Failed to store signed key in local cache");
}
@ -795,7 +818,11 @@ public class KeychainIntentService extends IntentService
if (this.mIsCanceled) {
return;
}
Log.e(Constants.TAG, "KeychainIntentService Exception: ", e);
// contextualize the exception, if necessary
if(e instanceof PgpGeneralMsgIdException) {
e = ((PgpGeneralMsgIdException) e).getContextualized(this);
}
Log.e(Constants.TAG, "ApgService Exception: ", e);
e.printStackTrace();
Bundle data = new Bundle();

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2012-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.util;
/** This is a simple class that wraps a ProgressDialogUpdater, scaling the progress
* values into a specified range.
*/
public class ProgressScaler implements ProgressDialogUpdater {
final ProgressDialogUpdater mWrapped;
final int mFrom, mTo, mMax;
public ProgressScaler(ProgressDialogUpdater wrapped, int from, int to, int max) {
this.mWrapped = wrapped;
this.mFrom = from;
this.mTo = to;
this.mMax = max;
}
/**
* Set progressDialogUpdater of ProgressDialog by sending message to handler on UI thread
*/
public void setProgress(String message, int progress, int max) {
mWrapped.setProgress(message, mFrom+ progress*(mTo-mFrom)/max, mMax);
}
public void setProgress(int resourceId, int progress, int max) {
mWrapped.setProgress(resourceId, progress, mMax);
}
public void setProgress(int progress, int max) {
mWrapped.setProgress(progress, max);
}
}