651 lines
27 KiB
Java
651 lines
27 KiB
Java
/*
|
|
* Copyright (C) 2017 Schürmann & Breitmoser GbR
|
|
*
|
|
* 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.operations;
|
|
|
|
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.concurrent.Callable;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.ExecutorCompletionService;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
import android.content.Context;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import org.sufficientlysecure.keychain.R;
|
|
import org.sufficientlysecure.keychain.daos.KeyMetadataDao;
|
|
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
|
|
import org.sufficientlysecure.keychain.keyimport.FacebookKeyserverClient;
|
|
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverAddress;
|
|
import org.sufficientlysecure.keychain.keyimport.HkpKeyserverClient;
|
|
import org.sufficientlysecure.keychain.keyimport.KeyserverClient;
|
|
import org.sufficientlysecure.keychain.keyimport.KeyserverClient.QueryNotFoundException;
|
|
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
|
import org.sufficientlysecure.keychain.network.orbot.OrbotHelper;
|
|
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
|
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
|
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
|
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
|
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
|
import org.sufficientlysecure.keychain.operations.results.UpdateTrustResult;
|
|
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing;
|
|
import org.sufficientlysecure.keychain.pgp.Progressable;
|
|
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
|
|
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
|
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
|
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
|
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
|
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
|
import org.sufficientlysecure.keychain.util.IteratorWithSize;
|
|
import org.sufficientlysecure.keychain.util.ParcelableFileCache;
|
|
import org.sufficientlysecure.keychain.util.ParcelableProxy;
|
|
import org.sufficientlysecure.keychain.util.Preferences;
|
|
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
|
import timber.log.Timber;
|
|
|
|
|
|
/**
|
|
* An operation class which implements high level import
|
|
* operations.
|
|
* This class receives a source and/or destination of keys as input and performs
|
|
* all steps for this import.
|
|
* For the import operation, the only valid source is an Iterator of
|
|
* ParcelableKeyRing, each of which must contain either a single
|
|
* keyring encoded as bytes, or a unique reference to a keyring on keyservers.
|
|
* It is important to note that public keys should generally be imported before
|
|
* secret keys, because some implementations (notably Symantec PGP Desktop) do
|
|
* not include self certificates for user ids in the secret keyring. The import
|
|
* method here will generally import keyrings in the order given by the
|
|
* iterator, so this should be ensured beforehand.
|
|
*/
|
|
public class ImportOperation extends BaseReadWriteOperation<ImportKeyringParcel> {
|
|
|
|
private static final int MAX_THREADS = 10;
|
|
|
|
public static final String CACHE_FILE_NAME = "key_import.pcl";
|
|
|
|
private final KeyMetadataDao keyMetadataDao;
|
|
|
|
private FacebookKeyserverClient facebookServer;
|
|
|
|
public ImportOperation(Context context, KeyWritableRepository databaseInteractor, Progressable progressable) {
|
|
super(context, databaseInteractor, progressable);
|
|
|
|
this.keyMetadataDao = KeyMetadataDao.create(context);
|
|
}
|
|
|
|
public ImportOperation(Context context, KeyWritableRepository databaseInteractor,
|
|
Progressable progressable, AtomicBoolean cancelled) {
|
|
super(context, databaseInteractor, progressable, cancelled);
|
|
|
|
this.keyMetadataDao = KeyMetadataDao.create(context);
|
|
}
|
|
|
|
// Overloaded functions for using progressable supplied in constructor during import
|
|
public ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int num,
|
|
HkpKeyserverAddress keyserver, ParcelableProxy proxy, boolean skipSave, boolean forceReinsert) {
|
|
return serialKeyRingImport(entries, num, keyserver, mProgressable, proxy, skipSave, forceReinsert);
|
|
}
|
|
|
|
@NonNull
|
|
private ImportKeyResult serialKeyRingImport(ParcelableFileCache<ParcelableKeyRing> cache,
|
|
HkpKeyserverAddress keyserver, ParcelableProxy proxy, boolean skipSave, boolean forceReinsert) {
|
|
|
|
// get entries from cached file
|
|
try {
|
|
IteratorWithSize<ParcelableKeyRing> it = cache.readCache();
|
|
int numEntries = it.getSize();
|
|
|
|
return serialKeyRingImport(it, numEntries, keyserver, mProgressable, proxy, skipSave, forceReinsert);
|
|
} catch (IOException e) {
|
|
|
|
// Special treatment here, we need a lot
|
|
OperationLog log = new OperationLog();
|
|
log.add(LogType.MSG_IMPORT, 0, 0);
|
|
log.add(LogType.MSG_IMPORT_ERROR_IO, 0, 0);
|
|
|
|
return new ImportKeyResult(ImportKeyResult.RESULT_ERROR, log);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* @param entries keys to import
|
|
* @param numTotalKeys number of keys to import
|
|
* @param hkpKeyserver contains uri of keyserver to import from, if it is an import from cloud
|
|
* @param progressable Allows multi-threaded import to supply a progressable that ignores the
|
|
* progress of a single key being imported
|
|
*/
|
|
@NonNull
|
|
private ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int numTotalKeys,
|
|
HkpKeyserverAddress hkpKeyserver, Progressable progressable, @NonNull ParcelableProxy proxy,
|
|
boolean skipSave, boolean forceReinsert) {
|
|
if (progressable != null) {
|
|
progressable.setProgress(R.string.progress_importing, 0, 100);
|
|
}
|
|
|
|
OperationLog log = new OperationLog();
|
|
log.add(LogType.MSG_IMPORT, 0, numTotalKeys);
|
|
|
|
// If there aren't even any keys, do nothing here.
|
|
if (entries == null || !entries.hasNext()) {
|
|
return new ImportKeyResult(ImportKeyResult.RESULT_FAIL_NOTHING, log);
|
|
}
|
|
|
|
int newKeys = 0, updatedKeys = 0, missingKeys = 0, badKeys = 0;
|
|
ArrayList<Long> secretMasterKeyIds = new ArrayList<>();
|
|
ArrayList<Long> importedMasterKeyIds = new ArrayList<>();
|
|
|
|
ArrayList<CanonicalizedKeyRing> canKeyRings = new ArrayList<>();
|
|
|
|
boolean cancelled = false;
|
|
int keyImportsFinished = 0;
|
|
|
|
// iterate over all entries
|
|
while (entries.hasNext()) {
|
|
ParcelableKeyRing entry = entries.next();
|
|
|
|
// Has this action been cancelled? If so, don't proceed any further
|
|
if (checkCancelled()) {
|
|
cancelled = true;
|
|
break;
|
|
}
|
|
|
|
boolean keyWasDownloaded = false;
|
|
|
|
try {
|
|
|
|
UncachedKeyRing key = null;
|
|
|
|
// If there is already byte data, use that
|
|
if (entry.getBytes() != null) {
|
|
key = UncachedKeyRing.decodeFromData(entry.getBytes());
|
|
} else {
|
|
try {
|
|
key = fetchKeyFromInternet(hkpKeyserver, proxy, log, entry, key);
|
|
} catch (QueryNotFoundException e) {
|
|
// note that this does NOT fire on network errors! those will be logged inline and return in null
|
|
log.add(LogType.MSG_IMPORT_FETCH_ERROR_NOT_FOUND, 2);
|
|
missingKeys += 1;
|
|
|
|
byte[] fingerprintHex = entry.getExpectedFingerprint();
|
|
if (fingerprintHex != null) {
|
|
keyMetadataDao.renewKeyLastUpdatedTime(
|
|
KeyFormattingUtils.getKeyIdFromFingerprint(fingerprintHex), false);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (key != null) {
|
|
keyWasDownloaded = true;
|
|
|
|
if (key.isSecret()) {
|
|
log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER_SECRET, 2);
|
|
badKeys += 1;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (key == null) {
|
|
log.add(LogType.MSG_IMPORT_FETCH_ERROR, 2);
|
|
badKeys += 1;
|
|
continue;
|
|
}
|
|
|
|
SaveKeyringResult result;
|
|
// synchronizing prevents https://github.com/open-keychain/open-keychain/issues/1221
|
|
// and https://github.com/open-keychain/open-keychain/issues/1480
|
|
synchronized (mKeyRepository) {
|
|
mKeyRepository.clearLog();
|
|
if (key.isSecret()) {
|
|
result = mKeyWritableRepository.saveSecretKeyRing(key, canKeyRings, skipSave);
|
|
} else {
|
|
result = mKeyWritableRepository.savePublicKeyRing(key, entry.getExpectedFingerprint(), canKeyRings,
|
|
forceReinsert, skipSave);
|
|
}
|
|
}
|
|
if (!result.success()) {
|
|
badKeys += 1;
|
|
} else {
|
|
if (result.updated()) {
|
|
updatedKeys += 1;
|
|
importedMasterKeyIds.add(key.getMasterKeyId());
|
|
} else {
|
|
newKeys += 1;
|
|
if (key.isSecret()) {
|
|
secretMasterKeyIds.add(key.getMasterKeyId());
|
|
}
|
|
importedMasterKeyIds.add(key.getMasterKeyId());
|
|
}
|
|
|
|
if (!skipSave && keyWasDownloaded) {
|
|
keyMetadataDao.renewKeyLastUpdatedTime(key.getMasterKeyId(), true);
|
|
}
|
|
}
|
|
|
|
log.add(result, 2);
|
|
} catch (IOException | PgpGeneralException e) {
|
|
Timber.e(e, "Encountered bad key on import!");
|
|
++badKeys;
|
|
}
|
|
|
|
// update progress
|
|
keyImportsFinished += 1;
|
|
progressable.setProgress(keyImportsFinished, numTotalKeys);
|
|
}
|
|
|
|
// Special: consolidate on secret key import (cannot be cancelled!)
|
|
// synchronized on mProviderHelper to prevent
|
|
// https://github.com/open-keychain/open-keychain/issues/1221 since a consolidate deletes
|
|
// and re-inserts keys, which could conflict with a parallel db key update
|
|
if (!skipSave && !secretMasterKeyIds.isEmpty()) {
|
|
setPreventCancel();
|
|
synchronized (mKeyRepository) {
|
|
UpdateTrustResult result = mKeyWritableRepository.updateTrustDb(secretMasterKeyIds, progressable);
|
|
log.add(result, 1);
|
|
}
|
|
}
|
|
|
|
// convert to long array
|
|
long[] importedMasterKeyIdsArray = new long[importedMasterKeyIds.size()];
|
|
for (int i = 0; i < importedMasterKeyIds.size(); ++i) {
|
|
importedMasterKeyIdsArray[i] = importedMasterKeyIds.get(i);
|
|
}
|
|
|
|
int resultType = 0;
|
|
if (cancelled) {
|
|
log.add(LogType.MSG_OPERATION_CANCELLED, 1);
|
|
resultType |= ImportKeyResult.RESULT_CANCELLED;
|
|
}
|
|
|
|
// special return case: no new keys at all
|
|
if (badKeys == 0 && newKeys == 0 && updatedKeys == 0) {
|
|
// if keys merely aren't on keyservers, it's just a warning
|
|
resultType = ImportKeyResult.RESULT_FAIL_NOTHING;
|
|
} else {
|
|
if (newKeys > 0) {
|
|
resultType |= ImportKeyResult.RESULT_OK_NEWKEYS;
|
|
}
|
|
if (updatedKeys > 0) {
|
|
resultType |= ImportKeyResult.RESULT_OK_UPDATED;
|
|
}
|
|
if (badKeys > 0) {
|
|
resultType |= ImportKeyResult.RESULT_WITH_ERRORS;
|
|
if (newKeys == 0 && updatedKeys == 0) {
|
|
resultType |= ImportKeyResult.RESULT_ERROR;
|
|
}
|
|
}
|
|
if (log.containsWarnings()) {
|
|
resultType |= ImportKeyResult.RESULT_WARNINGS;
|
|
}
|
|
}
|
|
|
|
if (!cancelled) {
|
|
// Final log entry, it's easier to do this individually
|
|
if ((newKeys > 0 || updatedKeys > 0) && badKeys > 0) {
|
|
log.add(LogType.MSG_IMPORT_PARTIAL, 1);
|
|
} else if (newKeys > 0 || updatedKeys > 0) {
|
|
log.add(LogType.MSG_IMPORT_SUCCESS, 1);
|
|
} else {
|
|
log.add(LogType.MSG_IMPORT_ERROR, 1);
|
|
}
|
|
}
|
|
|
|
ImportKeyResult result = new ImportKeyResult(
|
|
resultType, log, newKeys, updatedKeys, missingKeys, badKeys, secretMasterKeyIds.size(),
|
|
importedMasterKeyIdsArray);
|
|
|
|
result.setCanonicalizedKeyRings(canKeyRings);
|
|
return result;
|
|
}
|
|
|
|
private UncachedKeyRing fetchKeyFromInternet(HkpKeyserverAddress hkpKeyserver, @NonNull ParcelableProxy proxy,
|
|
OperationLog log, ParcelableKeyRing entry, UncachedKeyRing key)
|
|
throws PgpGeneralException, IOException, QueryNotFoundException {
|
|
QueryNotFoundException queryNotFoundException = null;
|
|
|
|
boolean canFetchFromKeyservers =
|
|
hkpKeyserver != null && (entry.getKeyIdHex() != null || entry.getExpectedFingerprint()!= null);
|
|
if (canFetchFromKeyservers) {
|
|
UncachedKeyRing keyserverKey = null;
|
|
try {
|
|
keyserverKey = fetchKeyFromKeyserver(hkpKeyserver, proxy, log, entry);
|
|
} catch (QueryNotFoundException e) {
|
|
queryNotFoundException = e;
|
|
}
|
|
|
|
if (keyserverKey != null) {
|
|
key = keyserverKey;
|
|
}
|
|
}
|
|
|
|
boolean hasFacebookName = entry.getFbUsername() != null;
|
|
if (hasFacebookName) {
|
|
UncachedKeyRing facebookKey = fetchKeyFromFacebook(proxy, log, entry);
|
|
if (facebookKey != null) {
|
|
key = mergeKeysOrUseEither(log, 3, key, facebookKey);
|
|
}
|
|
}
|
|
|
|
if (key == null && queryNotFoundException != null) {
|
|
throw queryNotFoundException;
|
|
}
|
|
|
|
return key;
|
|
}
|
|
|
|
@Nullable
|
|
private UncachedKeyRing fetchKeyFromKeyserver(HkpKeyserverAddress hkpKeyserver, @NonNull ParcelableProxy proxy,
|
|
OperationLog log, ParcelableKeyRing entry) throws PgpGeneralException, IOException, KeyserverClient.QueryNotFoundException {
|
|
try {
|
|
byte[] data;
|
|
log.add(LogType.MSG_IMPORT_KEYSERVER, 1, hkpKeyserver);
|
|
|
|
HkpKeyserverClient keyserverInteractor = HkpKeyserverClient.fromHkpKeyserverAddress(hkpKeyserver);
|
|
|
|
// Download by fingerprint, or keyId - whichever is available
|
|
if (entry.getExpectedFingerprint() != null) {
|
|
String fingerprintHex = KeyFormattingUtils.convertFingerprintToHex(entry.getExpectedFingerprint());
|
|
log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER, 2, "0x" +
|
|
fingerprintHex.substring(24));
|
|
data = keyserverInteractor.get("0x" + fingerprintHex, proxy).getBytes();
|
|
} else {
|
|
log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER, 2, entry.getKeyIdHex());
|
|
data = keyserverInteractor.get(entry.getKeyIdHex(), proxy).getBytes();
|
|
}
|
|
UncachedKeyRing keyserverKey = UncachedKeyRing.decodeFromData(data);
|
|
if (keyserverKey != null) {
|
|
log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_OK, 3);
|
|
} else {
|
|
log.add(LogType.MSG_IMPORT_FETCH_ERROR_DECODE, 3);
|
|
}
|
|
|
|
return keyserverKey;
|
|
} catch (KeyserverClient.QueryNotFoundException e) {
|
|
throw e;
|
|
} catch (KeyserverClient.QueryFailedException e) {
|
|
Timber.d(e, "query failed");
|
|
log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER, 3, e.getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private UncachedKeyRing fetchKeyFromFacebook(@NonNull ParcelableProxy proxy, OperationLog log, ParcelableKeyRing entry)
|
|
throws PgpGeneralException, IOException {
|
|
if (facebookServer == null) {
|
|
facebookServer = FacebookKeyserverClient.getInstance();
|
|
}
|
|
|
|
try {
|
|
log.add(LogType.MSG_IMPORT_FETCH_FACEBOOK, 2, entry.getFbUsername());
|
|
byte[] data = facebookServer.get(entry.getFbUsername(), proxy).getBytes();
|
|
UncachedKeyRing facebookKey = UncachedKeyRing.decodeFromData(data);
|
|
|
|
if (facebookKey != null) {
|
|
log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_OK, 3);
|
|
} else {
|
|
log.add(LogType.MSG_IMPORT_FETCH_ERROR_DECODE, 3);
|
|
}
|
|
|
|
return facebookKey;
|
|
} catch (KeyserverClient.QueryFailedException e) {
|
|
// download failed, too bad. just proceed
|
|
Timber.e(e, "query failed");
|
|
log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER, 3, e.getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
private UncachedKeyRing mergeKeysOrUseEither(OperationLog log, int indent,
|
|
UncachedKeyRing firstKey, UncachedKeyRing otherKey) {
|
|
if (firstKey == null) {
|
|
return otherKey;
|
|
}
|
|
|
|
log.add(LogType.MSG_IMPORT_MERGE, indent);
|
|
UncachedKeyRing mergedKey = firstKey.merge(otherKey, log, indent +1);
|
|
|
|
if (mergedKey != null) {
|
|
return mergedKey;
|
|
} else {
|
|
log.add(LogType.MSG_IMPORT_MERGE_ERROR, indent +1);
|
|
return firstKey;
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) {
|
|
List<ParcelableKeyRing> keyList = importInput.getKeyList();
|
|
HkpKeyserverAddress keyServer = importInput.getKeyserver();
|
|
boolean skipSave = importInput.isSkipSave();
|
|
boolean forceReinsert = importInput.isForceReinsert();
|
|
|
|
ImportKeyResult result;
|
|
if (keyList == null) {// import from file, do serially
|
|
ParcelableFileCache<ParcelableKeyRing> cache =
|
|
new ParcelableFileCache<>(mContext, CACHE_FILE_NAME);
|
|
result = serialKeyRingImport(cache, null, null, skipSave, forceReinsert);
|
|
} else {
|
|
ParcelableProxy proxy;
|
|
if (cryptoInput.getParcelableProxy() == null) {
|
|
// explicit proxy not set
|
|
if (!OrbotHelper.isOrbotInRequiredState(mContext)) {
|
|
// show dialog to enable/install dialog
|
|
return new ImportKeyResult(null,
|
|
RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput);
|
|
}
|
|
proxy = Preferences.getPreferences(mContext).getParcelableProxy();
|
|
} else {
|
|
proxy = cryptoInput.getParcelableProxy();
|
|
}
|
|
|
|
result = multiThreadedKeyImport(keyList, keyServer, proxy, skipSave, forceReinsert);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@NonNull
|
|
private ImportKeyResult multiThreadedKeyImport(List<ParcelableKeyRing> keyList, HkpKeyserverAddress keyServer,
|
|
ParcelableProxy proxy, boolean skipSave, boolean forceReinsert) {
|
|
Timber.d("Multi-threaded key import starting");
|
|
|
|
final Iterator<ParcelableKeyRing> keyListIterator = keyList.iterator();
|
|
final int totKeys = keyList.size();
|
|
|
|
ExecutorService importExecutor = new ThreadPoolExecutor(0, MAX_THREADS, 30L, TimeUnit.SECONDS,
|
|
new LinkedBlockingQueue<Runnable>());
|
|
ExecutorCompletionService<ImportKeyResult> importCompletionService =
|
|
new ExecutorCompletionService<>(importExecutor);
|
|
|
|
while (keyListIterator.hasNext()) { // submit all key rings to be imported
|
|
|
|
final ParcelableKeyRing pkRing = keyListIterator.next();
|
|
|
|
Callable<ImportKeyResult> importOperationCallable = new Callable<ImportKeyResult>
|
|
() {
|
|
|
|
@Override
|
|
public ImportKeyResult call() {
|
|
if (checkCancelled()) {
|
|
return null;
|
|
}
|
|
|
|
ArrayList<ParcelableKeyRing> list = new ArrayList<>();
|
|
list.add(pkRing);
|
|
ProgressScaler ignoreProgressable = new ProgressScaler();
|
|
|
|
return serialKeyRingImport(list.iterator(), 1, keyServer, ignoreProgressable,
|
|
proxy, skipSave, forceReinsert);
|
|
}
|
|
};
|
|
|
|
importCompletionService.submit(importOperationCallable);
|
|
}
|
|
|
|
KeyImportAccumulator accumulator = new KeyImportAccumulator(totKeys, mProgressable);
|
|
while (!accumulator.isImportFinished()) { // accumulate the results of each import
|
|
try {
|
|
accumulator.accumulateKeyImport(importCompletionService.take().get());
|
|
} catch (InterruptedException | ExecutionException e) {
|
|
Timber.e(e, "A key could not be imported during multi-threaded " +
|
|
"import");
|
|
// do nothing?
|
|
if (e instanceof ExecutionException) {
|
|
// Since serialKeyRingImport does not throw any exceptions, this is what
|
|
// would have happened if
|
|
// we were importing the key on this thread
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
}
|
|
return accumulator.getConsolidatedResult();
|
|
}
|
|
|
|
/**
|
|
* Used to accumulate the results of individual key imports
|
|
*/
|
|
public static class KeyImportAccumulator {
|
|
private OperationResult.OperationLog mImportLog = new OperationResult.OperationLog();
|
|
private Progressable mProgressable;
|
|
private int mTotalKeys;
|
|
private int mImportedKeys = 0;
|
|
private ArrayList<Long> mImportedMasterKeyIds = new ArrayList<>();
|
|
private int mBadKeys = 0;
|
|
private int mNewKeys = 0;
|
|
private int mUpdatedKeys = 0;
|
|
private int mMissingKeys = 0;
|
|
private int mSecret = 0;
|
|
private int mResultType = 0;
|
|
private boolean mHasCancelledResult;
|
|
|
|
ArrayList<CanonicalizedKeyRing> mCanonicalizedKeyRings;
|
|
|
|
/**
|
|
* Accumulates keyring imports and updates the progressable whenever a new key is imported.
|
|
* Also sets the progress to 0 on instantiation.
|
|
*
|
|
* @param totalKeys total number of keys to be imported
|
|
* @param externalProgressable the external progressable to be updated every time a key
|
|
* is imported
|
|
*/
|
|
public KeyImportAccumulator(int totalKeys, Progressable externalProgressable) {
|
|
mTotalKeys = totalKeys;
|
|
mProgressable = externalProgressable;
|
|
if (mProgressable != null) {
|
|
mProgressable.setProgress(0, totalKeys);
|
|
}
|
|
|
|
mCanonicalizedKeyRings = new ArrayList<>();
|
|
}
|
|
|
|
public void accumulateKeyImport(ImportKeyResult result) {
|
|
mImportedKeys++;
|
|
|
|
if (result == null) {
|
|
return;
|
|
}
|
|
|
|
if (mProgressable != null) {
|
|
mProgressable.setProgress(mImportedKeys, mTotalKeys);
|
|
}
|
|
|
|
boolean notCancelledOrFirstCancelled = !result.cancelled() || !mHasCancelledResult;
|
|
if (notCancelledOrFirstCancelled) {
|
|
mImportLog.addAll(result.getLog().toList()); //accumulates log
|
|
if (result.cancelled()) {
|
|
mHasCancelledResult = true;
|
|
}
|
|
}
|
|
|
|
mBadKeys += result.mBadKeys;
|
|
mNewKeys += result.mNewKeys;
|
|
mUpdatedKeys += result.mUpdatedKeys;
|
|
mMissingKeys += result.mMissingKeys;
|
|
mSecret += result.mSecret;
|
|
|
|
long[] masterKeyIds = result.getImportedMasterKeyIds();
|
|
for (long masterKeyId : masterKeyIds) {
|
|
mImportedMasterKeyIds.add(masterKeyId);
|
|
}
|
|
|
|
mCanonicalizedKeyRings.addAll(result.mCanonicalizedKeyRings);
|
|
|
|
// if any key import has been cancelled, set result type to cancelled
|
|
// resultType is added to in getConsolidatedKayImport to account for remaining factors
|
|
mResultType |= result.getResult() & ImportKeyResult.RESULT_CANCELLED;
|
|
}
|
|
|
|
/**
|
|
* returns accumulated result of all imports so far
|
|
*/
|
|
public ImportKeyResult getConsolidatedResult() {
|
|
|
|
// adding required information to mResultType
|
|
// special case,no keys requested for import
|
|
if (mBadKeys == 0 && mNewKeys == 0 && mUpdatedKeys == 0
|
|
&& (mResultType & ImportKeyResult.RESULT_CANCELLED)
|
|
!= ImportKeyResult.RESULT_CANCELLED) {
|
|
mResultType = ImportKeyResult.RESULT_FAIL_NOTHING;
|
|
} else {
|
|
if (mNewKeys > 0) {
|
|
mResultType |= ImportKeyResult.RESULT_OK_NEWKEYS;
|
|
}
|
|
if (mUpdatedKeys > 0) {
|
|
mResultType |= ImportKeyResult.RESULT_OK_UPDATED;
|
|
}
|
|
if (mBadKeys > 0) {
|
|
mResultType |= ImportKeyResult.RESULT_WITH_ERRORS;
|
|
if (mNewKeys == 0 && mUpdatedKeys == 0) {
|
|
mResultType |= ImportKeyResult.RESULT_ERROR;
|
|
}
|
|
}
|
|
if (mImportLog.containsWarnings()) {
|
|
mResultType |= ImportKeyResult.RESULT_WARNINGS;
|
|
}
|
|
}
|
|
|
|
long masterKeyIds[] = new long[mImportedMasterKeyIds.size()];
|
|
for (int i = 0; i < masterKeyIds.length; i++) {
|
|
masterKeyIds[i] = mImportedMasterKeyIds.get(i);
|
|
}
|
|
|
|
ImportKeyResult result = new ImportKeyResult(mResultType, mImportLog, mNewKeys,
|
|
mUpdatedKeys, mMissingKeys, mBadKeys, mSecret, masterKeyIds);
|
|
|
|
result.setCanonicalizedKeyRings(mCanonicalizedKeyRings);
|
|
return result;
|
|
}
|
|
|
|
boolean isImportFinished() {
|
|
return mTotalKeys == mImportedKeys;
|
|
}
|
|
}
|
|
|
|
}
|