/* * 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 . */ package org.sufficientlysecure.keychain.remote; import java.security.AccessControlException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import androidx.annotation.NonNull; import android.widget.Toast; import org.sufficientlysecure.keychain.BuildConfig; import org.sufficientlysecure.keychain.daos.ApiAppDao; import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager; import org.sufficientlysecure.keychain.daos.UserIdDao; import org.sufficientlysecure.keychain.model.UserPacket.UidStatus; import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus; import org.sufficientlysecure.keychain.provider.KeychainExternalContract; import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus; import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptRecommendationResult; import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptState; import timber.log.Timber; public class KeychainExternalProvider extends ContentProvider { private static final int EMAIL_STATUS = 101; private static final int AUTOCRYPT_STATUS = 201; private static final int AUTOCRYPT_STATUS_INTERNAL = 202; private UriMatcher uriMatcher; private ApiPermissionHelper apiPermissionHelper; protected UriMatcher buildUriMatcher() { final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); String authority = KeychainExternalContract.CONTENT_AUTHORITY_EXTERNAL; matcher.addURI(authority, KeychainExternalContract.BASE_EMAIL_STATUS, EMAIL_STATUS); matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_STATUS, AUTOCRYPT_STATUS); matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_STATUS + "/*", AUTOCRYPT_STATUS_INTERNAL); return matcher; } @Override public boolean onCreate() { uriMatcher = buildUriMatcher(); Context context = getContext(); if (context == null) { throw new NullPointerException("Context can't be null during onCreate!"); } apiPermissionHelper = new ApiPermissionHelper(context, ApiAppDao.getInstance(getContext())); return true; } @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Timber.v("query(uri=" + uri + ", proj=" + Arrays.toString(projection) + ")"); Context context = getContext(); if (context == null) { throw new IllegalStateException(); } String callingPackageName = apiPermissionHelper.getCurrentCallingPackage(); int match = uriMatcher.match(uri); switch (match) { case EMAIL_STATUS: { Toast.makeText(context, "This API is no longer supported by OpenKeychain!", Toast.LENGTH_SHORT).show(); return new MatrixCursor(projection); } case AUTOCRYPT_STATUS_INTERNAL: if (!BuildConfig.APPLICATION_ID.equals(callingPackageName)) { throw new AccessControlException("This URI can only be called internally!"); } // override package name to use any external callingPackageName = uri.getLastPathSegment(); case AUTOCRYPT_STATUS: { boolean callerIsAllowed = (match == AUTOCRYPT_STATUS_INTERNAL) || apiPermissionHelper.isAllowedIgnoreErrors(); if (!callerIsAllowed) { throw new AccessControlException("An application must register before use of KeychainExternalProvider!"); } if (projection == null) { throw new IllegalArgumentException("Please provide a projection!"); } List plist = Arrays.asList(projection); boolean isWildcardSelector = selectionArgs.length == 1 && selectionArgs[0].contains("%"); boolean queriesUidResult = plist.contains(AutocryptStatus.UID_KEY_STATUS) || plist.contains(AutocryptStatus.UID_ADDRESS) || plist.contains(AutocryptStatus.UID_MASTER_KEY_ID) || plist.contains(AutocryptStatus.UID_CANDIDATES); boolean queriesAutocryptResult = plist.contains(AutocryptStatus.AUTOCRYPT_PEER_STATE) || plist.contains(AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID) || plist.contains(AutocryptStatus.AUTOCRYPT_KEY_STATUS); if (isWildcardSelector && queriesAutocryptResult) { throw new UnsupportedOperationException("Cannot wildcard-query autocrypt results!"); } Map uidStatuses = queriesUidResult ? loadUidStatusMap(selectionArgs, isWildcardSelector) : Collections.emptyMap(); Map autocryptStates = queriesAutocryptResult ? loadAutocryptRecommendationMap(selectionArgs, callingPackageName) : Collections.emptyMap(); MatrixCursor cursor = mapResultsToProjectedMatrixCursor(projection, selectionArgs, uidStatuses, autocryptStates); uri = DatabaseNotifyManager.getNotifyUriAllKeys(); cursor.setNotificationUri(context.getContentResolver(), uri); return cursor; } default: { throw new IllegalArgumentException("Unknown URI " + uri + " (" + match + ")"); } } } @NonNull private MatrixCursor mapResultsToProjectedMatrixCursor(String[] projection, String[] selectionArgs, Map uidStatuses, Map autocryptStates) { MatrixCursor cursor = new MatrixCursor(projection); for (String selectionArg : selectionArgs) { AutocryptRecommendationResult autocryptResult = autocryptStates.get(selectionArg); UidStatus uidStatus = uidStatuses.get(selectionArg); Object[] row = new Object[projection.length]; for (int i = 0; i < projection.length; i++) { if (AutocryptStatus.ADDRESS.equals(projection[i]) || AutocryptStatus._ID.equals(projection[i])) { row[i] = selectionArg; } else { row[i] = columnNameToRowContent(projection[i], autocryptResult, uidStatus); } } cursor.addRow(row); } return cursor; } private Object columnNameToRowContent( String columnName, AutocryptRecommendationResult autocryptResult, UidStatus uidStatus) { switch (columnName) { case AutocryptStatus.UID_KEY_STATUS: { if (uidStatus == null) { return null; } return uidStatus.keyStatus() == VerificationStatus.VERIFIED_SECRET ? KeychainExternalContract.KEY_STATUS_VERIFIED : KeychainExternalContract.KEY_STATUS_UNVERIFIED; } case AutocryptStatus.UID_ADDRESS: if (uidStatus == null) { return null; } return uidStatus.user_id(); case AutocryptStatus.UID_MASTER_KEY_ID: if (uidStatus == null) { return null; } return uidStatus.master_key_id(); case AutocryptStatus.UID_CANDIDATES: if (uidStatus == null) { return null; } return uidStatus.candidates(); case AutocryptStatus.AUTOCRYPT_PEER_STATE: if (autocryptResult == null) { return null; } return getPeerStateValue(autocryptResult.autocryptState); case AutocryptStatus.AUTOCRYPT_KEY_STATUS: if (autocryptResult == null) { return null; } return autocryptResult.isVerified ? KeychainExternalContract.KEY_STATUS_VERIFIED : KeychainExternalContract.KEY_STATUS_UNVERIFIED; case AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID: if (autocryptResult == null) { return null; } return autocryptResult.masterKeyId; default: throw new IllegalArgumentException("Unhandled case " + columnName); } } private Map loadUidStatusMap(String[] selectionArgs, boolean isWildcardSelector) { UserIdDao userIdDao = UserIdDao.getInstance(getContext()); if (isWildcardSelector) { UidStatus uidStatus = userIdDao.getUidStatusByEmailLike(selectionArgs[0]); return Collections.singletonMap(selectionArgs[0], uidStatus); } else { return userIdDao.getUidStatusByEmail(selectionArgs); } } private Map loadAutocryptRecommendationMap( String[] selectionArgs, String callingPackageName) { AutocryptInteractor autocryptInteractor = AutocryptInteractor.getInstance(getContext(), callingPackageName); return autocryptInteractor.determineAutocryptRecommendations(selectionArgs); } private int getPeerStateValue(AutocryptState autocryptState) { switch (autocryptState) { case DISABLE: return AutocryptStatus.AUTOCRYPT_PEER_DISABLED; case DISCOURAGED_OLD: return AutocryptStatus.AUTOCRYPT_PEER_DISCOURAGED_OLD; case DISCOURAGED_GOSSIP: return AutocryptStatus.AUTOCRYPT_PEER_GOSSIP; case AVAILABLE: return AutocryptStatus.AUTOCRYPT_PEER_AVAILABLE; case MUTUAL: return AutocryptStatus.AUTOCRYPT_PEER_MUTUAL; } throw new IllegalStateException("Unhandled case!"); } @Override public String getType(@NonNull Uri uri) { throw new UnsupportedOperationException("Unknown uri: " + uri); } @Override public Uri insert(@NonNull Uri uri, ContentValues values) { throw new UnsupportedOperationException(); } @Override public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } @Override public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } }