362 lines
15 KiB
Java
362 lines
15 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.remote;
|
|
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
|
|
import android.app.PendingIntent;
|
|
import android.content.ContentResolver;
|
|
import android.content.Intent;
|
|
import android.database.Cursor;
|
|
import android.net.Uri;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
import org.openintents.openpgp.util.OpenPgpApi;
|
|
import org.sufficientlysecure.keychain.provider.KeychainExternalContract;
|
|
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus;
|
|
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
|
import timber.log.Timber;
|
|
|
|
|
|
class OpenPgpServiceKeyIdExtractor {
|
|
@VisibleForTesting
|
|
static final String[] PROJECTION_MAIL_STATUS = {
|
|
AutocryptStatus.ADDRESS,
|
|
AutocryptStatus.UID_MASTER_KEY_ID,
|
|
AutocryptStatus.UID_KEY_STATUS,
|
|
AutocryptStatus.UID_CANDIDATES,
|
|
AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID,
|
|
AutocryptStatus.AUTOCRYPT_KEY_STATUS,
|
|
AutocryptStatus.AUTOCRYPT_PEER_STATE
|
|
};
|
|
private static final int INDEX_EMAIL_ADDRESS = 0;
|
|
private static final int INDEX_MASTER_KEY_ID = 1;
|
|
private static final int INDEX_USER_ID_STATUS = 2;
|
|
private static final int INDEX_USER_ID_CANDIDATES = 3;
|
|
private static final int INDEX_AUTOCRYPT_MASTER_KEY_ID = 4;
|
|
private static final int INDEX_AUTOCRYPT_KEY_STATUS = 5;
|
|
private static final int INDEX_AUTOCRYPT_PEER_STATE = 6;
|
|
|
|
|
|
private final ApiPendingIntentFactory apiPendingIntentFactory;
|
|
private final ContentResolver contentResolver;
|
|
|
|
|
|
static OpenPgpServiceKeyIdExtractor getInstance(ContentResolver contentResolver, ApiPendingIntentFactory apiPendingIntentFactory) {
|
|
return new OpenPgpServiceKeyIdExtractor(contentResolver, apiPendingIntentFactory);
|
|
}
|
|
|
|
private OpenPgpServiceKeyIdExtractor(ContentResolver contentResolver,
|
|
ApiPendingIntentFactory apiPendingIntentFactory) {
|
|
this.contentResolver = contentResolver;
|
|
this.apiPendingIntentFactory = apiPendingIntentFactory;
|
|
}
|
|
|
|
|
|
KeyIdResult returnKeyIdsFromIntent(Intent data, boolean askIfNoUserIdsProvided, String callingPackageName) {
|
|
boolean hasKeysFromSelectPubkeyActivity = data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS_SELECTED);
|
|
|
|
KeyIdResult result;
|
|
if (hasKeysFromSelectPubkeyActivity) {
|
|
HashSet<Long> encryptKeyIds = new HashSet<>();
|
|
for (long keyId : data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS_SELECTED)) {
|
|
encryptKeyIds.add(keyId);
|
|
}
|
|
result = createKeysOkResult(encryptKeyIds, false, AutocryptStatus.AUTOCRYPT_PEER_DISABLED);
|
|
} else if (data.hasExtra(OpenPgpApi.EXTRA_USER_IDS) || askIfNoUserIdsProvided) {
|
|
String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS);
|
|
result = returnKeyIdsFromEmails(data, userIds, callingPackageName);
|
|
} else {
|
|
result = createNoKeysResult();
|
|
}
|
|
|
|
// add key ids from non-ambiguous key id extra
|
|
if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) {
|
|
HashSet<Long> explicitKeyIds = new HashSet<>();
|
|
for (long keyId : data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)) {
|
|
explicitKeyIds.add(keyId);
|
|
}
|
|
result = result.withExplicitKeyIds(explicitKeyIds);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
KeyIdResult returnKeyIdsFromEmails(Intent data, String[] encryptionAddresses, String callingPackageName) {
|
|
boolean hasAddresses = (encryptionAddresses != null && encryptionAddresses.length > 0);
|
|
|
|
boolean allKeysConfirmed = false;
|
|
HashSet<Long> keyIds = new HashSet<>();
|
|
ArrayList<String> missingEmails = new ArrayList<>();
|
|
ArrayList<String> duplicateEmails = new ArrayList<>();
|
|
Integer combinedAutocryptState = null;
|
|
|
|
if (hasAddresses) {
|
|
HashMap<String, AddressQueryResult> userIdEntries = getStatusMapForQueriedAddresses(
|
|
encryptionAddresses, callingPackageName);
|
|
|
|
boolean anyKeyNotVerified = false;
|
|
for (String queriedAddress : encryptionAddresses) {
|
|
AddressQueryResult addressQueryResult = userIdEntries.get(queriedAddress);
|
|
if (addressQueryResult == null) {
|
|
throw new IllegalStateException("No result for address - shouldn't happen!");
|
|
}
|
|
|
|
if (addressQueryResult.autocryptMasterKeyId != null) {
|
|
keyIds.add(addressQueryResult.autocryptMasterKeyId);
|
|
|
|
if (addressQueryResult.autocryptKeyStatus != KeychainExternalContract.KEY_STATUS_VERIFIED) {
|
|
anyKeyNotVerified = true;
|
|
}
|
|
|
|
if (combinedAutocryptState == null) {
|
|
combinedAutocryptState = addressQueryResult.autocryptState;
|
|
} else {
|
|
combinedAutocryptState = combineAutocryptState(
|
|
combinedAutocryptState, addressQueryResult.autocryptState);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (addressQueryResult.uidMasterKeyId != null) {
|
|
keyIds.add(addressQueryResult.uidMasterKeyId);
|
|
combinedAutocryptState = AutocryptStatus.AUTOCRYPT_PEER_AVAILABLE_EXTERNAL;
|
|
|
|
if (addressQueryResult.uidHasMultipleCandidates) {
|
|
duplicateEmails.add(queriedAddress);
|
|
}
|
|
|
|
if (addressQueryResult.uidKeyStatus != KeychainExternalContract.KEY_STATUS_VERIFIED) {
|
|
anyKeyNotVerified = true;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
missingEmails.add(queriedAddress);
|
|
}
|
|
|
|
if (userIdEntries.size() != encryptionAddresses.length) {
|
|
Timber.e("Number of rows doesn't match number of retrieved rows! Probably a bug?");
|
|
}
|
|
|
|
allKeysConfirmed = !anyKeyNotVerified;
|
|
}
|
|
|
|
if (!missingEmails.isEmpty()) {
|
|
return createMissingKeysResult(data, keyIds, missingEmails, duplicateEmails);
|
|
}
|
|
|
|
if (!duplicateEmails.isEmpty()) {
|
|
return createDuplicateKeysResult(data, callingPackageName, duplicateEmails);
|
|
}
|
|
|
|
if (keyIds.isEmpty()) {
|
|
return createNoKeysResult(data, keyIds, missingEmails, duplicateEmails);
|
|
}
|
|
|
|
return createKeysOkResult(keyIds, allKeysConfirmed, combinedAutocryptState);
|
|
}
|
|
|
|
private int combineAutocryptState(int first, int second) {
|
|
return first < second ? first : second;
|
|
}
|
|
|
|
/** This method queries the KeychainExternalProvider for all addresses given in encryptionUserIds.
|
|
* It returns a map with one UserIdStatus per queried address. If multiple key candidates exist,
|
|
* the one with the highest verification status is selected. If two candidates with the same
|
|
* verification status exist, the first one is returned and marked as having a duplicate.
|
|
*/
|
|
@NonNull
|
|
private HashMap<String, AddressQueryResult> getStatusMapForQueriedAddresses(String[] encryptionUserIds, String callingPackageName) {
|
|
HashMap<String,AddressQueryResult> keyRows = new HashMap<>();
|
|
Uri queryUri = AutocryptStatus.CONTENT_URI.buildUpon().appendPath(callingPackageName).build();
|
|
Cursor cursor = contentResolver.query(queryUri, PROJECTION_MAIL_STATUS, null, encryptionUserIds, null);
|
|
if (cursor == null) {
|
|
throw new IllegalStateException("Internal error, received null cursor!");
|
|
}
|
|
|
|
try {
|
|
while (cursor.moveToNext()) {
|
|
String queryAddress = cursor.getString(INDEX_EMAIL_ADDRESS);
|
|
Long uidMasterKeyId =
|
|
cursor.isNull(INDEX_MASTER_KEY_ID) ? null : cursor.getLong(INDEX_MASTER_KEY_ID);
|
|
int uidKeyStatus = cursor.getInt(INDEX_USER_ID_STATUS);
|
|
boolean uidHasMultipleCandidates = cursor.getInt(INDEX_USER_ID_CANDIDATES) > 1;
|
|
|
|
Long autocryptMasterKeyId =
|
|
cursor.isNull(INDEX_AUTOCRYPT_MASTER_KEY_ID) ? null : cursor.getLong(INDEX_AUTOCRYPT_MASTER_KEY_ID);
|
|
int autocryptKeyStatus = cursor.getInt(INDEX_AUTOCRYPT_KEY_STATUS);
|
|
int autocryptPeerStatus = cursor.getInt(INDEX_AUTOCRYPT_PEER_STATE);
|
|
|
|
AddressQueryResult status = new AddressQueryResult(
|
|
uidMasterKeyId, uidKeyStatus, uidHasMultipleCandidates, autocryptMasterKeyId,
|
|
autocryptKeyStatus, autocryptPeerStatus);
|
|
|
|
keyRows.put(queryAddress, status);
|
|
}
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
return keyRows;
|
|
}
|
|
|
|
private static class AddressQueryResult {
|
|
private final Long uidMasterKeyId;
|
|
private final int uidKeyStatus;
|
|
private boolean uidHasMultipleCandidates;
|
|
private final Long autocryptMasterKeyId;
|
|
private final int autocryptKeyStatus;
|
|
private final int autocryptState;
|
|
|
|
AddressQueryResult(Long uidMasterKeyId, int uidKeyStatus, boolean uidHasMultipleCandidates, Long autocryptMasterKeyId,
|
|
int autocryptKeyStatus, int autocryptState) {
|
|
this.uidMasterKeyId = uidMasterKeyId;
|
|
this.uidKeyStatus = uidKeyStatus;
|
|
this.uidHasMultipleCandidates = uidHasMultipleCandidates;
|
|
this.autocryptMasterKeyId = autocryptMasterKeyId;
|
|
this.autocryptKeyStatus = autocryptKeyStatus;
|
|
this.autocryptState = autocryptState;
|
|
}
|
|
}
|
|
|
|
static class KeyIdResult {
|
|
private final PendingIntent mKeySelectionPendingIntent;
|
|
private final HashSet<Long> mUserKeyIds;
|
|
private final HashSet<Long> mExplicitKeyIds;
|
|
private final KeyIdResultStatus mStatus;
|
|
private final boolean mAllKeysConfirmed;
|
|
private final int mCombinedAutocryptState;
|
|
|
|
private KeyIdResult(PendingIntent keySelectionPendingIntent, KeyIdResultStatus keyIdResultStatus) {
|
|
mKeySelectionPendingIntent = keySelectionPendingIntent;
|
|
mUserKeyIds = null;
|
|
mAllKeysConfirmed = false;
|
|
mStatus = keyIdResultStatus;
|
|
mExplicitKeyIds = null;
|
|
mCombinedAutocryptState = AutocryptStatus.AUTOCRYPT_PEER_DISABLED;
|
|
}
|
|
|
|
private KeyIdResult(HashSet<Long> keyIds, boolean allKeysConfirmed, KeyIdResultStatus keyIdResultStatus,
|
|
int combinedAutocryptState) {
|
|
mKeySelectionPendingIntent = null;
|
|
mUserKeyIds = keyIds;
|
|
mAllKeysConfirmed = allKeysConfirmed;
|
|
mStatus = keyIdResultStatus;
|
|
mExplicitKeyIds = null;
|
|
mCombinedAutocryptState = combinedAutocryptState;
|
|
}
|
|
|
|
private KeyIdResult(KeyIdResult keyIdResult, HashSet<Long> explicitKeyIds) {
|
|
mKeySelectionPendingIntent = keyIdResult.mKeySelectionPendingIntent;
|
|
mUserKeyIds = keyIdResult.mUserKeyIds;
|
|
mAllKeysConfirmed = keyIdResult.mAllKeysConfirmed;
|
|
mStatus = keyIdResult.mStatus;
|
|
mExplicitKeyIds = explicitKeyIds;
|
|
mCombinedAutocryptState = keyIdResult.mCombinedAutocryptState;
|
|
}
|
|
|
|
boolean hasKeySelectionPendingIntent() {
|
|
return mKeySelectionPendingIntent != null;
|
|
}
|
|
|
|
PendingIntent getKeySelectionPendingIntent() {
|
|
if (mKeySelectionPendingIntent == null) {
|
|
throw new AssertionError("result intent must not be null when getResultIntent is called!");
|
|
}
|
|
if (mUserKeyIds != null) {
|
|
throw new AssertionError("key ids must be null when getKeyIds is called!");
|
|
}
|
|
return mKeySelectionPendingIntent;
|
|
}
|
|
|
|
long[] getKeyIds() {
|
|
if (mKeySelectionPendingIntent != null) {
|
|
throw new AssertionError("result intent must be null when getKeyIds is called!");
|
|
}
|
|
HashSet<Long> allKeyIds = new HashSet<>();
|
|
if (mUserKeyIds != null) {
|
|
allKeyIds.addAll(mUserKeyIds);
|
|
}
|
|
if (mExplicitKeyIds != null) {
|
|
allKeyIds.addAll(mExplicitKeyIds);
|
|
}
|
|
return KeyFormattingUtils.getUnboxedLongArray(allKeyIds);
|
|
}
|
|
|
|
boolean isAllKeysConfirmed() {
|
|
return mAllKeysConfirmed;
|
|
}
|
|
|
|
private KeyIdResult withExplicitKeyIds(HashSet<Long> explicitKeyIds) {
|
|
return new KeyIdResult(this, explicitKeyIds);
|
|
}
|
|
|
|
KeyIdResultStatus getStatus() {
|
|
return mStatus;
|
|
}
|
|
|
|
int getAutocryptRecommendation() {
|
|
return mCombinedAutocryptState;
|
|
}
|
|
}
|
|
|
|
enum KeyIdResultStatus {
|
|
OK, MISSING, DUPLICATE, NO_KEYS, NO_KEYS_ERROR
|
|
}
|
|
|
|
private KeyIdResult createKeysOkResult(HashSet<Long> encryptKeyIds, boolean allKeysConfirmed,
|
|
int combinedAutocryptState) {
|
|
return new KeyIdResult(encryptKeyIds, allKeysConfirmed, KeyIdResultStatus.OK, combinedAutocryptState);
|
|
}
|
|
|
|
private KeyIdResult createNoKeysResult(Intent data,
|
|
HashSet<Long> selectedKeyIds, ArrayList<String> missingEmails, ArrayList<String> duplicateEmails) {
|
|
long[] keyIdsArray = KeyFormattingUtils.getUnboxedLongArray(selectedKeyIds);
|
|
PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createSelectPublicKeyPendingIntent(
|
|
data, keyIdsArray, missingEmails, duplicateEmails, false);
|
|
|
|
return new KeyIdResult(selectKeyPendingIntent, KeyIdResultStatus.NO_KEYS);
|
|
}
|
|
|
|
private KeyIdResult createDuplicateKeysResult(Intent data, String packageName, ArrayList<String> duplicateEmails) {
|
|
PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createDeduplicatePendingIntent(
|
|
packageName, data, duplicateEmails);
|
|
|
|
return new KeyIdResult(selectKeyPendingIntent, KeyIdResultStatus.DUPLICATE);
|
|
}
|
|
|
|
private KeyIdResult createMissingKeysResult(Intent data,
|
|
HashSet<Long> selectedKeyIds, ArrayList<String> missingEmails, ArrayList<String> duplicateEmails) {
|
|
long[] keyIdsArray = KeyFormattingUtils.getUnboxedLongArray(selectedKeyIds);
|
|
PendingIntent selectKeyPendingIntent = apiPendingIntentFactory.createSelectPublicKeyPendingIntent(
|
|
data, keyIdsArray, missingEmails, duplicateEmails, false);
|
|
|
|
return new KeyIdResult(selectKeyPendingIntent, KeyIdResultStatus.MISSING);
|
|
}
|
|
|
|
private KeyIdResult createNoKeysResult() {
|
|
return new KeyIdResult(null, KeyIdResultStatus.NO_KEYS_ERROR);
|
|
}
|
|
}
|