diff options
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure')
218 files changed, 17636 insertions, 9106 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index fc1cb8acc..6a9656b28 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -19,26 +19,31 @@ package org.sufficientlysecure.keychain; import android.os.Environment; -import org.spongycastle.bcpg.HashAlgorithmTags; -import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags; import org.spongycastle.jce.provider.BouncyCastleProvider; import java.io.File; +import java.net.Proxy; public final class Constants { public static final boolean DEBUG = BuildConfig.DEBUG; public static final boolean DEBUG_LOG_DB_QUERIES = false; public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false; + public static final boolean DEBUG_KEYSERVER_SYNC = false; - public static final String TAG = "Keychain"; + public static final String TAG = DEBUG ? "Keychain D" : "Keychain"; public static final String PACKAGE_NAME = "org.sufficientlysecure.keychain"; - public static final String ACCOUNT_NAME = "OpenKeychain"; - public static final String ACCOUNT_TYPE = PACKAGE_NAME + ".account"; + public static final String ACCOUNT_NAME = DEBUG ? "OpenKeychain D" : "OpenKeychain"; + public static final String ACCOUNT_TYPE = BuildConfig.ACCOUNT_TYPE; public static final String CUSTOM_CONTACT_DATA_MIME_TYPE = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.key"; + public static final String PROVIDER_AUTHORITY = BuildConfig.PROVIDER_CONTENT_AUTHORITY; + public static final String TEMPSTORAGE_AUTHORITY = BuildConfig.APPLICATION_ID + ".tempstorage"; + + public static final String CLIPBOARD_LABEL = "Keychain"; + // as defined in http://tools.ietf.org/html/rfc3156, section 7 public static final String NFC_MIME = "application/pgp-keys"; @@ -72,6 +77,11 @@ public final class Constants { public static final File APP_DIR_FILE = new File(APP_DIR, "export.asc"); } + public static final class Notification { + public static final int PASSPHRASE_CACHE = 1; + public static final int KEYSERVER_SYNC_FAIL_ORBOT = 2; + } + public static final class Pref { public static final String PASSPHRASE_CACHE_TTL = "passphraseCacheTtl"; public static final String PASSPHRASE_CACHE_SUBS = "passphraseCacheSubs"; @@ -84,11 +94,49 @@ public final class Constants { public static final String SEARCH_KEYBASE = "search_keybase_pref"; public static final String USE_DEFAULT_YUBIKEY_PIN = "useDefaultYubikeyPin"; public static final String USE_NUMKEYPAD_FOR_YUBIKEY_PIN = "useNumKeypadForYubikeyPin"; + public static final String ENCRYPT_FILENAMES = "encryptFilenames"; + public static final String FILE_USE_COMPRESSION = "useFileCompression"; + public static final String TEXT_USE_COMPRESSION = "useTextCompression"; + public static final String USE_ARMOR = "useArmor"; + // proxy settings + public static final String USE_NORMAL_PROXY = "useNormalProxy"; + public static final String USE_TOR_PROXY = "useTorProxy"; + public static final String PROXY_HOST = "proxyHost"; + public static final String PROXY_PORT = "proxyPort"; + public static final String PROXY_TYPE = "proxyType"; + public static final String THEME = "theme"; + // keyserver sync settings + public static final String SYNC_CONTACTS = "syncContacts"; + public static final String SYNC_KEYSERVER = "syncKeyserver"; + // other settings + public static final String EXPERIMENTAL_ENABLE_WORD_CONFIRM = "experimentalEnableWordConfirm"; + public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities"; + public static final String EXPERIMENTAL_ENABLE_KEYBASE = "experimentalEnableKeybase"; + + public static final class Theme { + public static final String LIGHT = "light"; + public static final String DARK = "dark"; + public static final String DEFAULT = Constants.Pref.Theme.LIGHT; + } + + public static final class ProxyType { + public static final String TYPE_HTTP = "proxyHttp"; + public static final String TYPE_SOCKS = "proxySocks"; + } + } + + /** + * information to connect to Orbot's localhost HTTP proxy + */ + public static final class Orbot { + public static final String PROXY_HOST = "127.0.0.1"; + public static final int PROXY_PORT = 8118; + public static final Proxy.Type PROXY_TYPE = Proxy.Type.HTTP; } public static final class Defaults { public static final String KEY_SERVERS = "hkps://hkps.pool.sks-keyservers.net, hkps://pgp.mit.edu"; - public static final int PREF_VERSION = 4; + public static final int PREF_VERSION = 6; } public static final class key { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java index 710dbf8aa..311ef2d3b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java @@ -23,7 +23,6 @@ import android.app.Application; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; @@ -33,8 +32,11 @@ import android.provider.ContactsContract; import android.widget.Toast; import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.PRNGFixes; import org.sufficientlysecure.keychain.util.Preferences; @@ -89,18 +91,21 @@ public class KeychainApplication extends Application { } brandGlowEffect(getApplicationContext(), - getApplicationContext().getResources().getColor(R.color.primary)); + FormattingUtils.getColorFromAttr(getApplicationContext(), R.attr.colorPrimary)); setupAccountAsNeeded(this); // Update keyserver list as needed - Preferences.getPreferences(this).updatePreferences(); + Preferences.getPreferences(this).upgradePreferences(this); TlsHelper.addStaticCA("pool.sks-keyservers.net", getAssets(), "sks-keyservers.netCA.cer"); TemporaryStorageProvider.cleanUp(this); - checkConsolidateRecovery(); + if (!checkConsolidateRecovery()) { + // force DB upgrade, https://github.com/open-keychain/open-keychain/issues/1334 + new KeychainDatabase(this).getReadableDatabase().close(); + } } public static HashMap<String,Bitmap> qrCodeCache = new HashMap<>(); @@ -117,29 +122,33 @@ public class KeychainApplication extends Application { /** * Restart consolidate process if it has been interruped before */ - public void checkConsolidateRecovery() { + public boolean checkConsolidateRecovery() { if (Preferences.getPreferences(this).getCachedConsolidate()) { Intent consolidateIntent = new Intent(this, ConsolidateDialogActivity.class); consolidateIntent.putExtra(ConsolidateDialogActivity.EXTRA_CONSOLIDATE_RECOVERY, true); consolidateIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(consolidateIntent); + return true; + } else { + return false; } } /** - * Add OpenKeychain account to Android to link contacts with keys - * - * @param context + * Add OpenKeychain account to Android to link contacts with keys and keyserver sync */ public static void setupAccountAsNeeded(Context context) { try { AccountManager manager = AccountManager.get(context); Account[] accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE); - if (accounts == null || accounts.length == 0) { + + if (accounts.length == 0) { Account account = new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE); if (manager.addAccountExplicitly(account, null, null)) { + // for contact sync ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true); + KeyserverSyncAdapterService.enableKeyserverSync(context); } else { Log.e(Constants.TAG, "Adding account failed!"); } @@ -165,7 +174,7 @@ public class KeychainApplication extends Application { int edgeDrawableId = context.getResources().getIdentifier("overscroll_edge", "drawable", "android"); Drawable androidEdge = context.getResources().getDrawable(edgeDrawableId); androidEdge.setColorFilter(brandColor, PorterDuff.Mode.SRC_IN); - } catch (Resources.NotFoundException e) { + } catch (Exception ignored) { } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/AppCompatPreferenceActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/AppCompatPreferenceActivity.java new file mode 100644 index 000000000..5200b8ced --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/AppCompatPreferenceActivity.java @@ -0,0 +1,127 @@ + +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.compatibility; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.widget.Toolbar; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls + * to be used with AppCompat. + * <p/> + * This technique can be used with an {@link android.app.Activity} class, not just + * {@link android.preference.PreferenceActivity}. + */ +public abstract class AppCompatPreferenceActivity extends PreferenceActivity { + private AppCompatDelegate mDelegate; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); + super.onCreate(savedInstanceState); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + getDelegate().onPostCreate(savedInstanceState); + } + + public ActionBar getSupportActionBar() { + return getDelegate().getSupportActionBar(); + } + + public void setSupportActionBar(@Nullable Toolbar toolbar) { + getDelegate().setSupportActionBar(toolbar); + } + + @Override + public MenuInflater getMenuInflater() { + return getDelegate().getMenuInflater(); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) { + getDelegate().setContentView(layoutResID); + } + + @Override + public void setContentView(View view) { + getDelegate().setContentView(view); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().setContentView(view, params); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().addContentView(view, params); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + protected void onTitleChanged(CharSequence title, int color) { + super.onTitleChanged(title, color); + getDelegate().setTitle(title); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + getDelegate().onConfigurationChanged(newConfig); + } + + @Override + protected void onStop() { + super.onStop(); + getDelegate().onStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + getDelegate().onDestroy(); + } + + public void invalidateOptionsMenu() { + getDelegate().invalidateOptionsMenu(); + } + + private AppCompatDelegate getDelegate() { + if (mDelegate == null) { + mDelegate = AppCompatDelegate.create(this, null); + } + return mDelegate; + } +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java index fa3600ffb..abf16851d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java @@ -17,79 +17,35 @@ package org.sufficientlysecure.keychain.compatibility; + +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; +import android.support.annotation.Nullable; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.util.Log; -import java.lang.reflect.Method; - public class ClipboardReflection { - private static final String clipboardLabel = "Keychain"; - - /** - * Wrapper around ClipboardManager based on Android version using Reflection API - * - * @param context - * @param text - */ - public static void copyToClipboard(Context context, String text) { - Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE); - try { - if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) { - Method methodSetText = clipboard.getClass() - .getMethod("setText", CharSequence.class); - methodSetText.invoke(clipboard, text); - } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) { - Class<?> classClipData = Class.forName("android.content.ClipData"); - Method methodNewPlainText = classClipData.getMethod("newPlainText", - CharSequence.class, CharSequence.class); - Object clip = methodNewPlainText.invoke(null, clipboardLabel, text); - methodNewPlainText = clipboard.getClass() - .getMethod("setPrimaryClip", classClipData); - methodNewPlainText.invoke(clipboard, clip); - } - } catch (Exception e) { - Log.e(Constants.TAG, "There was an error copying the text to the clipboard", e); + @Nullable + public static String getClipboardText(@Nullable Context context) { + if (context == null) { + return null; } - } - - /** - * Wrapper around ClipboardManager based on Android version using Reflection API - * - * @param context - */ - public static CharSequence getClipboardText(Context context) { - Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE); - try { - if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) { - // CharSequence text = clipboard.getText(); - Method methodGetText = clipboard.getClass().getMethod("getText"); - Object text = methodGetText.invoke(clipboard); + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - return (CharSequence) text; - } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) { - // ClipData clipData = clipboard.getPrimaryClip(); - Method methodGetPrimaryClip = clipboard.getClass().getMethod("getPrimaryClip"); - Object clipData = methodGetPrimaryClip.invoke(clipboard); - - // ClipData.Item clipDataItem = clipData.getItemAt(0); - Method methodGetItemAt = clipData.getClass().getMethod("getItemAt", int.class); - Object clipDataItem = methodGetItemAt.invoke(clipData, 0); - - // CharSequence text = clipDataItem.coerceToText(context); - Method methodGetString = clipDataItem.getClass().getMethod("coerceToText", - Context.class); - Object text = methodGetString.invoke(clipDataItem, context); - - return (CharSequence) text; - } else { - return null; - } - } catch (Exception e) { - Log.e(Constants.TAG, "There was an error getting the text from the clipboard", e); + ClipData clip = clipboard.getPrimaryClip(); + if (clip == null || clip.getItemCount() == 0) { + Log.e(Constants.TAG, "No clipboard data!"); return null; } + + ClipData.Item item = clip.getItemAt(0); + CharSequence seq = item.coerceToText(context); + if (seq != null) { + return seq.toString(); + } + return null; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java index 07799f466..e1d8c0da7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java @@ -21,7 +21,7 @@ import android.os.Build; import android.os.Handler; /** - * Bug on Android >= 4.2 + * Bug on Android >= 4.2. Fixed in 4.2.2 ? * * http://code.google.com/p/android/issues/detail?id=41901 * diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java index c0221fad3..d91dd28bc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java @@ -20,6 +20,7 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; +import java.net.Proxy; import java.util.ArrayList; import java.util.Vector; @@ -30,7 +31,8 @@ public class CloudSearch { private final static long SECONDS = 1000; - public static ArrayList<ImportKeysListEntry> search(final String query, Preferences.CloudSearchPrefs cloudPrefs) + public static ArrayList<ImportKeysListEntry> search(final String query, Preferences.CloudSearchPrefs cloudPrefs, + final Proxy proxy) throws Keyserver.CloudSearchFailureException { final ArrayList<Keyserver> servers = new ArrayList<>(); @@ -45,31 +47,42 @@ public class CloudSearch { } final ImportKeysList results = new ImportKeysList(servers.size()); + ArrayList<Thread> searchThreads = new ArrayList<>(); for (final Keyserver keyserver : servers) { Runnable r = new Runnable() { @Override public void run() { try { - results.addAll(keyserver.search(query)); + results.addAll(keyserver.search(query, proxy)); } catch (Keyserver.CloudSearchFailureException e) { problems.add(e); } results.finishedAdding(); // notifies if all searchers done } }; - new Thread(r).start(); + Thread searchThread = new Thread(r); + searchThreads.add(searchThread); + searchThread.start(); } - // wait for either all the searches to come back, or 10 seconds - synchronized(results) { + // wait for either all the searches to come back, or 10 seconds. If using proxy, wait 30 seconds. + synchronized (results) { try { - results.wait(10 * SECONDS); + if (proxy != null) { + results.wait(30 * SECONDS); + } else { + results.wait(10 * SECONDS); + } + for (Thread thread : searchThreads) { + // kill threads that haven't returned yet + thread.interrupt(); + } } catch (InterruptedException e) { } } if (results.outstandingSuppliers() > 0) { - String message = "Launched " + servers.size() + " cloud searchers, but" + + String message = "Launched " + servers.size() + " cloud searchers, but " + results.outstandingSuppliers() + "failed to complete."; problems.add(new Keyserver.QueryFailedException(message)); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java index cb8a53e25..558b8ce7d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java @@ -18,18 +18,20 @@ package org.sufficientlysecure.keychain.keyimport; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.PgpHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.TlsHelper; -import java.io.BufferedWriter; import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; +import java.net.Proxy; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; @@ -39,6 +41,7 @@ import java.util.Comparator; import java.util.GregorianCalendar; import java.util.Locale; import java.util.TimeZone; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -190,36 +193,52 @@ public class HkpKeyserver extends Keyserver { return mSecure ? "https://" : "http://"; } - private HttpURLConnection openConnection(URL url) throws IOException { - HttpURLConnection conn = null; + /** + * returns a client with pinned certificate if necessary + * + * @param url url to be queried by client + * @param proxy proxy to be used by client + * @return client with a pinned certificate if necesary + */ + public static OkHttpClient getClient(URL url, Proxy proxy) throws IOException { + OkHttpClient client = new OkHttpClient(); + try { - conn = (HttpURLConnection) TlsHelper.openConnection(url); + TlsHelper.pinCertificateIfNecessary(client, url); } catch (TlsHelper.TlsHelperException e) { Log.w(Constants.TAG, e); } - if (conn == null) { - conn = (HttpURLConnection) url.openConnection(); + + if (proxy != null) { + client.setProxy(proxy); + client.setConnectTimeout(30000, TimeUnit.MILLISECONDS); + } else { + client.setProxy(Proxy.NO_PROXY); + client.setConnectTimeout(5000, TimeUnit.MILLISECONDS); } - conn.setConnectTimeout(5000); - conn.setReadTimeout(25000); - return conn; + client.setReadTimeout(45000, TimeUnit.MILLISECONDS); + + return client; } - private String query(String request) throws QueryFailedException, HttpError { + private String query(String request, Proxy proxy) throws QueryFailedException, HttpError { try { URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + request); - Log.d(Constants.TAG, "hkp keyserver query: " + url); - HttpURLConnection conn = openConnection(url); - conn.connect(); - int response = conn.getResponseCode(); - if (response >= 200 && response < 300) { - return readAll(conn.getInputStream(), conn.getContentEncoding()); + Log.d(Constants.TAG, "hkp keyserver query: " + url + " Proxy: " + proxy); + OkHttpClient client = getClient(url, proxy); + Response response = client.newCall(new Request.Builder().url(url).build()).execute(); + + String responseBody = response.body().string();// contains body both in case of success or failure + + if (response.isSuccessful()) { + return responseBody; } else { - String data = readAll(conn.getErrorStream(), conn.getContentEncoding()); - throw new HttpError(response, data); + throw new HttpError(response.code(), responseBody); } } catch (IOException e) { - throw new QueryFailedException("Keyserver '" + mHost + "' is unavailable. Check your Internet connection!"); + Log.e(Constants.TAG, "IOException at HkpKeyserver", e); + throw new QueryFailedException("Keyserver '" + mHost + "' is unavailable. Check your Internet connection!" + + proxy == null?"":" Using proxy " + proxy); } } @@ -232,7 +251,7 @@ public class HkpKeyserver extends Keyserver { * @throws QueryNeedsRepairException */ @Override - public ArrayList<ImportKeysListEntry> search(String query) throws QueryFailedException, + public ArrayList<ImportKeysListEntry> search(String query, Proxy proxy) throws QueryFailedException, QueryNeedsRepairException { ArrayList<ImportKeysListEntry> results = new ArrayList<>(); @@ -250,7 +269,7 @@ public class HkpKeyserver extends Keyserver { String data; try { - data = query(request); + data = query(request, proxy); } catch (HttpError e) { if (e.getData() != null) { Log.d(Constants.TAG, "returned error data: " + e.getData().toLowerCase(Locale.ENGLISH)); @@ -334,13 +353,14 @@ public class HkpKeyserver extends Keyserver { } @Override - public String get(String keyIdHex) throws QueryFailedException { + public String get(String keyIdHex, Proxy proxy) throws QueryFailedException { String request = "/pks/lookup?op=get&options=mr&search=" + keyIdHex; - Log.d(Constants.TAG, "hkp keyserver get: " + request); + Log.d(Constants.TAG, "hkp keyserver get: " + request + " using Proxy: " + proxy); String data; try { - data = query(request); + data = query(request, proxy); } catch (HttpError httpError) { + Log.d(Constants.TAG, "Failed to get key at HkpKeyserver", httpError); throw new QueryFailedException("not found"); } Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(data); @@ -351,38 +371,38 @@ public class HkpKeyserver extends Keyserver { } @Override - public void add(String armoredKey) throws AddKeyException { + public void add(String armoredKey, Proxy proxy) throws AddKeyException { try { - String request = "/pks/add"; + String path = "/pks/add"; String params; try { params = "keytext=" + URLEncoder.encode(armoredKey, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new AddKeyException(); } - URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + request); + URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + path); Log.d(Constants.TAG, "hkp keyserver add: " + url.toString()); Log.d(Constants.TAG, "params: " + params); - HttpURLConnection conn = openConnection(url); - conn.setRequestMethod("POST"); - conn.addRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - conn.setRequestProperty("Content-Length", Integer.toString(params.getBytes().length)); - conn.setDoInput(true); - conn.setDoOutput(true); + RequestBody body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), params); + + Request request = new Request.Builder() + .url(url) + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .addHeader("Content-Length", Integer.toString(params.getBytes().length)) + .post(body) + .build(); - OutputStream os = conn.getOutputStream(); - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); - writer.write(params); - writer.flush(); - writer.close(); - os.close(); + Response response = getClient(url, proxy).newCall(request).execute(); - conn.connect(); + Log.d(Constants.TAG, "response code: " + response.code()); + Log.d(Constants.TAG, "answer: " + response.body().string()); + + if (response.code() != 200) { + throw new AddKeyException(); + } - Log.d(Constants.TAG, "response code: " + conn.getResponseCode()); - Log.d(Constants.TAG, "answer: " + readAll(conn.getInputStream(), conn.getContentEncoding())); } catch (IOException e) { Log.e(Constants.TAG, "IOException", e); throw new AddKeyException(); @@ -398,6 +418,7 @@ public class HkpKeyserver extends Keyserver { * Tries to find a server responsible for a given domain * * @return A responsible Keyserver or null if not found. + * TODO: PHILIP Add proxy functionality */ public static HkpKeyserver resolve(String domain) { try { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java index e310e9a3f..c2865410e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java @@ -26,6 +26,7 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; +import java.net.Proxy; import java.util.ArrayList; import java.util.List; @@ -34,7 +35,7 @@ public class KeybaseKeyserver extends Keyserver { private String mQuery; @Override - public ArrayList<ImportKeysListEntry> search(String query) throws QueryFailedException, + public ArrayList<ImportKeysListEntry> search(String query, Proxy proxy) throws QueryFailedException, QueryNeedsRepairException { ArrayList<ImportKeysListEntry> results = new ArrayList<>(); @@ -48,7 +49,7 @@ public class KeybaseKeyserver extends Keyserver { mQuery = query; try { - Iterable<Match> matches = Search.search(query); + Iterable<Match> matches = Search.search(query, proxy); for (Match match : matches) { results.add(makeEntry(match)); } @@ -98,16 +99,16 @@ public class KeybaseKeyserver extends Keyserver { } @Override - public String get(String id) throws QueryFailedException { + public String get(String id, Proxy proxy) throws QueryFailedException { try { - return User.keyForUsername(id); + return User.keyForUsername(id, proxy); } catch (KeybaseException e) { throw new QueryFailedException(e.getMessage()); } } @Override - public void add(String armoredKey) throws AddKeyException { + public void add(String armoredKey, Proxy proxy) throws AddKeyException { throw new AddKeyException(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java index 5e4bd0b70..640b39f44 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java @@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.keyimport; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.Proxy; import java.util.List; public abstract class Keyserver { @@ -31,6 +32,7 @@ public abstract class Keyserver { public CloudSearchFailureException(String message) { super(message); } + public CloudSearchFailureException() { super(); } @@ -67,12 +69,12 @@ public abstract class Keyserver { private static final long serialVersionUID = -507574859137295530L; } - public abstract List<ImportKeysListEntry> search(String query) throws QueryFailedException, + public abstract List<ImportKeysListEntry> search(String query, Proxy proxy) throws QueryFailedException, QueryNeedsRepairException; - public abstract String get(String keyIdHex) throws QueryFailedException; + public abstract String get(String keyIdHex, Proxy proxy) throws QueryFailedException; - public abstract void add(String armoredKey) throws AddKeyException; + public abstract void add(String armoredKey, Proxy proxy) throws AddKeyException; public static String readAll(InputStream in, String encoding) throws IOException { ByteArrayOutputStream raw = new ByteArrayOutputStream(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java index a824e73d7..e4026eaaf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java @@ -18,17 +18,22 @@ package org.sufficientlysecure.keychain.operations; import android.content.Context; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import org.sufficientlysecure.keychain.Constants.key; +import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.PassphraseCacheInterface; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.util.Passphrase; import java.util.concurrent.atomic.AtomicBoolean; -public abstract class BaseOperation implements PassphraseCacheInterface { +public abstract class BaseOperation <T extends Parcelable> implements PassphraseCacheInterface { final public Context mContext; final public Progressable mProgressable; @@ -40,7 +45,7 @@ public abstract class BaseOperation implements PassphraseCacheInterface { * of common methods for progress, cancellation and passphrase cache handling. * * An "operation" in this sense is a high level operation which is called - * by the KeychainIntentService or OpenPgpService services. Concrete + * by the KeychainService or OpenPgpService services. Concrete * subclasses of this class should implement either a single or a group of * related operations. An operation must rely solely on its input * parameters for operation specifics. It should also write a log of its @@ -49,7 +54,7 @@ public abstract class BaseOperation implements PassphraseCacheInterface { * * An operation must *not* throw exceptions of any kind, errors should be * handled as part of the OperationResult! Consequently, all handling of - * errors in KeychainIntentService and OpenPgpService should consist of + * errors in KeychainService and OpenPgpService should consist of * informational rather than operational means. * * Note that subclasses of this class should be either Android- or @@ -73,6 +78,9 @@ public abstract class BaseOperation implements PassphraseCacheInterface { mCancelled = cancelled; } + @NonNull + public abstract OperationResult execute(T input, CryptoInputParcel cryptoInput); + public void updateProgress(int message, int current, int total) { if (mProgressable != null) { mProgressable.setProgress(message, current, total); @@ -104,8 +112,11 @@ public abstract class BaseOperation implements PassphraseCacheInterface { @Override public Passphrase getCachedPassphrase(long subKeyId) throws NoSecretKeyException { try { - long masterKeyId = mProviderHelper.getMasterKeyId(subKeyId); - return getCachedPassphrase(masterKeyId, subKeyId); + if (subKeyId != key.symmetric) { + long masterKeyId = mProviderHelper.getMasterKeyId(subKeyId); + return getCachedPassphrase(masterKeyId, subKeyId); + } + return getCachedPassphrase(key.symmetric, key.symmetric); } catch (NotFoundException e) { throw new PassphraseCacheInterface.NoSecretKeyException(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java index 051517abd..eeed24db0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java @@ -18,17 +18,18 @@ package org.sufficientlysecure.keychain.operations; import android.content.Context; +import android.support.annotation.NonNull; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; -import org.sufficientlysecure.keychain.keyimport.Keyserver.AddKeyException; import org.sufficientlysecure.keychain.operations.results.CertifyResult; +import org.sufficientlysecure.keychain.operations.results.ExportResult; 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.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; +import org.sufficientlysecure.keychain.pgp.PassphraseCacheInterface; import org.sufficientlysecure.keychain.pgp.PgpCertifyOperation; import org.sufficientlysecure.keychain.pgp.PgpCertifyOperation.PgpCertifyResult; import org.sufficientlysecure.keychain.pgp.Progressable; @@ -43,28 +44,33 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; +import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; +import java.net.Proxy; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; -/** An operation which implements a high level user id certification operation. - * +/** + * An operation which implements a high level user id certification operation. + * <p/> * This operation takes a specific CertifyActionsParcel as its input. These * contain a masterKeyId to be used for certification, and a list of * masterKeyIds and related user ids to certify. * * @see CertifyActionsParcel - * */ -public class CertifyOperation extends BaseOperation { +public class CertifyOperation extends BaseOperation<CertifyActionsParcel> { - public CertifyOperation(Context context, ProviderHelper providerHelper, Progressable progressable, AtomicBoolean cancelled) { + public CertifyOperation(Context context, ProviderHelper providerHelper, Progressable progressable, AtomicBoolean + cancelled) { super(context, providerHelper, progressable, cancelled); } - public CertifyResult certify(CertifyActionsParcel parcel, CryptoInputParcel cryptoInput, String keyServerUri) { + @NonNull + @Override + public CertifyResult execute(CertifyActionsParcel parcel, CryptoInputParcel cryptoInput) { OperationLog log = new OperationLog(); log.add(LogType.MSG_CRT, 0); @@ -79,14 +85,46 @@ public class CertifyOperation extends BaseOperation { log.add(LogType.MSG_CRT_UNLOCK, 1); certificationKey = secretKeyRing.getSecretKey(); - if (!cryptoInput.hasPassphrase()) { - return new CertifyResult(log, RequiredInputParcel.createRequiredSignPassphrase( - certificationKey.getKeyId(), certificationKey.getKeyId(), null)); + Passphrase passphrase; + + switch (certificationKey.getSecretKeyType()) { + case PIN: + case PATTERN: + case PASSPHRASE: + passphrase = cryptoInput.getPassphrase(); + if (passphrase == null) { + try { + passphrase = getCachedPassphrase(certificationKey.getKeyId(), certificationKey.getKeyId()); + } catch (PassphraseCacheInterface.NoSecretKeyException ignored) { + // treat as a cache miss for error handling purposes + } + } + + if (passphrase == null) { + return new CertifyResult(log, + RequiredInputParcel.createRequiredSignPassphrase( + certificationKey.getKeyId(), + certificationKey.getKeyId(), + null), + cryptoInput + ); + } + break; + + case PASSPHRASE_EMPTY: + passphrase = new Passphrase(""); + break; + + case DIVERT_TO_CARD: + // the unlock operation will succeed for passphrase == null in a divertToCard key + passphrase = null; + break; + + default: + log.add(LogType.MSG_CRT_ERROR_UNLOCK, 2); + return new CertifyResult(CertifyResult.RESULT_ERROR, log); } - // certification is always with the master key id, so use that one - Passphrase passphrase = cryptoInput.getPassphrase(); - if (!certificationKey.unlock(passphrase)) { log.add(LogType.MSG_CRT_ERROR_UNLOCK, 2); return new CertifyResult(CertifyResult.RESULT_ERROR, log); @@ -152,9 +190,9 @@ public class CertifyOperation extends BaseOperation { } - if ( ! allRequiredInput.isEmpty()) { + if (!allRequiredInput.isEmpty()) { log.add(LogType.MSG_CRT_NFC_RETURN, 1); - return new CertifyResult(log, allRequiredInput.build()); + return new CertifyResult(log, allRequiredInput.build(), cryptoInput); } log.add(LogType.MSG_CRT_SAVING, 1); @@ -165,11 +203,24 @@ public class CertifyOperation extends BaseOperation { return new CertifyResult(CertifyResult.RESULT_CANCELLED, log); } + // these variables are used inside the following loop, but they need to be created only once HkpKeyserver keyServer = null; - ImportExportOperation importExportOperation = null; - if (keyServerUri != null) { - keyServer = new HkpKeyserver(keyServerUri); - importExportOperation = new ImportExportOperation(mContext, mProviderHelper, mProgressable); + ExportOperation exportOperation = null; + Proxy proxy = null; + if (parcel.keyServerUri != null) { + keyServer = new HkpKeyserver(parcel.keyServerUri); + exportOperation = new ExportOperation(mContext, mProviderHelper, mProgressable); + if (cryptoInput.getParcelableProxy() == null) { + // explicit proxy not set + if (!OrbotHelper.isOrbotInRequiredState(mContext)) { + return new CertifyResult(null, + RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); + } + proxy = Preferences.getPreferences(mContext).getProxyPrefs() + .parcelableProxy.getProxy(); + } else { + proxy = cryptoInput.getParcelableProxy().getProxy(); + } } // Write all certified keys into the database @@ -178,7 +229,8 @@ public class CertifyOperation extends BaseOperation { // Check if we were cancelled if (checkCancelled()) { log.add(LogType.MSG_OPERATION_CANCELLED, 0); - return new CertifyResult(CertifyResult.RESULT_CANCELLED, log, certifyOk, certifyError, uploadOk, uploadError); + return new CertifyResult(CertifyResult.RESULT_CANCELLED, log, certifyOk, certifyError, uploadOk, + uploadError); } log.add(LogType.MSG_CRT_SAVE, 2, @@ -187,13 +239,16 @@ public class CertifyOperation extends BaseOperation { mProviderHelper.clearLog(); SaveKeyringResult result = mProviderHelper.savePublicKeyRing(certifiedKey); - if (importExportOperation != null) { - // TODO use subresult, get rid of try/catch! - try { - importExportOperation.uploadKeyRingToServer(keyServer, certifiedKey); + if (exportOperation != null) { + ExportResult uploadResult = exportOperation.uploadKeyRingToServer( + keyServer, + certifiedKey, + proxy); + log.add(uploadResult, 2); + + if (uploadResult.success()) { uploadOk += 1; - } catch (AddKeyException e) { - Log.e(Constants.TAG, "error uploading key", e); + } else { uploadError += 1; } } @@ -205,19 +260,24 @@ public class CertifyOperation extends BaseOperation { } log.add(result, 2); - } if (certifyOk == 0) { log.add(LogType.MSG_CRT_ERROR_NOTHING, 0); - return new CertifyResult(CertifyResult.RESULT_ERROR, log, certifyOk, certifyError, uploadOk, uploadError); + return new CertifyResult(CertifyResult.RESULT_ERROR, log, certifyOk, certifyError, + uploadOk, uploadError); } - log.add(LogType.MSG_CRT_SUCCESS, 0); - //since only verified keys are synced to contacts, we need to initiate a sync now + // since only verified keys are synced to contacts, we need to initiate a sync now ContactSyncAdapterService.requestSync(); - - return new CertifyResult(CertifyResult.RESULT_OK, log, certifyOk, certifyError, uploadOk, uploadError); + + log.add(LogType.MSG_CRT_SUCCESS, 0); + if (uploadError != 0) { + return new CertifyResult(CertifyResult.RESULT_WARNINGS, log, certifyOk, certifyError, uploadOk, + uploadError); + } else { + return new CertifyResult(CertifyResult.RESULT_OK, log, certifyOk, certifyError, uploadOk, uploadError); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ConsolidateOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ConsolidateOperation.java new file mode 100644 index 000000000..782cd6800 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ConsolidateOperation.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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 android.content.Context; +import android.support.annotation.NonNull; + +import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.ConsolidateInputParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; + +public class ConsolidateOperation extends BaseOperation<ConsolidateInputParcel> { + + public ConsolidateOperation(Context context, ProviderHelper providerHelper, Progressable + progressable) { + super(context, providerHelper, progressable); + } + + @NonNull + @Override + public ConsolidateResult execute(ConsolidateInputParcel consolidateInputParcel, + CryptoInputParcel cryptoInputParcel) { + if (consolidateInputParcel.mConsolidateRecovery) { + return mProviderHelper.consolidateDatabaseStep2(mProgressable); + } else { + return mProviderHelper.consolidateDatabaseStep1(mProgressable); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/DeleteOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/DeleteOperation.java index 5ef04ab05..56bd3b786 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/DeleteOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/DeleteOperation.java @@ -18,15 +18,19 @@ package org.sufficientlysecure.keychain.operations; import android.content.Context; +import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; import org.sufficientlysecure.keychain.operations.results.DeleteResult; +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.pgp.Progressable; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; +import org.sufficientlysecure.keychain.service.DeleteKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; /** An operation which implements a high level keyring delete operation. @@ -37,13 +41,24 @@ import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; * a list. * */ -public class DeleteOperation extends BaseOperation { +public class DeleteOperation extends BaseOperation<DeleteKeyringParcel> { public DeleteOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { super(context, providerHelper, progressable); } - public DeleteResult execute(long[] masterKeyIds, boolean isSecret) { + @NonNull + @Override + public OperationResult execute(DeleteKeyringParcel deleteKeyringParcel, + CryptoInputParcel cryptoInputParcel) { + + long[] masterKeyIds = deleteKeyringParcel.mMasterKeyIds; + boolean isSecret = deleteKeyringParcel.mIsSecret; + + return onlyDeleteKey(masterKeyIds, isSecret); + } + + private DeleteResult onlyDeleteKey(long[] masterKeyIds, boolean isSecret) { OperationLog log = new OperationLog(); @@ -104,7 +119,6 @@ public class DeleteOperation extends BaseOperation { } return new DeleteResult(result, log, success, fail); - } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java index 4072d91c5..f5ba88502 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java @@ -18,10 +18,12 @@ package org.sufficientlysecure.keychain.operations; import android.content.Context; +import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.operations.results.InputPendingResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; @@ -33,17 +35,20 @@ import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; +import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; 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.Passphrase; import org.sufficientlysecure.keychain.util.ProgressScaler; +import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; -/** An operation which implements a high level key edit operation. - * +/** + * An operation which implements a high level key edit operation. + * <p/> * This operation provides a higher level interface to the edit and * create key operations in PgpKeyOperation. It takes care of fetching * and saving the key before and after the operation. @@ -51,14 +56,23 @@ import java.util.concurrent.atomic.AtomicBoolean; * @see SaveKeyringParcel * */ -public class EditKeyOperation extends BaseOperation { +public class EditKeyOperation extends BaseOperation<SaveKeyringParcel> { public EditKeyOperation(Context context, ProviderHelper providerHelper, Progressable progressable, AtomicBoolean cancelled) { super(context, providerHelper, progressable, cancelled); } - public OperationResult execute(SaveKeyringParcel saveParcel, CryptoInputParcel cryptoInput) { + /** + * Saves an edited key, and uploads it to a server atomically or otherwise as + * specified in saveParcel + * + * @param saveParcel primary input to the operation + * @param cryptoInput input that changes if user interaction is required + * @return the result of the operation + */ + @NonNull + public InputPendingResult execute(SaveKeyringParcel saveParcel, CryptoInputParcel cryptoInput) { OperationLog log = new OperationLog(); log.add(LogType.MSG_ED, 0); @@ -119,6 +133,36 @@ public class EditKeyOperation extends BaseOperation { // It's a success, so this must be non-null now UncachedKeyRing ring = modifyResult.getRing(); + if (saveParcel.isUpload()) { + UncachedKeyRing publicKeyRing; + try { + publicKeyRing = ring.extractPublicKeyRing(); + } catch (IOException e) { + log.add(LogType.MSG_ED_ERROR_EXTRACTING_PUBLIC_UPLOAD, 1); + return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null); + } + + ExportKeyringParcel exportKeyringParcel = + new ExportKeyringParcel(saveParcel.getUploadKeyserver(), + publicKeyRing); + + ExportResult uploadResult = + new ExportOperation(mContext, mProviderHelper, mProgressable) + .execute(exportKeyringParcel, cryptoInput); + + if (uploadResult.isPending()) { + return uploadResult; + } else if (!uploadResult.success() && saveParcel.isUploadAtomic()) { + // if atomic, update fail implies edit operation should also fail and not save + log.add(uploadResult, 2); + return new EditKeyResult(log, RequiredInputParcel.createRetryUploadOperation(), + cryptoInput); + } else { + // upload succeeded or not atomic so we continue + log.add(uploadResult, 2); + } + } + // Save the new keyring. SaveKeyringResult saveResult = mProviderHelper .saveSecretKeyRing(ring, new ProgressScaler(mProgressable, 60, 95, 100)); @@ -130,15 +174,24 @@ public class EditKeyOperation extends BaseOperation { } // There is a new passphrase - cache it - if (saveParcel.mNewUnlock != null) { + if (saveParcel.mNewUnlock != null && cryptoInput.mCachePassphrase) { log.add(LogType.MSG_ED_CACHING_NEW, 1); - PassphraseCacheService.addCachedPassphrase(mContext, - ring.getMasterKeyId(), - ring.getMasterKeyId(), - saveParcel.mNewUnlock.mNewPassphrase != null - ? saveParcel.mNewUnlock.mNewPassphrase - : saveParcel.mNewUnlock.mNewPin, - ring.getPublicKey().getPrimaryUserIdWithFallback()); + + // NOTE: Don't cache empty passphrases! Important for MOVE_KEY_TO_CARD + if (saveParcel.mNewUnlock.mNewPassphrase != null + && ( ! saveParcel.mNewUnlock.mNewPassphrase.isEmpty())) { + PassphraseCacheService.addCachedPassphrase(mContext, + ring.getMasterKeyId(), + ring.getMasterKeyId(), + saveParcel.mNewUnlock.mNewPassphrase, + ring.getPublicKey().getPrimaryUserIdWithFallback()); + } else if (saveParcel.mNewUnlock.mNewPin != null) { + PassphraseCacheService.addCachedPassphrase(mContext, + ring.getMasterKeyId(), + ring.getMasterKeyId(), + saveParcel.mNewUnlock.mNewPin, + ring.getPublicKey().getPrimaryUserIdWithFallback()); + } } updateProgress(R.string.progress_done, 100, 100); @@ -150,5 +203,4 @@ public class EditKeyOperation extends BaseOperation { return new EditKeyResult(EditKeyResult.RESULT_OK, log, ring.getMasterKeyId()); } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java new file mode 100644 index 000000000..531ac01f2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> + * + * 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.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.Proxy; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; +import org.sufficientlysecure.keychain.keyimport.Keyserver.AddKeyException; +import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.ExportKeyringParcel; +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.FileHelper; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; + +/** + * An operation class which implements high level export + * operations. + * This class receives a source and/or destination of keys as input and performs + * all steps for this export. + * + * @see org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter#getSelectedEntries() + * For the export operation, the input consists of a set of key ids and + * either the name of a file or an output uri to write to. + */ +public class ExportOperation extends BaseOperation<ExportKeyringParcel> { + + public ExportOperation(Context context, ProviderHelper providerHelper, Progressable + progressable) { + super(context, providerHelper, progressable); + } + + public ExportOperation(Context context, ProviderHelper providerHelper, + Progressable progressable, AtomicBoolean cancelled) { + super(context, providerHelper, progressable, cancelled); + } + + public ExportResult uploadKeyRingToServer(HkpKeyserver server, CanonicalizedPublicKeyRing keyring, + Proxy proxy) { + return uploadKeyRingToServer(server, keyring.getUncachedKeyRing(), proxy); + } + + public ExportResult uploadKeyRingToServer(HkpKeyserver server, UncachedKeyRing keyring, Proxy proxy) { + mProgressable.setProgress(R.string.progress_uploading, 0, 1); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ArmoredOutputStream aos = null; + OperationLog log = new OperationLog(); + log.add(LogType.MSG_EXPORT_UPLOAD_PUBLIC, 0, KeyFormattingUtils.convertKeyIdToHex( + keyring.getPublicKey().getKeyId() + )); + + try { + aos = new ArmoredOutputStream(bos); + keyring.encode(aos); + aos.close(); + + String armoredKey = bos.toString("UTF-8"); + server.add(armoredKey, proxy); + + log.add(LogType.MSG_EXPORT_UPLOAD_SUCCESS, 1); + return new ExportResult(ExportResult.RESULT_OK, log); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException", e); + + log.add(LogType.MSG_EXPORT_ERROR_KEY, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } catch (AddKeyException e) { + Log.e(Constants.TAG, "AddKeyException", e); + + log.add(LogType.MSG_EXPORT_ERROR_UPLOAD, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } finally { + mProgressable.setProgress(R.string.progress_uploading, 1, 1); + try { + if (aos != null) { + aos.close(); + } + bos.close(); + } catch (IOException e) { + // this is just a finally thing, no matter if it doesn't work out. + } + } + } + + public ExportResult exportToFile(long[] masterKeyIds, boolean exportSecret, String outputFile) { + + OperationLog log = new OperationLog(); + if (masterKeyIds != null) { + log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); + } else { + log.add(LogType.MSG_EXPORT_ALL, 0); + } + + // do we have a file name? + if (outputFile == null) { + log.add(LogType.MSG_EXPORT_ERROR_NO_FILE, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + + log.add(LogType.MSG_EXPORT_FILE_NAME, 1, outputFile); + + // check if storage is ready + if (!FileHelper.isStorageMounted(outputFile)) { + log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + + try { + OutputStream outStream = new FileOutputStream(outputFile); + try { + ExportResult result = exportKeyRings(log, masterKeyIds, exportSecret, outStream); + if (result.cancelled()) { + //noinspection ResultOfMethodCallIgnored + new File(outputFile).delete(); + } + return result; + } finally { + outStream.close(); + } + } catch (IOException e) { + log.add(LogType.MSG_EXPORT_ERROR_FOPEN, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + + } + + public ExportResult exportToUri(long[] masterKeyIds, boolean exportSecret, Uri outputUri) { + + OperationLog log = new OperationLog(); + if (masterKeyIds != null) { + log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); + } else { + log.add(LogType.MSG_EXPORT_ALL, 0); + } + + // do we have a file name? + if (outputUri == null) { + log.add(LogType.MSG_EXPORT_ERROR_NO_URI, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + + try { + OutputStream outStream = mProviderHelper.getContentResolver().openOutputStream + (outputUri); + return exportKeyRings(log, masterKeyIds, exportSecret, outStream); + } catch (FileNotFoundException e) { + log.add(LogType.MSG_EXPORT_ERROR_URI_OPEN, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + + } + + ExportResult exportKeyRings(OperationLog log, long[] masterKeyIds, boolean exportSecret, + OutputStream outStream) { + + /* TODO isn't this checked above, with the isStorageMounted call? + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + */ + + if (!BufferedOutputStream.class.isInstance(outStream)) { + outStream = new BufferedOutputStream(outStream); + } + + int okSecret = 0, okPublic = 0, progress = 0; + + Cursor cursor = null; + try { + + String selection = null, selectionArgs[] = null; + + if (masterKeyIds != null) { + // convert long[] to String[] + selectionArgs = new String[masterKeyIds.length]; + for (int i = 0; i < masterKeyIds.length; i++) { + selectionArgs[i] = Long.toString(masterKeyIds[i]); + } + + // generates ?,?,? as placeholders for selectionArgs + String placeholders = TextUtils.join(",", + Collections.nCopies(masterKeyIds.length, "?")); + + // put together selection string + selection = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + + " IN (" + placeholders + ")"; + } + + cursor = mProviderHelper.getContentResolver().query( + KeyRings.buildUnifiedKeyRingsUri(), new String[]{ + KeyRings.MASTER_KEY_ID, KeyRings.PUBKEY_DATA, + KeyRings.PRIVKEY_DATA, KeyRings.HAS_ANY_SECRET + }, selection, selectionArgs, Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + ); + + if (cursor == null || !cursor.moveToFirst()) { + log.add(LogType.MSG_EXPORT_ERROR_DB, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); + } + + int numKeys = cursor.getCount(); + + updateProgress( + mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, + numKeys), 0, numKeys); + + // For each public masterKey id + while (!cursor.isAfterLast()) { + + long keyId = cursor.getLong(0); + ArmoredOutputStream arOutStream = null; + + // Create an output stream + try { + arOutStream = new ArmoredOutputStream(outStream); + + log.add(LogType.MSG_EXPORT_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyId)); + + byte[] data = cursor.getBlob(1); + CanonicalizedKeyRing ring = + UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); + ring.encode(arOutStream); + + okPublic += 1; + } catch (PgpGeneralException e) { + log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); + updateProgress(progress++, numKeys); + continue; + } finally { + // make sure this is closed + if (arOutStream != null) { + arOutStream.close(); + } + arOutStream = null; + } + + if (exportSecret && cursor.getInt(3) > 0) { + try { + arOutStream = new ArmoredOutputStream(outStream); + + // export secret key part + log.add(LogType.MSG_EXPORT_SECRET, 2, KeyFormattingUtils.beautifyKeyId + (keyId)); + byte[] data = cursor.getBlob(2); + CanonicalizedKeyRing ring = + UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); + ring.encode(arOutStream); + + okSecret += 1; + } catch (PgpGeneralException e) { + log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); + updateProgress(progress++, numKeys); + continue; + } finally { + // make sure this is closed + if (arOutStream != null) { + arOutStream.close(); + } + } + } + + updateProgress(progress++, numKeys); + + cursor.moveToNext(); + } + + updateProgress(R.string.progress_done, numKeys, numKeys); + + } catch (IOException e) { + log.add(LogType.MSG_EXPORT_ERROR_IO, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); + } finally { + // Make sure the stream is closed + if (outStream != null) try { + outStream.close(); + } catch (Exception e) { + Log.e(Constants.TAG, "error closing stream", e); + } + if (cursor != null) { + cursor.close(); + } + } + + + log.add(LogType.MSG_EXPORT_SUCCESS, 1); + return new ExportResult(ExportResult.RESULT_OK, log, okPublic, okSecret); + + } + + @NonNull + public ExportResult execute(ExportKeyringParcel exportInput, CryptoInputParcel cryptoInput) { + switch (exportInput.mExportType) { + case UPLOAD_KEYSERVER: { + Proxy proxy; + if (cryptoInput.getParcelableProxy() == null) { + // explicit proxy not set + if (!OrbotHelper.isOrbotInRequiredState(mContext)) { + return new ExportResult(null, + RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); + } + proxy = Preferences.getPreferences(mContext).getProxyPrefs() + .parcelableProxy.getProxy(); + } else { + proxy = cryptoInput.getParcelableProxy().getProxy(); + } + + HkpKeyserver hkpKeyserver = new HkpKeyserver(exportInput.mKeyserver); + try { + if (exportInput.mCanonicalizedPublicKeyringUri != null) { + CanonicalizedPublicKeyRing keyring + = mProviderHelper.getCanonicalizedPublicKeyRing( + exportInput.mCanonicalizedPublicKeyringUri); + return uploadKeyRingToServer(hkpKeyserver, keyring, proxy); + } else { + return uploadKeyRingToServer(hkpKeyserver, exportInput.mUncachedKeyRing, + proxy); + } + } catch (ProviderHelper.NotFoundException e) { + Log.e(Constants.TAG, "error uploading key", e); + return new ExportResult(ExportResult.RESULT_ERROR, new OperationLog()); + } + } + case EXPORT_FILE: { + return exportToFile(exportInput.mMasterKeyIds, exportInput.mExportSecret, + exportInput.mOutputFile); + } + case EXPORT_URI: { + return exportToUri(exportInput.mMasterKeyIds, exportInput.mExportSecret, + exportInput.mOutputUri); + } + default: { // can never happen, all enum types must be handled above + throw new AssertionError("must not happen, this is a bug!"); + } + } + } +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportExportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportExportOperation.java deleted file mode 100644 index 86cfc21a3..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportExportOperation.java +++ /dev/null @@ -1,583 +0,0 @@ -/* - * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> - * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> - * - * 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 android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -import org.spongycastle.bcpg.ArmoredOutputStream; -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; -import org.sufficientlysecure.keychain.keyimport.KeybaseKeyserver; -import org.sufficientlysecure.keychain.keyimport.Keyserver; -import org.sufficientlysecure.keychain.keyimport.Keyserver.AddKeyException; -import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; -import org.sufficientlysecure.keychain.operations.results.ExportResult; -import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; -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.pgp.CanonicalizedKeyRing; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; -import org.sufficientlysecure.keychain.pgp.PgpHelper; -import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; -import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.ParcelableFileCache; -import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; -import org.sufficientlysecure.keychain.util.ProgressScaler; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -/** An operation class which implements high level import and export - * operations. - * - * This class receives a source and/or destination of keys as input and performs - * all steps for this import or export. - * - * 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 and/or keybase.io. - * 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. - * @see org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter#getSelectedEntries() - * - * For the export operation, the input consists of a set of key ids and - * either the name of a file or an output uri to write to. - * - * TODO rework uploadKeyRingToServer - * - */ -public class ImportExportOperation extends BaseOperation { - - public ImportExportOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { - super(context, providerHelper, progressable); - } - - public ImportExportOperation(Context context, ProviderHelper providerHelper, - Progressable progressable, AtomicBoolean cancelled) { - super(context, providerHelper, progressable, cancelled); - } - - public void uploadKeyRingToServer(HkpKeyserver server, CanonicalizedPublicKeyRing keyring) throws AddKeyException { - uploadKeyRingToServer(server, keyring.getUncachedKeyRing()); - } - - public void uploadKeyRingToServer(HkpKeyserver server, UncachedKeyRing keyring) throws AddKeyException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ArmoredOutputStream aos = null; - try { - aos = new ArmoredOutputStream(bos); - keyring.encode(aos); - aos.close(); - - String armoredKey = bos.toString("UTF-8"); - server.add(armoredKey); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException", e); - throw new AddKeyException(); - } finally { - try { - if (aos != null) { - aos.close(); - } - bos.close(); - } catch (IOException e) { - // this is just a finally thing, no matter if it doesn't work out. - } - } - } - - public ImportKeyResult importKeyRings(List<ParcelableKeyRing> entries, String keyServerUri) { - - Iterator<ParcelableKeyRing> it = entries.iterator(); - int numEntries = entries.size(); - - return importKeyRings(it, numEntries, keyServerUri); - - } - - public ImportKeyResult importKeyRings(ParcelableFileCache<ParcelableKeyRing> cache, String keyServerUri) { - - // get entries from cached file - try { - IteratorWithSize<ParcelableKeyRing> it = cache.readCache(); - int numEntries = it.getSize(); - - return importKeyRings(it, numEntries, keyServerUri); - } 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); - } - - } - - public ImportKeyResult importKeyRings(Iterator<ParcelableKeyRing> entries, int num, String keyServerUri) { - updateProgress(R.string.progress_importing, 0, 100); - - OperationLog log = new OperationLog(); - log.add(LogType.MSG_IMPORT, 0, num); - - // 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, badKeys = 0, secret = 0; - ArrayList<Long> importedMasterKeyIds = new ArrayList<>(); - - boolean cancelled = false; - int position = 0; - double progSteps = 100.0 / num; - - KeybaseKeyserver keybaseServer = null; - HkpKeyserver keyServer = null; - - // 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; - } - - try { - - UncachedKeyRing key = null; - - // If there is already byte data, use that - if (entry.mBytes != null) { - key = UncachedKeyRing.decodeFromData(entry.mBytes); - } - // Otherwise, we need to fetch the data from a server first - else { - - // We fetch from keyservers first, because we tend to get more certificates - // from there, so the number of certificates which are merged in later is smaller. - - // If we have a keyServerUri and a fingerprint or at least a keyId, - // download from HKP - if (keyServerUri != null - && (entry.mKeyIdHex != null || entry.mExpectedFingerprint != null)) { - // Make sure we have the keyserver instance cached - if (keyServer == null) { - log.add(LogType.MSG_IMPORT_KEYSERVER, 1, keyServerUri); - keyServer = new HkpKeyserver(keyServerUri); - } - - try { - byte[] data; - // Download by fingerprint, or keyId - whichever is available - if (entry.mExpectedFingerprint != null) { - log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER, 2, "0x" + entry.mExpectedFingerprint.substring(24)); - data = keyServer.get("0x" + entry.mExpectedFingerprint).getBytes(); - } else { - log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER, 2, entry.mKeyIdHex); - data = keyServer.get(entry.mKeyIdHex).getBytes(); - } - key = UncachedKeyRing.decodeFromData(data); - if (key != null) { - log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_OK, 3); - } else { - log.add(LogType.MSG_IMPORT_FETCH_ERROR_DECODE, 3); - } - } catch (Keyserver.QueryFailedException e) { - Log.e(Constants.TAG, "query failed", e); - log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3, e.getMessage()); - } - } - - // If we have a keybase name, try to fetch from there - if (entry.mKeybaseName != null) { - // Make sure we have this cached - if (keybaseServer == null) { - keybaseServer = new KeybaseKeyserver(); - } - - try { - log.add(LogType.MSG_IMPORT_FETCH_KEYBASE, 2, entry.mKeybaseName); - byte[] data = keybaseServer.get(entry.mKeybaseName).getBytes(); - key = UncachedKeyRing.decodeFromData(data); - - // If there already is a key (of keybase origin), merge the two - if (key != null) { - log.add(LogType.MSG_IMPORT_MERGE, 3); - UncachedKeyRing merged = UncachedKeyRing.decodeFromData(data); - merged = key.merge(merged, log, 4); - // If the merge didn't fail, use the new merged key - if (merged != null) { - key = merged; - } - } else { - log.add(LogType.MSG_IMPORT_FETCH_ERROR_DECODE, 3); - key = UncachedKeyRing.decodeFromData(data); - } - } catch (Keyserver.QueryFailedException e) { - // download failed, too bad. just proceed - Log.e(Constants.TAG, "query failed", e); - log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3); - } - } - } - - if (key == null) { - log.add(LogType.MSG_IMPORT_FETCH_ERROR, 2); - badKeys += 1; - continue; - } - - // If we have an expected fingerprint, make sure it matches - if (entry.mExpectedFingerprint != null) { - if (!key.containsSubkey(entry.mExpectedFingerprint)) { - log.add(LogType.MSG_IMPORT_FINGERPRINT_ERROR, 2); - badKeys += 1; - continue; - } else { - log.add(LogType.MSG_IMPORT_FINGERPRINT_OK, 2); - } - } - - // Another check if we have been cancelled - if (checkCancelled()) { - cancelled = true; - break; - } - - SaveKeyringResult result; - mProviderHelper.clearLog(); - if (key.isSecret()) { - result = mProviderHelper.saveSecretKeyRing(key, - new ProgressScaler(mProgressable, (int)(position*progSteps), (int)((position+1)*progSteps), 100)); - } else { - result = mProviderHelper.savePublicKeyRing(key, - new ProgressScaler(mProgressable, (int)(position*progSteps), (int)((position+1)*progSteps), 100)); - } - if (!result.success()) { - badKeys += 1; - } else if (result.updated()) { - updatedKeys += 1; - importedMasterKeyIds.add(key.getMasterKeyId()); - } else { - newKeys += 1; - if (key.isSecret()) { - secret += 1; - } - importedMasterKeyIds.add(key.getMasterKeyId()); - } - - log.add(result, 2); - - } catch (IOException | PgpGeneralException e) { - Log.e(Constants.TAG, "Encountered bad key on import!", e); - ++badKeys; - } - // update progress - position++; - } - - // Special: consolidate on secret key import (cannot be cancelled!) - if (secret > 0) { - setPreventCancel(); - ConsolidateResult result = mProviderHelper.consolidateDatabaseStep1(mProgressable); - log.add(result, 1); - } - - // Special: make sure new data is synced into contacts - // disabling sync right now since it reduces speed while multi-threading - // so, we expect calling functions to take care of it. KeychainIntentService handles this - //ContactSyncAdapterService.requestSync(); - - // 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) { - 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; - } - } - - // 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); - } - - ContactSyncAdapterService.requestSync(); - - return new ImportKeyResult(resultType, log, newKeys, updatedKeys, badKeys, secret, - importedMasterKeyIdsArray); - } - - public ExportResult exportToFile(long[] masterKeyIds, boolean exportSecret, String outputFile) { - - OperationLog log = new OperationLog(); - if (masterKeyIds != null) { - log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); - } else { - log.add(LogType.MSG_EXPORT_ALL, 0); - } - - // do we have a file name? - if (outputFile == null) { - log.add(LogType.MSG_EXPORT_ERROR_NO_FILE, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - // check if storage is ready - if (!FileHelper.isStorageMounted(outputFile)) { - log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - try { - OutputStream outStream = new FileOutputStream(outputFile); - ExportResult result = exportKeyRings(log, masterKeyIds, exportSecret, outStream); - if (result.cancelled()) { - //noinspection ResultOfMethodCallIgnored - new File(outputFile).delete(); - } - return result; - } catch (FileNotFoundException e) { - log.add(LogType.MSG_EXPORT_ERROR_FOPEN, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - } - - public ExportResult exportToUri(long[] masterKeyIds, boolean exportSecret, Uri outputUri) { - - OperationLog log = new OperationLog(); - if (masterKeyIds != null) { - log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); - } else { - log.add(LogType.MSG_EXPORT_ALL, 0); - } - - // do we have a file name? - if (outputUri == null) { - log.add(LogType.MSG_EXPORT_ERROR_NO_URI, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - try { - OutputStream outStream = mProviderHelper.getContentResolver().openOutputStream(outputUri); - return exportKeyRings(log, masterKeyIds, exportSecret, outStream); - } catch (FileNotFoundException e) { - log.add(LogType.MSG_EXPORT_ERROR_URI_OPEN, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - } - - ExportResult exportKeyRings(OperationLog log, long[] masterKeyIds, boolean exportSecret, - OutputStream outStream) { - - /* TODO isn't this checked above, with the isStorageMounted call? - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - */ - - if ( ! BufferedOutputStream.class.isInstance(outStream)) { - outStream = new BufferedOutputStream(outStream); - } - - int okSecret = 0, okPublic = 0, progress = 0; - - Cursor cursor = null; - try { - - String selection = null, ids[] = null; - - if (masterKeyIds != null) { - // generate placeholders and string selection args - ids = new String[masterKeyIds.length]; - StringBuilder placeholders = new StringBuilder("?"); - for (int i = 0; i < masterKeyIds.length; i++) { - ids[i] = Long.toString(masterKeyIds[i]); - if (i != 0) { - placeholders.append(",?"); - } - } - - // put together selection string - selection = Tables.KEY_RINGS_PUBLIC + "." + KeyRings.MASTER_KEY_ID - + " IN (" + placeholders + ")"; - } - - cursor = mProviderHelper.getContentResolver().query( - KeyRings.buildUnifiedKeyRingsUri(), new String[]{ - KeyRings.MASTER_KEY_ID, KeyRings.PUBKEY_DATA, - KeyRings.PRIVKEY_DATA, KeyRings.HAS_ANY_SECRET - }, selection, ids, Tables.KEYS + "." + KeyRings.MASTER_KEY_ID - ); - - if (cursor == null || !cursor.moveToFirst()) { - log.add(LogType.MSG_EXPORT_ERROR_DB, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); - } - - int numKeys = cursor.getCount(); - - updateProgress( - mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, - numKeys), 0, numKeys); - - // For each public masterKey id - while (!cursor.isAfterLast()) { - - long keyId = cursor.getLong(0); - ArmoredOutputStream arOutStream = null; - - // Create an output stream - try { - arOutStream = new ArmoredOutputStream(outStream); - - log.add(LogType.MSG_EXPORT_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyId)); - - byte[] data = cursor.getBlob(1); - CanonicalizedKeyRing ring = - UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); - ring.encode(arOutStream); - - okPublic += 1; - } catch (PgpGeneralException e) { - log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); - updateProgress(progress++, numKeys); - continue; - } finally { - // make sure this is closed - if (arOutStream != null) { - arOutStream.close(); - } - arOutStream = null; - } - - if (exportSecret && cursor.getInt(3) > 0) { - try { - arOutStream = new ArmoredOutputStream(outStream); - - // export secret key part - log.add(LogType.MSG_EXPORT_SECRET, 2, KeyFormattingUtils.beautifyKeyId(keyId)); - byte[] data = cursor.getBlob(2); - CanonicalizedKeyRing ring = - UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); - ring.encode(arOutStream); - - okSecret += 1; - } catch (PgpGeneralException e) { - log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); - updateProgress(progress++, numKeys); - continue; - } finally { - // make sure this is closed - if (arOutStream != null) { - arOutStream.close(); - } - } - } - - updateProgress(progress++, numKeys); - - cursor.moveToNext(); - } - - updateProgress(R.string.progress_done, numKeys, numKeys); - - } catch (IOException e) { - log.add(LogType.MSG_EXPORT_ERROR_IO, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); - } finally { - // Make sure the stream is closed - if (outStream != null) try { - outStream.close(); - } catch (Exception e) { - Log.e(Constants.TAG, "error closing stream", e); - } - if (cursor != null) { - cursor.close(); - } - } - - - log.add(LogType.MSG_EXPORT_SUCCESS, 1); - return new ExportResult(ExportResult.RESULT_OK, log, okPublic, okSecret); - - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java new file mode 100644 index 000000000..7b224fe8e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java @@ -0,0 +1,574 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> + * + * 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.net.Proxy; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.Iterator; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; +import org.sufficientlysecure.keychain.keyimport.KeybaseKeyserver; +import org.sufficientlysecure.keychain.keyimport.Keyserver; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; +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.pgp.Progressable; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ParcelableFileCache; +import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; +import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.ProgressScaler; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; + +/** + * 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 and/or keybase.io. + * 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. + * + * @see org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter#getSelectedEntries() + */ +public class ImportOperation extends BaseOperation<ImportKeyringParcel> { + + public ImportOperation(Context context, ProviderHelper providerHelper, Progressable + progressable) { + super(context, providerHelper, progressable); + } + + public ImportOperation(Context context, ProviderHelper providerHelper, + Progressable progressable, AtomicBoolean cancelled) { + super(context, providerHelper, progressable, cancelled); + } + + // Overloaded functions for using progressable supplied in constructor during import + public ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int num, + String keyServerUri, Proxy proxy) { + return serialKeyRingImport(entries, num, keyServerUri, mProgressable, proxy); + } + + @NonNull + private ImportKeyResult serialKeyRingImport(ParcelableFileCache<ParcelableKeyRing> cache, + String keyServerUri, Proxy proxy) { + + // get entries from cached file + try { + IteratorWithSize<ParcelableKeyRing> it = cache.readCache(); + int numEntries = it.getSize(); + + return serialKeyRingImport(it, numEntries, keyServerUri, mProgressable, proxy); + } 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); + } + + } + + /** + * Since the introduction of multithreaded import, we expect calling functions to handle the + * contact-to-key sync i.e ContactSyncAdapterService.requestSync() + * + * @param entries keys to import + * @param num number of keys to import + * @param keyServerUri 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 num, + String keyServerUri, Progressable progressable, + Proxy proxy) { + if (progressable != null) { + progressable.setProgress(R.string.progress_importing, 0, 100); + } + + OperationLog log = new OperationLog(); + log.add(LogType.MSG_IMPORT, 0, num); + + // 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, badKeys = 0, secret = 0; + ArrayList<Long> importedMasterKeyIds = new ArrayList<>(); + + boolean cancelled = false; + int position = 0; + double progSteps = 100.0 / num; + + KeybaseKeyserver keybaseServer = null; + HkpKeyserver keyServer = null; + + // 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; + } + + try { + + UncachedKeyRing key = null; + + // If there is already byte data, use that + if (entry.mBytes != null) { + key = UncachedKeyRing.decodeFromData(entry.mBytes); + } + // Otherwise, we need to fetch the data from a server first + else { + + // We fetch from keyservers first, because we tend to get more certificates + // from there, so the number of certificates which are merged in later is + // smaller. + + // If we have a keyServerUri and a fingerprint or at least a keyId, + // download from HKP + if (keyServerUri != null + && (entry.mKeyIdHex != null || entry.mExpectedFingerprint != null)) { + // Make sure we have the keyserver instance cached + if (keyServer == null) { + log.add(LogType.MSG_IMPORT_KEYSERVER, 1, keyServerUri); + keyServer = new HkpKeyserver(keyServerUri); + } + + try { + byte[] data; + // Download by fingerprint, or keyId - whichever is available + if (entry.mExpectedFingerprint != null) { + log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER, 2, "0x" + + entry.mExpectedFingerprint.substring(24)); + data = keyServer.get("0x" + entry.mExpectedFingerprint, proxy) + .getBytes(); + } else { + log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER, 2, entry.mKeyIdHex); + data = keyServer.get(entry.mKeyIdHex, proxy).getBytes(); + } + key = UncachedKeyRing.decodeFromData(data); + if (key != null) { + log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_OK, 3); + } else { + log.add(LogType.MSG_IMPORT_FETCH_ERROR_DECODE, 3); + } + } catch (Keyserver.QueryFailedException e) { + Log.d(Constants.TAG, "query failed", e); + log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3, e.getMessage()); + } + } + + // If we have a keybase name, try to fetch from there + if (entry.mKeybaseName != null) { + // Make sure we have this cached + if (keybaseServer == null) { + keybaseServer = new KeybaseKeyserver(); + } + + try { + log.add(LogType.MSG_IMPORT_FETCH_KEYBASE, 2, entry.mKeybaseName); + byte[] data = keybaseServer.get(entry.mKeybaseName, proxy).getBytes(); + UncachedKeyRing keybaseKey = UncachedKeyRing.decodeFromData(data); + + // If there already is a key, merge the two + if (key != null && keybaseKey != null) { + log.add(LogType.MSG_IMPORT_MERGE, 3); + keybaseKey = key.merge(keybaseKey, log, 4); + // If the merge didn't fail, use the new merged key + if (keybaseKey != null) { + key = keybaseKey; + } else { + log.add(LogType.MSG_IMPORT_MERGE_ERROR, 4); + } + } else if (keybaseKey != null) { + key = keybaseKey; + } + } catch (Keyserver.QueryFailedException e) { + // download failed, too bad. just proceed + Log.e(Constants.TAG, "query failed", e); + log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3, e.getMessage()); + } + } + } + + if (key == null) { + log.add(LogType.MSG_IMPORT_FETCH_ERROR, 2); + badKeys += 1; + continue; + } + + // If we have an expected fingerprint, make sure it matches + if (entry.mExpectedFingerprint != null) { + if (!key.containsSubkey(entry.mExpectedFingerprint)) { + log.add(LogType.MSG_IMPORT_FINGERPRINT_ERROR, 2); + badKeys += 1; + continue; + } else { + log.add(LogType.MSG_IMPORT_FINGERPRINT_OK, 2); + } + } + + // Another check if we have been cancelled + if (checkCancelled()) { + cancelled = true; + break; + } + + SaveKeyringResult result; + // synchronizing prevents https://github.com/open-keychain/open-keychain/issues/1221 + // and https://github.com/open-keychain/open-keychain/issues/1480 + synchronized (mProviderHelper) { + mProviderHelper.clearLog(); + if (key.isSecret()) { + result = mProviderHelper.saveSecretKeyRing(key, + new ProgressScaler(progressable, (int) (position * progSteps), + (int) ((position + 1) * progSteps), 100)); + } else { + result = mProviderHelper.savePublicKeyRing(key, + new ProgressScaler(progressable, (int) (position * progSteps), + (int) ((position + 1) * progSteps), 100)); + } + } + if (!result.success()) { + badKeys += 1; + } else { + if (result.updated()) { + updatedKeys += 1; + importedMasterKeyIds.add(key.getMasterKeyId()); + } else { + newKeys += 1; + if (key.isSecret()) { + secret += 1; + } + importedMasterKeyIds.add(key.getMasterKeyId()); + } + if (entry.mBytes == null) { + // synonymous to isDownloadFromKeyserver. + // If no byte data was supplied, import from keyserver took place + // this prevents file imports being noted as keyserver imports + mProviderHelper.renewKeyLastUpdatedTime(key.getMasterKeyId(), + GregorianCalendar.getInstance().getTimeInMillis(), + TimeUnit.MILLISECONDS); + } + } + + log.add(result, 2); + } catch (IOException | PgpGeneralException e) { + Log.e(Constants.TAG, "Encountered bad key on import!", e); + ++badKeys; + } + // update progress + position++; + } + + // 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 (secret > 0) { + setPreventCancel(); + ConsolidateResult result; + synchronized (mProviderHelper) { + result = mProviderHelper.consolidateDatabaseStep1(progressable); + } + log.add(result, 1); + } + + // Special: make sure new data is synced into contacts + // disabling sync right now since it reduces speed while multi-threading + // so, we expect calling functions to take care of it. KeychainService handles this + // ContactSyncAdapterService.requestSync(); + + // 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) { + 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; + } + } + + // 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); + } + + return new ImportKeyResult(resultType, log, newKeys, updatedKeys, badKeys, secret, + importedMasterKeyIdsArray); + } + + @NonNull + @Override + public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) { + ArrayList<ParcelableKeyRing> keyList = importInput.mKeyList; + String keyServer = importInput.mKeyserver; + + ImportKeyResult result; + + if (keyList == null) {// import from file, do serially + ParcelableFileCache<ParcelableKeyRing> cache = new ParcelableFileCache<>(mContext, + "key_import.pcl"); + + result = serialKeyRingImport(cache, null, null); + } else { + Proxy 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).getProxyPrefs().parcelableProxy + .getProxy(); + } else { + proxy = cryptoInput.getParcelableProxy().getProxy(); + } + + result = multiThreadedKeyImport(keyList.iterator(), keyList.size(), keyServer, proxy); + } + + ContactSyncAdapterService.requestSync(); + return result; + } + + @NonNull + private ImportKeyResult multiThreadedKeyImport(Iterator<ParcelableKeyRing> keyListIterator, + int totKeys, final String keyServer, + final Proxy proxy) { + Log.d(Constants.TAG, "Multi-threaded key import starting"); + if (keyListIterator != null) { + KeyImportAccumulator accumulator = new KeyImportAccumulator(totKeys, mProgressable); + + final ProgressScaler ignoreProgressable = new ProgressScaler(); + + final int maxThreads = 200; + ExecutorService importExecutor = new ThreadPoolExecutor(0, maxThreads, + 30L, TimeUnit.SECONDS, + new SynchronousQueue<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() { + + ArrayList<ParcelableKeyRing> list = new ArrayList<>(); + list.add(pkRing); + + return serialKeyRingImport(list.iterator(), 1, keyServer, + ignoreProgressable, proxy); + } + }; + + importCompletionService.submit(importOperationCallable); + } + + while (!accumulator.isImportFinished()) { // accumulate the results of each import + try { + accumulator.accumulateKeyImport(importCompletionService.take().get()); + } catch (InterruptedException | ExecutionException e) { + Log.e(Constants.TAG, "A key could not be imported during multi-threaded " + + "import", e); + // 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(); + } + } + } + return accumulator.getConsolidatedResult(); + } + return new ImportKeyResult(ImportKeyResult.RESULT_FAIL_NOTHING, new OperationLog()); + } + + /** + * Used to accumulate the results of individual key imports + */ + public static class KeyImportAccumulator { + private OperationResult.OperationLog mImportLog = new OperationResult.OperationLog(); + Progressable mProgressable; + private int mTotalKeys; + private int mImportedKeys = 0; + ArrayList<Long> mImportedMasterKeyIds = new ArrayList<>(); + private int mBadKeys = 0; + private int mNewKeys = 0; + private int mUpdatedKeys = 0; + private int mSecret = 0; + private int mResultType = 0; + + /** + * 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); + } + } + + public synchronized void accumulateKeyImport(ImportKeyResult result) { + mImportedKeys++; + + if (mProgressable != null) { + mProgressable.setProgress(mImportedKeys, mTotalKeys); + } + + mImportLog.addAll(result.getLog().toList());//accumulates log + mBadKeys += result.mBadKeys; + mNewKeys += result.mNewKeys; + mUpdatedKeys += result.mUpdatedKeys; + mSecret += result.mSecret; + + long[] masterKeyIds = result.getImportedMasterKeyIds(); + for (long masterKeyId : masterKeyIds) { + mImportedMasterKeyIds.add(masterKeyId); + } + + // 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_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); + } + + return new ImportKeyResult(mResultType, mImportLog, mNewKeys, mUpdatedKeys, mBadKeys, + mSecret, masterKeyIds); + } + + public boolean isImportFinished() { + return mTotalKeys == mImportedKeys; + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java new file mode 100644 index 000000000..8f1abde83 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.ByteArrayInputStream; +import java.io.InputStream; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.textuality.keybase.lib.Proof; +import com.textuality.keybase.lib.prover.Prover; +import de.measite.minidns.Client; +import de.measite.minidns.DNSMessage; +import de.measite.minidns.Question; +import de.measite.minidns.Record; +import de.measite.minidns.record.Data; +import de.measite.minidns.record.TXT; +import org.json.JSONObject; +import org.spongycastle.openpgp.PGPUtil; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.KeybaseVerificationParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; + +public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificationParcel> { + + public KeybaseVerificationOperation(Context context, ProviderHelper providerHelper, + Progressable progressable) { + super(context, providerHelper, progressable); + } + + @NonNull + @Override + public KeybaseVerificationResult execute(KeybaseVerificationParcel keybaseInput, + CryptoInputParcel cryptoInput) { + Proxy proxy; + if (cryptoInput.getParcelableProxy() == null) { + // explicit proxy not set + if (!OrbotHelper.isOrbotInRequiredState(mContext)) { + return new KeybaseVerificationResult(null, + RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); + } + proxy = Preferences.getPreferences(mContext).getProxyPrefs() + .parcelableProxy.getProxy(); + } else { + proxy = cryptoInput.getParcelableProxy().getProxy(); + } + + String requiredFingerprint = keybaseInput.mRequiredFingerprint; + + OperationResult.OperationLog log = new OperationResult.OperationLog(); + log.add(OperationResult.LogType.MSG_KEYBASE_VERIFICATION, 0, requiredFingerprint); + + try { + String keybaseProof = keybaseInput.mKeybaseProof; + Proof proof = new Proof(new JSONObject(keybaseProof)); + mProgressable.setProgress(R.string.keybase_message_fetching_data, 0, 100); + + Prover prover = Prover.findProverFor(proof); + + if (prover == null) { + log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_NO_PROVER, 1, + proof.getPrettyName()); + return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); + } + + if (!prover.fetchProofData(proxy)) { + log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_FETCH_PROOF, 1); + return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); + } + + if (!prover.checkFingerprint(requiredFingerprint)) { + log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_FINGERPRINT_MISMATCH, 1); + return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); + } + + String domain = prover.dnsTxtCheckRequired(); + if (domain != null) { + DNSMessage dnsQuery = new Client().query(new Question(domain, Record.TYPE.TXT)); + if (dnsQuery == null) { + log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_DNS_FAIL, 1); + log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_SPECIFIC, 2, + getFlattenedProverLog(prover)); + return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); + } + Record[] records = dnsQuery.getAnswers(); + List<List<byte[]>> extents = new ArrayList<>(); + for (Record r : records) { + Data d = r.getPayload(); + if (d instanceof TXT) { + extents.add(((TXT) d).getExtents()); + } + } + if (!prover.checkDnsTxt(extents)) { + log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_SPECIFIC, 1, + getFlattenedProverLog(prover)); + return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); + } + } + + byte[] messageBytes = prover.getPgpMessage().getBytes(); + if (prover.rawMessageCheckRequired()) { + InputStream messageByteStream = PGPUtil.getDecoderStream(new + ByteArrayInputStream + (messageBytes)); + if (!prover.checkRawMessageBytes(messageByteStream)) { + log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_SPECIFIC, 1, + getFlattenedProverLog(prover)); + return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); + } + } + + PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable); + + PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(messageBytes) + .setSignedLiteralData(true) + .setRequiredSignerFingerprint(requiredFingerprint); + + DecryptVerifyResult decryptVerifyResult = op.execute(input, new CryptoInputParcel()); + + if (!decryptVerifyResult.success()) { + log.add(decryptVerifyResult, 1); + return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); + } + + if (!prover.validate(new String(decryptVerifyResult.getOutputBytes()))) { + log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_PAYLOAD_MISMATCH, 1); + return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); + } + + return new KeybaseVerificationResult(OperationResult.RESULT_OK, log, prover); + } catch (Exception e) { + // just adds the passed parameter, in this case e.getMessage() + log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_SPECIFIC, 1, e.getMessage()); + return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); + } + } + + private String getFlattenedProverLog(Prover prover) { + String log = ""; + for (String line : prover.getLog()) { + log += line + "\n"; + } + return log; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperation.java index ef08b0b77..2f25b6926 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperation.java @@ -17,7 +17,11 @@ package org.sufficientlysecure.keychain.operations; + +import java.util.concurrent.atomic.AtomicBoolean; + import android.content.Context; +import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; @@ -25,17 +29,17 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.Operat import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; +import org.sufficientlysecure.keychain.service.PromoteKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.ProgressScaler; -import java.util.concurrent.atomic.AtomicBoolean; - /** An operation which promotes a public key ring to a secret one. * * This operation can only be applied to public key rings where no secret key @@ -43,14 +47,17 @@ import java.util.concurrent.atomic.AtomicBoolean; * without secret key material, using a GNU_DUMMY s2k type. * */ -public class PromoteKeyOperation extends BaseOperation { +public class PromoteKeyOperation extends BaseOperation<PromoteKeyringParcel> { public PromoteKeyOperation(Context context, ProviderHelper providerHelper, Progressable progressable, AtomicBoolean cancelled) { super(context, providerHelper, progressable, cancelled); } - public PromoteKeyResult execute(long masterKeyId, byte[] cardAid) { + @NonNull + @Override + public PromoteKeyResult execute(PromoteKeyringParcel promoteKeyringParcel, + CryptoInputParcel cryptoInputParcel) { OperationLog log = new OperationLog(); log.add(LogType.MSG_PR, 0); @@ -61,12 +68,29 @@ public class PromoteKeyOperation extends BaseOperation { try { log.add(LogType.MSG_PR_FETCHING, 1, - KeyFormattingUtils.convertKeyIdToHex(masterKeyId)); + KeyFormattingUtils.convertKeyIdToHex(promoteKeyringParcel.mKeyRingId)); CanonicalizedPublicKeyRing pubRing = - mProviderHelper.getCanonicalizedPublicKeyRing(masterKeyId); + mProviderHelper.getCanonicalizedPublicKeyRing(promoteKeyringParcel.mKeyRingId); + + if (promoteKeyringParcel.mSubKeyIds == null) { + log.add(LogType.MSG_PR_ALL, 1); + } else { + // sort for binary search + for (CanonicalizedPublicKey key : pubRing.publicKeyIterator()) { + long subKeyId = key.getKeyId(); + if (naiveIndexOf(promoteKeyringParcel.mSubKeyIds, subKeyId) != null) { + log.add(LogType.MSG_PR_SUBKEY_MATCH, 1, + KeyFormattingUtils.convertKeyIdToHex(subKeyId)); + } else { + log.add(LogType.MSG_PR_SUBKEY_NOMATCH, 1, + KeyFormattingUtils.convertKeyIdToHex(subKeyId)); + } + } + } // create divert-to-card secret key from public key - promotedRing = pubRing.createDivertSecretRing(cardAid); + promotedRing = pubRing.createDivertSecretRing(promoteKeyringParcel.mCardAid, + promoteKeyringParcel.mSubKeyIds); } catch (NotFoundException e) { log.add(LogType.MSG_PR_ERROR_KEY_NOT_FOUND, 2); @@ -106,4 +130,13 @@ public class PromoteKeyOperation extends BaseOperation { } + static private Integer naiveIndexOf(long[] haystack, long needle) { + for (int i = 0; i < haystack.length; i++) { + if (needle == haystack[i]) { + return i; + } + } + return null; + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java new file mode 100644 index 000000000..975cf541a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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 android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.operations.results.InputPendingResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.operations.results.RevokeResult; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.RevokeKeyringParcel; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.Log; + +public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> { + + public RevokeOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { + super(context, providerHelper, progressable); + } + + @NonNull + @Override + public OperationResult execute(RevokeKeyringParcel revokeKeyringParcel, + CryptoInputParcel cryptoInputParcel) { + + // we don't cache passphrases during revocation + cryptoInputParcel.mCachePassphrase = false; + + long masterKeyId = revokeKeyringParcel.mMasterKeyId; + + OperationResult.OperationLog log = new OperationResult.OperationLog(); + log.add(OperationResult.LogType.MSG_REVOKE, 0, + KeyFormattingUtils.beautifyKeyId(masterKeyId)); + + try { + + Uri secretUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(masterKeyId); + CachedPublicKeyRing keyRing = mProviderHelper.getCachedPublicKeyRing(secretUri); + + // check if this is a master secret key we can work with + switch (keyRing.getSecretKeyType(masterKeyId)) { + case GNU_DUMMY: + log.add(OperationResult.LogType.MSG_EK_ERROR_DUMMY, 1); + return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); + } + + SaveKeyringParcel saveKeyringParcel = + new SaveKeyringParcel(masterKeyId, keyRing.getFingerprint()); + + // all revoke operations are made atomic as of now + saveKeyringParcel.setUpdateOptions(revokeKeyringParcel.mUpload, true, + revokeKeyringParcel.mKeyserver); + + saveKeyringParcel.mRevokeSubKeys.add(masterKeyId); + + InputPendingResult revokeAndUploadResult = new EditKeyOperation(mContext, + mProviderHelper, mProgressable, mCancelled) + .execute(saveKeyringParcel, cryptoInputParcel); + + if (revokeAndUploadResult.isPending()) { + return revokeAndUploadResult; + } + + log.add(revokeAndUploadResult, 1); + + if (revokeAndUploadResult.success()) { + log.add(OperationResult.LogType.MSG_REVOKE_OK, 1); + return new RevokeResult(RevokeResult.RESULT_OK, log, masterKeyId); + } else { + log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1); + return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); + } + + } catch (PgpKeyNotFoundException | ProviderHelper.NotFoundException e) { + Log.e(Constants.TAG, "could not find key to revoke", e); + log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1); + return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java index 651d15e8f..843a55389 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java @@ -19,13 +19,13 @@ package org.sufficientlysecure.keychain.operations; import android.content.Context; import android.net.Uri; +import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; @@ -37,6 +37,7 @@ import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSign import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequiredInputType; import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ProgressScaler; import java.io.ByteArrayInputStream; @@ -55,13 +56,14 @@ import java.util.concurrent.atomic.AtomicBoolean; * a pending result, it will terminate. * */ -public class SignEncryptOperation extends BaseOperation { +public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> { public SignEncryptOperation(Context context, ProviderHelper providerHelper, Progressable progressable, AtomicBoolean cancelled) { super(context, providerHelper, progressable, cancelled); } + @NonNull public SignEncryptResult execute(SignEncryptParcel input, CryptoInputParcel cryptoInput) { OperationLog log = new OperationLog(); @@ -85,7 +87,7 @@ public class SignEncryptOperation extends BaseOperation { input.getSignatureMasterKeyId()).getSecretSignId(); input.setSignatureSubKeyId(signKeyId); } catch (PgpKeyNotFoundException e) { - e.printStackTrace(); + Log.e(Constants.TAG, "Key not found", e); return new SignEncryptResult(SignEncryptResult.RESULT_ERROR, log, results); } } @@ -153,7 +155,7 @@ public class SignEncryptOperation extends BaseOperation { RequiredInputParcel requiredInput = result.getRequiredInputParcel(); // Passphrase returns immediately, nfc are aggregated if (requiredInput.mType == RequiredInputType.PASSPHRASE) { - return new SignEncryptResult(log, requiredInput, results); + return new SignEncryptResult(log, requiredInput, results, cryptoInput); } if (pendingInputBuilder == null) { pendingInputBuilder = new NfcSignOperationsBuilder(requiredInput.mSignatureTime, @@ -171,7 +173,7 @@ public class SignEncryptOperation extends BaseOperation { } while (!inputUris.isEmpty()); if (pendingInputBuilder != null && !pendingInputBuilder.isEmpty()) { - return new SignEncryptResult(log, pendingInputBuilder.build(), results); + return new SignEncryptResult(log, pendingInputBuilder.build(), results, cryptoInput); } if (!outputUris.isEmpty()) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/CertifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/CertifyResult.java index 0a0e63330..cf73f019c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/CertifyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/CertifyResult.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.os.Parcel; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.LogDisplayActivity; import org.sufficientlysecure.keychain.ui.LogDisplayFragment; @@ -38,8 +39,9 @@ public class CertifyResult extends InputPendingResult { super(result, log); } - public CertifyResult(OperationLog log, RequiredInputParcel requiredInput) { - super(log, requiredInput); + public CertifyResult(OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); } public CertifyResult(int result, OperationLog log, int certifyOk, int certifyError, int uploadOk, int uploadError) { @@ -132,7 +134,7 @@ public class CertifyResult extends InputPendingResult { intent.putExtra(LogDisplayFragment.EXTRA_RESULT, CertifyResult.this); activity.startActivity(intent); } - }, R.string.view_log); + }, R.string.snackbar_details); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java index 917b3415f..e8be9fa78 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2014-2015 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> * * This program is free software: you can redistribute it and/or modify @@ -20,19 +20,50 @@ package org.sufficientlysecure.keychain.operations.results; import android.os.Parcel; +import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpSignatureResult; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; -import org.sufficientlysecure.keychain.util.Passphrase; public class DecryptVerifyResult extends InputPendingResult { + public static final int RESULT_NO_DATA = RESULT_ERROR + 16; + public static final int RESULT_KEY_DISALLOWED = RESULT_ERROR + 32; + OpenPgpSignatureResult mSignatureResult; - OpenPgpMetadata mDecryptMetadata; + OpenPgpDecryptionResult mDecryptionResult; + OpenPgpMetadata mDecryptionMetadata; // This holds the charset which was specified in the ascii armor, if specified // https://tools.ietf.org/html/rfc4880#page56 String mCharset; + CryptoInputParcel mCachedCryptoInputParcel; + + byte[] mOutputBytes; + + public DecryptVerifyResult(int result, OperationLog log) { + super(result, log); + } + + public DecryptVerifyResult(OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); + } + + public DecryptVerifyResult(Parcel source) { + super(source); + mSignatureResult = source.readParcelable(OpenPgpSignatureResult.class.getClassLoader()); + mDecryptionResult = source.readParcelable(OpenPgpDecryptionResult.class.getClassLoader()); + mDecryptionMetadata = source.readParcelable(OpenPgpMetadata.class.getClassLoader()); + mCachedCryptoInputParcel = source.readParcelable(CryptoInputParcel.class.getClassLoader()); + } + + + public boolean isKeysDisallowed () { + return (mResult & RESULT_KEY_DISALLOWED) == RESULT_KEY_DISALLOWED; + } + public OpenPgpSignatureResult getSignatureResult() { return mSignatureResult; } @@ -41,38 +72,44 @@ public class DecryptVerifyResult extends InputPendingResult { mSignatureResult = signatureResult; } - public OpenPgpMetadata getDecryptMetadata() { - return mDecryptMetadata; + public OpenPgpDecryptionResult getDecryptionResult() { + return mDecryptionResult; } - public void setDecryptMetadata(OpenPgpMetadata decryptMetadata) { - mDecryptMetadata = decryptMetadata; + public void setDecryptionResult(OpenPgpDecryptionResult decryptionResult) { + mDecryptionResult = decryptionResult; } - public String getCharset () { - return mCharset; + public CryptoInputParcel getCachedCryptoInputParcel() { + return mCachedCryptoInputParcel; } - public void setCharset(String charset) { - mCharset = charset; + public void setCachedCryptoInputParcel(CryptoInputParcel cachedCryptoInputParcel) { + mCachedCryptoInputParcel = cachedCryptoInputParcel; } - public boolean isPending() { - return (mResult & RESULT_PENDING) == RESULT_PENDING; + public OpenPgpMetadata getDecryptionMetadata() { + return mDecryptionMetadata; } - public DecryptVerifyResult(int result, OperationLog log) { - super(result, log); + public void setDecryptionMetadata(OpenPgpMetadata decryptMetadata) { + mDecryptionMetadata = decryptMetadata; } - public DecryptVerifyResult(OperationLog log, RequiredInputParcel requiredInput) { - super(log, requiredInput); + public String getCharset () { + return mCharset; } - public DecryptVerifyResult(Parcel source) { - super(source); - mSignatureResult = source.readParcelable(OpenPgpSignatureResult.class.getClassLoader()); - mDecryptMetadata = source.readParcelable(OpenPgpMetadata.class.getClassLoader()); + public void setCharset(String charset) { + mCharset = charset; + } + + public void setOutputBytes(byte[] outputBytes) { + mOutputBytes = outputBytes; + } + + public byte[] getOutputBytes() { + return mOutputBytes; } public int describeContents() { @@ -81,8 +118,10 @@ public class DecryptVerifyResult extends InputPendingResult { public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); - dest.writeParcelable(mSignatureResult, 0); - dest.writeParcelable(mDecryptMetadata, 0); + dest.writeParcelable(mSignatureResult, flags); + dest.writeParcelable(mDecryptionResult, flags); + dest.writeParcelable(mDecryptionMetadata, flags); + dest.writeParcelable(mCachedCryptoInputParcel, flags); } public static final Creator<DecryptVerifyResult> CREATOR = new Creator<DecryptVerifyResult>() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java index 50f49add2..1a8f10d4f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java @@ -21,8 +21,11 @@ package org.sufficientlysecure.keychain.operations.results; import android.app.Activity; import android.content.Intent; import android.os.Parcel; +import android.support.annotation.Nullable; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.LogDisplayActivity; import org.sufficientlysecure.keychain.ui.LogDisplayFragment; import org.sufficientlysecure.keychain.ui.util.Notify; @@ -30,7 +33,7 @@ import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; import org.sufficientlysecure.keychain.ui.util.Notify.Showable; import org.sufficientlysecure.keychain.ui.util.Notify.Style; -public class DeleteResult extends OperationResult { +public class DeleteResult extends InputPendingResult { final public int mOk, mFail; @@ -40,6 +43,19 @@ public class DeleteResult extends OperationResult { mFail = fail; } + /** + * used when more input is required + * @param log operation log upto point of required input, if any + * @param requiredInput represents input required + */ + public DeleteResult(@Nullable OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); + // values are not to be used + mOk = -1; + mFail = -1; + } + /** Construct from a parcel - trivial because we have no extra data. */ public DeleteResult(Parcel source) { super(source); @@ -109,7 +125,10 @@ public class DeleteResult extends OperationResult { } else { duration = 0; style = Style.ERROR; - if (mFail == 0) { + if (mLog.getLast().mType == LogType.MSG_DEL_ERROR_MULTI_SECRET) { + str = activity.getString(R.string.secret_cannot_multiple); + } + else if (mFail == 0) { str = activity.getString(R.string.delete_nothing); } else { str = activity.getResources().getQuantityString(R.plurals.delete_fail, mFail); @@ -124,7 +143,7 @@ public class DeleteResult extends OperationResult { intent.putExtra(LogDisplayFragment.EXTRA_RESULT, DeleteResult.this); activity.startActivity(intent); } - }, R.string.view_log); + }, R.string.snackbar_details); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/EditKeyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/EditKeyResult.java index 842b75c3b..6098d59d5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/EditKeyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/EditKeyResult.java @@ -20,7 +20,10 @@ package org.sufficientlysecure.keychain.operations.results; import android.os.Parcel; -public class EditKeyResult extends OperationResult { +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; + +public class EditKeyResult extends InputPendingResult { public final Long mMasterKeyId; @@ -29,6 +32,12 @@ public class EditKeyResult extends OperationResult { mMasterKeyId = masterKeyId; } + public EditKeyResult(OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); + mMasterKeyId = null; + } + public EditKeyResult(Parcel source) { super(source); mMasterKeyId = source.readInt() != 0 ? source.readLong() : null; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java index c8edce259..e21ef949f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java @@ -19,7 +19,10 @@ package org.sufficientlysecure.keychain.operations.results; import android.os.Parcel; -public class ExportResult extends OperationResult { +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; + +public class ExportResult extends InputPendingResult { final int mOkPublic, mOkSecret; @@ -33,6 +36,15 @@ public class ExportResult extends OperationResult { mOkSecret = okSecret; } + + public ExportResult(OperationLog log, RequiredInputParcel requiredInputParcel, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInputParcel, cryptoInputParcel); + // we won't use these values + mOkPublic = -1; + mOkSecret = -1; + } + /** Construct from a parcel - trivial because we have no extra data. */ public ExportResult(Parcel source) { super(source); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/GetKeyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/GetKeyResult.java index 53bc545c5..bdc4d9a47 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/GetKeyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/GetKeyResult.java @@ -20,7 +20,10 @@ package org.sufficientlysecure.keychain.operations.results; import android.os.Parcel; -public class GetKeyResult extends OperationResult { +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; + +public class GetKeyResult extends InputPendingResult { public int mNonPgpPartsCount; @@ -36,6 +39,11 @@ public class GetKeyResult extends OperationResult { super(result, log); } + public GetKeyResult(OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); + } + public static final int RESULT_ERROR_NO_VALID_KEYS = RESULT_ERROR + 8; public static final int RESULT_ERROR_NO_PGP_PARTS = RESULT_ERROR + 16; public static final int RESULT_ERROR_QUERY_TOO_SHORT = RESULT_ERROR + 32; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java index 1438ad698..5f5090bee 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ImportKeyResult.java @@ -23,6 +23,8 @@ import android.content.Intent; import android.os.Parcel; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.LogDisplayActivity; import org.sufficientlysecure.keychain.ui.LogDisplayFragment; import org.sufficientlysecure.keychain.ui.util.Notify; @@ -30,7 +32,7 @@ import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; import org.sufficientlysecure.keychain.ui.util.Notify.Showable; import org.sufficientlysecure.keychain.ui.util.Notify.Style; -public class ImportKeyResult extends OperationResult { +public class ImportKeyResult extends InputPendingResult { public final int mNewKeys, mUpdatedKeys, mBadKeys, mSecret; public final long[] mImportedMasterKeyIds; @@ -80,7 +82,7 @@ public class ImportKeyResult extends OperationResult { } public ImportKeyResult(int result, OperationLog log) { - this(result, log, 0, 0, 0, 0, new long[] { }); + this(result, log, 0, 0, 0, 0, new long[]{}); } public ImportKeyResult(int result, OperationLog log, @@ -94,6 +96,17 @@ public class ImportKeyResult extends OperationResult { mImportedMasterKeyIds = importedMasterKeyIds; } + public ImportKeyResult(OperationLog log, RequiredInputParcel requiredInputParcel, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInputParcel, cryptoInputParcel); + // just assign default values, we won't use them anyway + mNewKeys = 0; + mUpdatedKeys = 0; + mBadKeys = 0; + mSecret = 0; + mImportedMasterKeyIds = new long[]{}; + } + @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); @@ -190,7 +203,7 @@ public class ImportKeyResult extends OperationResult { intent.putExtra(LogDisplayFragment.EXTRA_RESULT, ImportKeyResult.this); activity.startActivity(intent); } - }, R.string.view_log); + }, R.string.snackbar_details); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java index 0b7aa6d03..d767382ae 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java @@ -18,10 +18,9 @@ package org.sufficientlysecure.keychain.operations.results; -import java.util.ArrayList; - import android.os.Parcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; public class InputPendingResult extends OperationResult { @@ -30,26 +29,33 @@ public class InputPendingResult extends OperationResult { public static final int RESULT_PENDING = RESULT_ERROR + 8; final RequiredInputParcel mRequiredInput; + // in case operation needs to add to/changes the cryptoInputParcel sent to it + public final CryptoInputParcel mCryptoInputParcel; public InputPendingResult(int result, OperationLog log) { super(result, log); mRequiredInput = null; + mCryptoInputParcel = null; } - public InputPendingResult(OperationLog log, RequiredInputParcel requiredInput) { + public InputPendingResult(OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { super(RESULT_PENDING, log); mRequiredInput = requiredInput; + mCryptoInputParcel = cryptoInputParcel; } public InputPendingResult(Parcel source) { super(source); mRequiredInput = source.readParcelable(getClass().getClassLoader()); + mCryptoInputParcel = source.readParcelable(getClass().getClassLoader()); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeParcelable(mRequiredInput, 0); + dest.writeParcelable(mCryptoInputParcel, 0); } public boolean isPending() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/KeybaseVerificationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/KeybaseVerificationResult.java new file mode 100644 index 000000000..84648d32c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/KeybaseVerificationResult.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.results; + +import android.os.Parcel; +import android.os.Parcelable; +import com.textuality.keybase.lib.KeybaseException; +import com.textuality.keybase.lib.prover.Prover; + +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; + +public class KeybaseVerificationResult extends InputPendingResult { + public final String mProofUrl; + public final String mPresenceUrl; + public final String mPresenceLabel; + + public KeybaseVerificationResult(int result, OperationLog log) { + super(result, log); + mProofUrl = null; + mPresenceLabel = null; + mPresenceUrl = null; + } + + public KeybaseVerificationResult(int result, OperationLog log, Prover prover) + throws KeybaseException { + super(result, log); + mProofUrl = prover.getProofUrl(); + mPresenceUrl = prover.getPresenceUrl(); + mPresenceLabel = prover.getPresenceLabel(); + } + + public KeybaseVerificationResult(OperationLog log, RequiredInputParcel requiredInputParcel, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInputParcel, cryptoInputParcel); + mProofUrl = null; + mPresenceUrl = null; + mPresenceLabel = null; + } + + protected KeybaseVerificationResult(Parcel in) { + super(in); + mProofUrl = in.readString(); + mPresenceUrl = in.readString(); + mPresenceLabel = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(mProofUrl); + dest.writeString(mPresenceUrl); + dest.writeString(mPresenceLabel); + } + + public static final Parcelable.Creator<KeybaseVerificationResult> CREATOR = new Parcelable.Creator<KeybaseVerificationResult>() { + @Override + public KeybaseVerificationResult createFromParcel(Parcel in) { + return new KeybaseVerificationResult(in); + } + + @Override + public KeybaseVerificationResult[] newArray(int size) { + return new KeybaseVerificationResult[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java index c93db5c39..f213b1aad 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java @@ -22,6 +22,7 @@ import android.app.Activity; import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; @@ -101,9 +102,9 @@ public abstract class OperationResult implements Parcelable { } public OperationLog getLog() { - // If there is only a single entry, and it's a compound one, return that log - if (mLog.isSingleCompound()) { - return ((SubLogEntryParcel) mLog.getFirst()).getSubResult().getLog(); + SubLogEntryParcel singleSubLog = mLog.getSubResultIfSingle(); + if (singleSubLog != null) { + return singleSubLog.getSubResult().getLog(); } // Otherwse, return our regular log return mLog; @@ -169,9 +170,9 @@ public abstract class OperationResult implements Parcelable { public static class SubLogEntryParcel extends LogEntryParcel { - OperationResult mSubResult; + @NonNull OperationResult mSubResult; - public SubLogEntryParcel(OperationResult subResult, LogType type, int indent, Object... parameters) { + public SubLogEntryParcel(@NonNull OperationResult subResult, LogType type, int indent, Object... parameters) { super(type, indent, parameters); mSubResult = subResult; @@ -209,6 +210,10 @@ public abstract class OperationResult implements Parcelable { String logText; LogEntryParcel entryParcel = mLog.getLast(); + if (entryParcel == null) { + Log.e(Constants.TAG, "Tried to show empty log!"); + return Notify.create(activity, R.string.error_empty_log, Style.ERROR); + } // special case: first parameter may be a quantity if (entryParcel.mParameters != null && entryParcel.mParameters.length > 0 && entryParcel.mParameters[0] instanceof Integer) { @@ -248,7 +253,7 @@ public abstract class OperationResult implements Parcelable { intent.putExtra(LogDisplayFragment.EXTRA_RESULT, OperationResult.this); activity.startActivity(intent); } - }, R.string.view_log); + }, R.string.snackbar_details); } @@ -269,7 +274,7 @@ public abstract class OperationResult implements Parcelable { * mark. * */ - public static enum LogType { + public enum LogType { MSG_INTERNAL_ERROR (LogLevel.ERROR, R.string.msg_internal_error), MSG_OPERATION_CANCELLED (LogLevel.CANCELLED, R.string.msg_cancelled), @@ -401,6 +406,7 @@ public abstract class OperationResult implements Parcelable { MSG_KC_SUB_BAD_LOCAL(LogLevel.WARN, R.string.msg_kc_sub_bad_local), MSG_KC_SUB_BAD_KEYID(LogLevel.WARN, R.string.msg_kc_sub_bad_keyid), MSG_KC_SUB_BAD_TIME(LogLevel.WARN, R.string.msg_kc_sub_bad_time), + MSG_KC_SUB_BAD_TIME_EARLY(LogLevel.WARN, R.string.msg_kc_sub_bad_time_early), MSG_KC_SUB_BAD_TYPE(LogLevel.WARN, R.string.msg_kc_sub_bad_type), MSG_KC_SUB_DUP (LogLevel.DEBUG, R.string.msg_kc_sub_dup), MSG_KC_SUB_PRIMARY_BAD(LogLevel.WARN, R.string.msg_kc_sub_primary_bad), @@ -476,6 +482,7 @@ public abstract class OperationResult implements Parcelable { // secret key modify MSG_MF (LogLevel.START, R.string.msg_mr), MSG_MF_DIVERT (LogLevel.DEBUG, R.string.msg_mf_divert), + MSG_MF_ERROR_DIVERT_NEWSUB (LogLevel.ERROR, R.string.msg_mf_error_divert_newsub), MSG_MF_ERROR_DIVERT_SERIAL (LogLevel.ERROR, R.string.msg_mf_error_divert_serial), MSG_MF_ERROR_ENCODE (LogLevel.ERROR, R.string.msg_mf_error_encode), MSG_MF_ERROR_FINGERPRINT (LogLevel.ERROR, R.string.msg_mf_error_fingerprint), @@ -493,11 +500,20 @@ public abstract class OperationResult implements Parcelable { MSG_MF_ERROR_RESTRICTED(LogLevel.ERROR, R.string.msg_mf_error_restricted), MSG_MF_ERROR_REVOKED_PRIMARY (LogLevel.ERROR, R.string.msg_mf_error_revoked_primary), MSG_MF_ERROR_SIG (LogLevel.ERROR, R.string.msg_mf_error_sig), + MSG_MF_ERROR_SUB_STRIPPED(LogLevel.ERROR, R.string.msg_mf_error_sub_stripped), MSG_MF_ERROR_SUBKEY_MISSING(LogLevel.ERROR, R.string.msg_mf_error_subkey_missing), + MSG_MF_ERROR_CONFLICTING_NFC_COMMANDS(LogLevel.ERROR, R.string.msg_mf_error_conflicting_nfc_commands), + MSG_MF_ERROR_DUPLICATE_KEYTOCARD_FOR_SLOT(LogLevel.ERROR, R.string.msg_mf_error_duplicate_keytocard_for_slot), + MSG_MF_ERROR_INVALID_FLAGS_FOR_KEYTOCARD(LogLevel.ERROR, R.string.msg_mf_error_invalid_flags_for_keytocard), + MSG_MF_ERROR_BAD_NFC_ALGO(LogLevel.ERROR, R.string.edit_key_error_bad_nfc_algo), + MSG_MF_ERROR_BAD_NFC_SIZE(LogLevel.ERROR, R.string.edit_key_error_bad_nfc_size), + MSG_MF_ERROR_BAD_NFC_STRIPPED(LogLevel.ERROR, R.string.edit_key_error_bad_nfc_stripped), MSG_MF_MASTER (LogLevel.DEBUG, R.string.msg_mf_master), MSG_MF_NOTATION_PIN (LogLevel.DEBUG, R.string.msg_mf_notation_pin), MSG_MF_NOTATION_EMPTY (LogLevel.DEBUG, R.string.msg_mf_notation_empty), MSG_MF_PASSPHRASE (LogLevel.INFO, R.string.msg_mf_passphrase), + MSG_MF_PIN (LogLevel.INFO, R.string.msg_mf_pin), + MSG_MF_ADMIN_PIN (LogLevel.INFO, R.string.msg_mf_admin_pin), MSG_MF_PASSPHRASE_KEY (LogLevel.DEBUG, R.string.msg_mf_passphrase_key), MSG_MF_PASSPHRASE_EMPTY_RETRY (LogLevel.DEBUG, R.string.msg_mf_passphrase_empty_retry), MSG_MF_PASSPHRASE_FAIL (LogLevel.WARN, R.string.msg_mf_passphrase_fail), @@ -511,6 +527,8 @@ public abstract class OperationResult implements Parcelable { MSG_MF_SUBKEY_NEW (LogLevel.INFO, R.string.msg_mf_subkey_new), MSG_MF_SUBKEY_REVOKE (LogLevel.INFO, R.string.msg_mf_subkey_revoke), MSG_MF_SUBKEY_STRIP (LogLevel.INFO, R.string.msg_mf_subkey_strip), + MSG_MF_KEYTOCARD_START (LogLevel.INFO, R.string.msg_mf_keytocard_start), + MSG_MF_KEYTOCARD_FINISH (LogLevel.OK, R.string.msg_mf_keytocard_finish), MSG_MF_SUCCESS (LogLevel.OK, R.string.msg_mf_success), MSG_MF_UID_ADD (LogLevel.INFO, R.string.msg_mf_uid_add), MSG_MF_UID_PRIMARY (LogLevel.INFO, R.string.msg_mf_uid_primary), @@ -553,13 +571,18 @@ public abstract class OperationResult implements Parcelable { MSG_ED_CACHING_NEW (LogLevel.DEBUG, R.string.msg_ed_caching_new), MSG_ED_ERROR_NO_PARCEL (LogLevel.ERROR, R.string.msg_ed_error_no_parcel), MSG_ED_ERROR_KEY_NOT_FOUND (LogLevel.ERROR, R.string.msg_ed_error_key_not_found), + MSG_ED_ERROR_EXTRACTING_PUBLIC_UPLOAD (LogLevel.ERROR, + R.string.msg_ed_error_extract_public_upload), MSG_ED_FETCHING (LogLevel.DEBUG, R.string.msg_ed_fetching), MSG_ED_SUCCESS (LogLevel.OK, R.string.msg_ed_success), // promote key MSG_PR (LogLevel.START, R.string.msg_pr), + MSG_PR_ALL (LogLevel.DEBUG, R.string.msg_pr_all), MSG_PR_ERROR_KEY_NOT_FOUND (LogLevel.ERROR, R.string.msg_pr_error_key_not_found), MSG_PR_FETCHING (LogLevel.DEBUG, R.string.msg_pr_fetching), + MSG_PR_SUBKEY_MATCH (LogLevel.DEBUG, R.string.msg_pr_subkey_match), + MSG_PR_SUBKEY_NOMATCH (LogLevel.WARN, R.string.msg_pr_subkey_nomatch), MSG_PR_SUCCESS (LogLevel.OK, R.string.msg_pr_success), // messages used in UI code @@ -584,15 +607,16 @@ public abstract class OperationResult implements Parcelable { MSG_DC_CLEAR_SIGNATURE_OK (LogLevel.OK, R.string.msg_dc_clear_signature_ok), MSG_DC_CLEAR_SIGNATURE (LogLevel.DEBUG, R.string.msg_dc_clear_signature), MSG_DC_ERROR_BAD_PASSPHRASE (LogLevel.ERROR, R.string.msg_dc_error_bad_passphrase), + MSG_DC_ERROR_SYM_PASSPHRASE (LogLevel.ERROR, R.string.msg_dc_error_sym_passphrase), + MSG_DC_ERROR_CORRUPT_DATA (LogLevel.ERROR, R.string.msg_dc_error_corrupt_data), MSG_DC_ERROR_EXTRACT_KEY (LogLevel.ERROR, R.string.msg_dc_error_extract_key), MSG_DC_ERROR_INTEGRITY_CHECK (LogLevel.ERROR, R.string.msg_dc_error_integrity_check), - MSG_DC_ERROR_INTEGRITY_MISSING (LogLevel.ERROR, R.string.msg_dc_error_integrity_missing), - MSG_DC_ERROR_INVALID_SIGLIST(LogLevel.ERROR, R.string.msg_dc_error_invalid_siglist), + MSG_DC_ERROR_INVALID_DATA (LogLevel.ERROR, R.string.msg_dc_error_invalid_data), MSG_DC_ERROR_IO (LogLevel.ERROR, R.string.msg_dc_error_io), + MSG_DC_ERROR_INPUT (LogLevel.ERROR, R.string.msg_dc_error_input), MSG_DC_ERROR_NO_DATA (LogLevel.ERROR, R.string.msg_dc_error_no_data), MSG_DC_ERROR_NO_KEY (LogLevel.ERROR, R.string.msg_dc_error_no_key), MSG_DC_ERROR_PGP_EXCEPTION (LogLevel.ERROR, R.string.msg_dc_error_pgp_exception), - MSG_DC_ERROR_UNSUPPORTED_HASH_ALGO (LogLevel.ERROR, R.string.msg_dc_error_unsupported_hash_algo), MSG_DC_INTEGRITY_CHECK_OK (LogLevel.INFO, R.string.msg_dc_integrity_check_ok), MSG_DC_OK_META_ONLY (LogLevel.OK, R.string.msg_dc_ok_meta_only), MSG_DC_OK (LogLevel.OK, R.string.msg_dc_ok), @@ -607,7 +631,10 @@ public abstract class OperationResult implements Parcelable { MSG_DC_TRAIL_SYM (LogLevel.DEBUG, R.string.msg_dc_trail_sym), MSG_DC_TRAIL_UNKNOWN (LogLevel.DEBUG, R.string.msg_dc_trail_unknown), MSG_DC_UNLOCKING (LogLevel.INFO, R.string.msg_dc_unlocking), - MSG_DC_OLD_SYMMETRIC_ENCRYPTION_ALGO (LogLevel.WARN, R.string.msg_dc_old_symmetric_encryption_algo), + MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO(LogLevel.WARN, R.string.msg_dc_insecure_symmetric_encryption_algo), + MSG_DC_INSECURE_HASH_ALGO(LogLevel.ERROR, R.string.msg_dc_insecure_hash_algo), + MSG_DC_INSECURE_MDC_MISSING(LogLevel.ERROR, R.string.msg_dc_insecure_mdc_missing), + MSG_DC_INSECURE_KEY(LogLevel.ERROR, R.string.msg_dc_insecure_key), // verify signed literal data MSG_VL (LogLevel.INFO, R.string.msg_vl), @@ -634,7 +661,6 @@ public abstract class OperationResult implements Parcelable { MSG_PSE_COMPRESSING (LogLevel.DEBUG, R.string.msg_pse_compressing), MSG_PSE_ENCRYPTING (LogLevel.DEBUG, R.string.msg_pse_encrypting), MSG_PSE_ERROR_BAD_PASSPHRASE (LogLevel.ERROR, R.string.msg_pse_error_bad_passphrase), - MSG_PSE_ERROR_HASH_ALGO (LogLevel.ERROR, R.string.msg_pse_error_hash_algo), MSG_PSE_ERROR_IO (LogLevel.ERROR, R.string.msg_pse_error_io), MSG_PSE_ERROR_SIGN_KEY(LogLevel.ERROR, R.string.msg_pse_error_sign_key), MSG_PSE_ERROR_KEY_SIGN (LogLevel.ERROR, R.string.msg_pse_error_key_sign), @@ -672,6 +698,7 @@ public abstract class OperationResult implements Parcelable { MSG_CRT_WARN_NOT_FOUND (LogLevel.WARN, R.string.msg_crt_warn_not_found), MSG_CRT_WARN_CERT_FAILED (LogLevel.WARN, R.string.msg_crt_warn_cert_failed), MSG_CRT_WARN_SAVE_FAILED (LogLevel.WARN, R.string.msg_crt_warn_save_failed), + MSG_CRT_WARN_UPLOAD_FAILED (LogLevel.WARN, R.string.msg_crt_warn_upload_failed), MSG_IMPORT (LogLevel.START, R.plurals.msg_import), @@ -683,6 +710,7 @@ public abstract class OperationResult implements Parcelable { MSG_IMPORT_FETCH_KEYBASE (LogLevel.INFO, R.string.msg_import_fetch_keybase), MSG_IMPORT_KEYSERVER (LogLevel.DEBUG, R.string.msg_import_keyserver), MSG_IMPORT_MERGE (LogLevel.DEBUG, R.string.msg_import_merge), + MSG_IMPORT_MERGE_ERROR (LogLevel.ERROR, R.string.msg_import_merge_error), MSG_IMPORT_FINGERPRINT_ERROR (LogLevel.ERROR, R.string.msg_import_fingerprint_error), MSG_IMPORT_FINGERPRINT_OK (LogLevel.DEBUG, R.string.msg_import_fingerprint_ok), MSG_IMPORT_ERROR (LogLevel.ERROR, R.string.msg_import_error), @@ -691,6 +719,8 @@ public abstract class OperationResult implements Parcelable { MSG_IMPORT_SUCCESS (LogLevel.OK, R.string.msg_import_success), MSG_EXPORT (LogLevel.START, R.plurals.msg_export), + MSG_EXPORT_FILE_NAME (LogLevel.INFO, R.string.msg_export_file_name), + MSG_EXPORT_UPLOAD_PUBLIC (LogLevel.START, R.string.msg_export_upload_public), MSG_EXPORT_PUBLIC (LogLevel.DEBUG, R.string.msg_export_public), MSG_EXPORT_SECRET (LogLevel.DEBUG, R.string.msg_export_secret), MSG_EXPORT_ALL (LogLevel.START, R.string.msg_export_all), @@ -702,13 +732,16 @@ public abstract class OperationResult implements Parcelable { MSG_EXPORT_ERROR_DB (LogLevel.ERROR, R.string.msg_export_error_db), MSG_EXPORT_ERROR_IO (LogLevel.ERROR, R.string.msg_export_error_io), MSG_EXPORT_ERROR_KEY (LogLevel.ERROR, R.string.msg_export_error_key), + MSG_EXPORT_ERROR_UPLOAD (LogLevel.ERROR, R.string.msg_export_error_upload), MSG_EXPORT_SUCCESS (LogLevel.OK, R.string.msg_export_success), + MSG_EXPORT_UPLOAD_SUCCESS (LogLevel.OK, R.string.msg_export_upload_success), MSG_CRT_UPLOAD_SUCCESS (LogLevel.OK, R.string.msg_crt_upload_success), MSG_ACC_SAVED (LogLevel.INFO, R.string.api_settings_save_msg), - MSG_WRONG_QR_CODE (LogLevel.INFO, R.string.import_qr_code_wrong), + MSG_WRONG_QR_CODE (LogLevel.ERROR, R.string.import_qr_code_wrong), + MSG_WRONG_QR_CODE_FP(LogLevel.ERROR, R.string.import_qr_code_fp), MSG_NO_VALID_ENC (LogLevel.ERROR, R.string.error_invalid_data), @@ -722,7 +755,7 @@ public abstract class OperationResult implements Parcelable { MSG_GET_QUERY_FAILED(LogLevel.ERROR, R.string.msg_download_query_failed), MSG_DEL_ERROR_EMPTY (LogLevel.ERROR, R.string.msg_del_error_empty), - MSG_DEL_ERROR_MULTI_SECRET (LogLevel.DEBUG, R.string.msg_del_error_multi_secret), + MSG_DEL_ERROR_MULTI_SECRET (LogLevel.ERROR, R.string.msg_del_error_multi_secret), MSG_DEL (LogLevel.START, R.plurals.msg_del), MSG_DEL_KEY (LogLevel.DEBUG, R.string.msg_del_key), MSG_DEL_KEY_FAIL (LogLevel.WARN, R.string.msg_del_key_fail), @@ -730,6 +763,25 @@ public abstract class OperationResult implements Parcelable { MSG_DEL_OK (LogLevel.OK, R.plurals.msg_del_ok), MSG_DEL_FAIL (LogLevel.WARN, R.plurals.msg_del_fail), + MSG_REVOKE_ERROR_EMPTY (LogLevel.ERROR, R.string.msg_revoke_error_empty), + MSG_REVOKE_ERROR_NOT_FOUND (LogLevel.ERROR, R.string.msg_revoke_error_not_found), + MSG_REVOKE (LogLevel.DEBUG, R.string.msg_revoke_key), + MSG_REVOKE_ERROR_KEY_FAIL (LogLevel.ERROR, R.string.msg_revoke_key_fail), + MSG_REVOKE_OK (LogLevel.OK, R.string.msg_revoke_ok), + + // keybase verification + MSG_KEYBASE_VERIFICATION(LogLevel.START, R.string.msg_keybase_verification), + + MSG_KEYBASE_ERROR_NO_PROVER(LogLevel.ERROR, R.string.msg_keybase_error_no_prover), + MSG_KEYBASE_ERROR_FETCH_PROOF(LogLevel.ERROR, R.string.msg_keybase_error_fetching_evidence), + MSG_KEYBASE_ERROR_FINGERPRINT_MISMATCH(LogLevel.ERROR, + R.string.msg_keybase_error_key_mismatch), + MSG_KEYBASE_ERROR_DNS_FAIL(LogLevel.ERROR, R.string.msg_keybase_error_dns_fail), + MSG_KEYBASE_ERROR_SPECIFIC(LogLevel.ERROR, R.string.msg_keybase_error_specific), + MSG_KEYBASE_ERROR_PAYLOAD_MISMATCH(LogLevel.ERROR, + R.string.msg_keybase_error_msg_payload_mismatch), + + // export log MSG_LV (LogLevel.START, R.string.msg_lv), MSG_LV_MATCH (LogLevel.DEBUG, R.string.msg_lv_match), MSG_LV_MATCH_ERROR (LogLevel.ERROR, R.string.msg_lv_match_error), @@ -770,7 +822,7 @@ public abstract class OperationResult implements Parcelable { } /** Enumeration of possible log levels. */ - public static enum LogLevel { + public enum LogLevel { DEBUG, INFO, WARN, @@ -810,8 +862,15 @@ public abstract class OperationResult implements Parcelable { mParcels.add(new SubLogEntryParcel(subResult, subLog.getFirst().mType, indent, subLog.getFirst().mParameters)); } - boolean isSingleCompound() { - return mParcels.size() == 1 && getFirst() instanceof SubLogEntryParcel; + public SubLogEntryParcel getSubResultIfSingle() { + if (mParcels.size() != 1) { + return null; + } + LogEntryParcel first = getFirst(); + if (first instanceof SubLogEntryParcel) { + return (SubLogEntryParcel) first; + } + return null; } public void clear() { @@ -859,7 +918,11 @@ public abstract class OperationResult implements Parcelable { if (mParcels.isEmpty()) { return null; } - return mParcels.get(mParcels.size() -1); + LogEntryParcel last = mParcels.get(mParcels.size() -1); + if (last instanceof SubLogEntryParcel) { + return ((SubLogEntryParcel) last).getSubResult().getLog().getLast(); + } + return last; } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpEditKeyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpEditKeyResult.java index 38edbf6ee..30307ba46 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpEditKeyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpEditKeyResult.java @@ -22,6 +22,7 @@ import android.os.Parcel; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; @@ -37,8 +38,9 @@ public class PgpEditKeyResult extends InputPendingResult { mRingMasterKeyId = ring != null ? ring.getMasterKeyId() : Constants.key.none; } - public PgpEditKeyResult(OperationLog log, RequiredInputParcel requiredInput) { - super(log, requiredInput); + public PgpEditKeyResult(OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); mRingMasterKeyId = Constants.key.none; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpSignEncryptResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpSignEncryptResult.java index acb265462..2b33b8ace 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpSignEncryptResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpSignEncryptResult.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.operations.results; import android.os.Parcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; @@ -38,8 +39,9 @@ public class PgpSignEncryptResult extends InputPendingResult { super(result, log); } - public PgpSignEncryptResult(OperationLog log, RequiredInputParcel requiredInput) { - super(log, requiredInput); + public PgpSignEncryptResult(OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); } public PgpSignEncryptResult(Parcel source) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/RevokeResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/RevokeResult.java new file mode 100644 index 000000000..b737f6e50 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/RevokeResult.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.results; + +import android.app.Activity; +import android.content.Intent; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import org.sufficientlysecure.keychain.ui.LogDisplayActivity; +import org.sufficientlysecure.keychain.ui.LogDisplayFragment; +import org.sufficientlysecure.keychain.ui.util.Notify; + +public class RevokeResult extends InputPendingResult { + + public final long mMasterKeyId; + + public RevokeResult(int result, OperationLog log, long masterKeyId) { + super(result, log); + mMasterKeyId = masterKeyId; + } + + /** + * used when more input is required + * + * @param log operation log upto point of required input, if any + * @param requiredInput represents input required + */ + @SuppressWarnings("unused") // standard pattern across all results, we might need it later + public RevokeResult(@Nullable OperationLog log, RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); + // we won't use these values + mMasterKeyId = -1; + } + + /** Construct from a parcel - trivial because we have no extra data. */ + public RevokeResult(Parcel source) { + super(source); + mMasterKeyId = source.readLong(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(mMasterKeyId); + } + + public static final Parcelable.Creator<RevokeResult> CREATOR = new Parcelable.Creator<RevokeResult>() { + @Override + public RevokeResult createFromParcel(Parcel in) { + return new RevokeResult(in); + } + + @Override + public RevokeResult[] newArray(int size) { + return new RevokeResult[size]; + } + }; + + @Override + public Notify.Showable createNotify(final Activity activity) { + + int resultType = getResult(); + + String str; + int duration; + Notify.Style style; + + // Not an overall failure + if ((resultType & OperationResult.RESULT_ERROR) == 0) { + + duration = Notify.LENGTH_LONG; + + // New and updated keys + if (resultType == OperationResult.RESULT_OK) { + style = Notify.Style.OK; + str = activity.getString(R.string.revoke_ok); + } else { + duration = 0; + style = Notify.Style.ERROR; + str = "internal error"; + } + + } else { + duration = 0; + style = Notify.Style.ERROR; + str = activity.getString(R.string.revoke_fail); + } + + return Notify.create(activity, str, duration, style, new Notify.ActionListener() { + @Override + public void onAction() { + Intent intent = new Intent( + activity, LogDisplayActivity.class); + intent.putExtra(LogDisplayFragment.EXTRA_RESULT, RevokeResult.this); + activity.startActivity(intent); + } + }, R.string.snackbar_details); + + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/SignEncryptResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/SignEncryptResult.java index b05921b0d..0e0c5d598 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/SignEncryptResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/SignEncryptResult.java @@ -19,18 +19,20 @@ package org.sufficientlysecure.keychain.operations.results; import android.os.Parcel; -import java.util.ArrayList; - +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import java.util.ArrayList; public class SignEncryptResult extends InputPendingResult { ArrayList<PgpSignEncryptResult> mResults; byte[] mResultBytes; - public SignEncryptResult(OperationLog log, RequiredInputParcel requiredInput, ArrayList<PgpSignEncryptResult> results) { - super(log, requiredInput); + public SignEncryptResult(OperationLog log, RequiredInputParcel requiredInput, + ArrayList<PgpSignEncryptResult> results, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInput, cryptoInputParcel); mResults = results; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java index 4adacaf23..770e8de91 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.pgp; import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPPublicKey; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.util.IterableIterator; @@ -26,6 +27,9 @@ import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; +import java.util.Set; + /** A generic wrapped PGPKeyRing object. * @@ -90,6 +94,16 @@ public abstract class CanonicalizedKeyRing extends KeyRing { return getRing().getPublicKey().isEncryptionKey(); } + public Set<Long> getEncryptIds() { + HashSet<Long> result = new HashSet<>(); + for(CanonicalizedPublicKey key : publicKeyIterator()) { + if (key.canEncrypt() && key.isValid()) { + result.add(key.getKeyId()); + } + } + return result; + } + public long getEncryptId() throws PgpKeyNotFoundException { for(CanonicalizedPublicKey key : publicKeyIterator()) { if (key.canEncrypt() && key.isValid()) { @@ -127,7 +141,11 @@ public abstract class CanonicalizedKeyRing extends KeyRing { } public CanonicalizedPublicKey getPublicKey(long id) { - return new CanonicalizedPublicKey(this, getRing().getPublicKey(id)); + PGPPublicKey pubKey = getRing().getPublicKey(id); + if (pubKey == null) { + return null; + } + return new CanonicalizedPublicKey(this, pubKey); } public byte[] getEncoded() throws IOException { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKeyRing.java index 8432b8f9f..be5f21f23 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKeyRing.java @@ -18,10 +18,10 @@ package org.sufficientlysecure.keychain.pgp; -import org.spongycastle.bcpg.S2K; import org.spongycastle.openpgp.PGPObjectFactory; import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKey; import org.spongycastle.openpgp.PGPSecretKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.util.IterableIterator; @@ -62,19 +62,6 @@ public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing { return mRing; } - /** Getter that returns the subkey that should be used for signing. */ - CanonicalizedPublicKey getEncryptionSubKey() throws PgpKeyNotFoundException { - PGPPublicKey key = getRing().getPublicKey(getEncryptId()); - if(key != null) { - CanonicalizedPublicKey cKey = new CanonicalizedPublicKey(this, key); - if(!cKey.canEncrypt()) { - throw new PgpKeyNotFoundException("key error"); - } - return cKey; - } - throw new PgpKeyNotFoundException("no encryption key available"); - } - public IterableIterator<CanonicalizedPublicKey> publicKeyIterator() { @SuppressWarnings("unchecked") final Iterator<PGPPublicKey> it = getRing().getPublicKeys(); @@ -97,15 +84,25 @@ public class CanonicalizedPublicKeyRing extends CanonicalizedKeyRing { } /** Create a dummy secret ring from this key */ - public UncachedKeyRing createDummySecretRing () { - PGPSecretKeyRing secRing = PGPSecretKeyRing.constructDummyFromPublic(getRing(), null); - return new UncachedKeyRing(secRing); - } - - /** Create a dummy secret ring from this key */ - public UncachedKeyRing createDivertSecretRing (byte[] cardAid) { + public UncachedKeyRing createDivertSecretRing (byte[] cardAid, long[] subKeyIds) { PGPSecretKeyRing secRing = PGPSecretKeyRing.constructDummyFromPublic(getRing(), cardAid); - return new UncachedKeyRing(secRing); + + if (subKeyIds == null) { + return new UncachedKeyRing(secRing); + } + + // if only specific subkeys should be promoted, construct a + // stripped dummy, then move divert-to-card keys over + PGPSecretKeyRing newRing = PGPSecretKeyRing.constructDummyFromPublic(getRing()); + for (long subKeyId : subKeyIds) { + PGPSecretKey key = secRing.getSecretKey(subKeyId); + if (key != null) { + newRing = PGPSecretKeyRing.insertSecretKey(newRing, key); + } + } + + return new UncachedKeyRing(newRing); + } }
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java index 39d0a2f1d..7394c07c3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedSecretKey.java @@ -21,22 +21,18 @@ package org.sufficientlysecure.keychain.pgp; import org.spongycastle.bcpg.S2K; import org.spongycastle.openpgp.PGPException; import org.spongycastle.openpgp.PGPPrivateKey; -import org.spongycastle.openpgp.PGPPublicKey; -import org.spongycastle.openpgp.PGPPublicKeyRing; import org.spongycastle.openpgp.PGPSecretKey; import org.spongycastle.openpgp.PGPSignature; import org.spongycastle.openpgp.PGPSignatureGenerator; import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.spongycastle.openpgp.PGPSignatureSubpacketVector; -import org.spongycastle.openpgp.PGPUserAttributeSubpacketVector; import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor; import org.spongycastle.openpgp.operator.PGPContentSignerBuilder; -import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.spongycastle.openpgp.operator.jcajce.CachingDataDecryptorFactory; import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyConverter; import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder; -import org.spongycastle.openpgp.operator.jcajce.NfcSyncPublicKeyDataDecryptorFactoryBuilder; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; @@ -45,10 +41,10 @@ import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; import java.nio.ByteBuffer; -import java.util.ArrayList; +import java.security.PrivateKey; +import java.security.interfaces.RSAPrivateCrtKey; import java.util.Date; import java.util.HashMap; -import java.util.List; import java.util.Map; @@ -69,9 +65,9 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { private PGPPrivateKey mPrivateKey = null; private int mPrivateKeyState = PRIVATE_KEY_STATE_LOCKED; - private static int PRIVATE_KEY_STATE_LOCKED = 0; - private static int PRIVATE_KEY_STATE_UNLOCKED = 1; - private static int PRIVATE_KEY_STATE_DIVERT_TO_CARD = 2; + final private static int PRIVATE_KEY_STATE_LOCKED = 0; + final private static int PRIVATE_KEY_STATE_UNLOCKED = 1; + final private static int PRIVATE_KEY_STATE_DIVERT_TO_CARD = 2; CanonicalizedSecretKey(CanonicalizedSecretKeyRing ring, PGPSecretKey key) { super(ring, key.getPublicKey()); @@ -123,9 +119,10 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { } public SecretKeyType getSecretKeyType() { - if (mSecretKey.getS2K() != null && mSecretKey.getS2K().getType() == S2K.GNU_DUMMY_S2K) { + S2K s2k = mSecretKey.getS2K(); + if (s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K) { // divert to card is special - if (mSecretKey.getS2K().getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + if (s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { return SecretKeyType.DIVERT_TO_CARD; } // no matter the exact protection mode, it's some kind of dummy key @@ -156,9 +153,10 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { */ public boolean unlock(Passphrase passphrase) throws PgpGeneralException { // handle keys on OpenPGP cards like they were unlocked - if (mSecretKey.getS2K() != null - && mSecretKey.getS2K().getType() == S2K.GNU_DUMMY_S2K - && mSecretKey.getS2K().getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + S2K s2k = mSecretKey.getS2K(); + if (s2k != null + && s2k.getType() == S2K.GNU_DUMMY_S2K + && s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { mPrivateKeyState = PRIVATE_KEY_STATE_DIVERT_TO_CARD; return true; } @@ -178,16 +176,6 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { return true; } - /** - * Returns a list of all supported hash algorithms. - */ - public ArrayList<Integer> getSupportedHashAlgorithms() { - // TODO: intersection between preferred hash algos of this key and PgpConstants.PREFERRED_HASH_ALGORITHMS - // choose best algo - - return PgpConstants.sPreferredHashAlgorithms; - } - private PGPContentSignerBuilder getContentSignerBuilder(int hashAlgo, Map<ByteBuffer,byte[]> signedHashes) { if (mPrivateKeyState == PRIVATE_KEY_STATE_DIVERT_TO_CARD) { @@ -206,7 +194,7 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { public PGPSignatureGenerator getCertSignatureGenerator(Map<ByteBuffer, byte[]> signedHashes) { PGPContentSignerBuilder contentSignerBuilder = getContentSignerBuilder( - PgpConstants.CERTIFY_HASH_ALGO, signedHashes); + PgpSecurityConstants.CERTIFY_HASH_ALGO, signedHashes); if (mPrivateKeyState == PRIVATE_KEY_STATE_LOCKED) { throw new PrivateKeyNotUnlockedException(); @@ -265,20 +253,42 @@ public class CanonicalizedSecretKey extends CanonicalizedPublicKey { } } - public PublicKeyDataDecryptorFactory getDecryptorFactory(CryptoInputParcel cryptoInput) { + public CachingDataDecryptorFactory getCachingDecryptorFactory(CryptoInputParcel cryptoInput) { if (mPrivateKeyState == PRIVATE_KEY_STATE_LOCKED) { throw new PrivateKeyNotUnlockedException(); } if (mPrivateKeyState == PRIVATE_KEY_STATE_DIVERT_TO_CARD) { - return new NfcSyncPublicKeyDataDecryptorFactoryBuilder() - .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( - cryptoInput.getCryptoData() - ); + return new CachingDataDecryptorFactory( + Constants.BOUNCY_CASTLE_PROVIDER_NAME, + cryptoInput.getCryptoData()); } else { - return new JcePublicKeyDataDecryptorFactoryBuilder() - .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mPrivateKey); + return new CachingDataDecryptorFactory( + new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mPrivateKey), + cryptoInput.getCryptoData()); + } + } + + // For use only in card export; returns the secret key in Chinese Remainder Theorem format. + public RSAPrivateCrtKey getCrtSecretKey() throws PgpGeneralException { + if (mPrivateKeyState == PRIVATE_KEY_STATE_LOCKED) { + throw new PgpGeneralException("Cannot get secret key attributes while key is locked."); } + + if (mPrivateKeyState == PRIVATE_KEY_STATE_DIVERT_TO_CARD) { + throw new PgpGeneralException("Cannot get secret key attributes of divert-to-card key."); + } + + JcaPGPKeyConverter keyConverter = new JcaPGPKeyConverter(); + PrivateKey retVal; + try { + retVal = keyConverter.getPrivateKey(mPrivateKey); + } catch (PGPException e) { + throw new PgpGeneralException("Error converting private key!", e); + } + + return (RSAPrivateCrtKey)retVal; } public byte[] getIv() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java index 825795cc6..77977b691 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java @@ -22,6 +22,7 @@ import android.text.TextUtils; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import java.io.Serializable; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -91,7 +92,7 @@ public abstract class KeyRing { return userIdString; } - public static class UserId { + public static class UserId implements Serializable { public final String name; public final String email; public final String comment; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java new file mode 100644 index 000000000..c4525e5cd --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 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.openintents.openpgp.OpenPgpDecryptionResult; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +public class OpenPgpDecryptionResultBuilder { + + // builder + private boolean mInsecure = false; + private boolean mEncrypted = false; + + public void setInsecure(boolean insecure) { + this.mInsecure = insecure; + } + + public void setEncrypted(boolean encrypted) { + this.mEncrypted = encrypted; + } + + public OpenPgpDecryptionResult build() { + OpenPgpDecryptionResult result = new OpenPgpDecryptionResult(); + + if (mInsecure) { + Log.d(Constants.TAG, "RESULT_INSECURE"); + result.setResult(OpenPgpDecryptionResult.RESULT_INSECURE); + return result; + } + + if (mEncrypted) { + Log.d(Constants.TAG, "RESULT_ENCRYPTED"); + result.setResult(OpenPgpDecryptionResult.RESULT_ENCRYPTED); + } else { + Log.d(Constants.TAG, "RESULT_NOT_ENCRYPTED"); + result.setResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED); + } + + return result; + } + + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java index ed4715681..9d059b58f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java @@ -30,7 +30,6 @@ import java.util.ArrayList; */ public class OpenPgpSignatureResultBuilder { // OpenPgpSignatureResult - private boolean mSignatureOnly = false; private String mPrimaryUserId; private ArrayList<String> mUserIds = new ArrayList<>(); private long mKeyId; @@ -42,10 +41,7 @@ public class OpenPgpSignatureResultBuilder { private boolean mIsSignatureKeyCertified = false; private boolean mIsKeyRevoked = false; private boolean mIsKeyExpired = false; - - public void setSignatureOnly(boolean signatureOnly) { - this.mSignatureOnly = signatureOnly; - } + private boolean mInsecure = false; public void setPrimaryUserId(String userId) { this.mPrimaryUserId = userId; @@ -63,6 +59,10 @@ public class OpenPgpSignatureResultBuilder { this.mValidSignature = validSignature; } + public void setInsecure(boolean insecure) { + this.mInsecure = insecure; + } + public void setSignatureKeyCertified(boolean isSignatureKeyCertified) { this.mIsSignatureKeyCertified = isSignatureKeyCertified; } @@ -87,6 +87,10 @@ public class OpenPgpSignatureResultBuilder { return mValidSignature; } + public boolean isInsecure() { + return mInsecure; + } + public void initValid(CanonicalizedPublicKeyRing signingRing, CanonicalizedPublicKey signingKey) { setSignatureAvailable(true); @@ -109,47 +113,50 @@ public class OpenPgpSignatureResultBuilder { } public OpenPgpSignatureResult build() { - if (mSignatureAvailable) { - OpenPgpSignatureResult result = new OpenPgpSignatureResult(); - result.setSignatureOnly(mSignatureOnly); - - // valid sig! - if (mKnownKey) { - if (mValidSignature) { - result.setKeyId(mKeyId); - result.setPrimaryUserId(mPrimaryUserId); - result.setUserIds(mUserIds); - - if (mIsKeyRevoked) { - Log.d(Constants.TAG, "SIGNATURE_KEY_REVOKED"); - result.setStatus(OpenPgpSignatureResult.SIGNATURE_KEY_REVOKED); - } else if (mIsKeyExpired) { - Log.d(Constants.TAG, "SIGNATURE_KEY_EXPIRED"); - result.setStatus(OpenPgpSignatureResult.SIGNATURE_KEY_EXPIRED); - } else if (mIsSignatureKeyCertified) { - Log.d(Constants.TAG, "SIGNATURE_SUCCESS_CERTIFIED"); - result.setStatus(OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED); - } else { - Log.d(Constants.TAG, "SIGNATURE_SUCCESS_UNCERTIFIED"); - result.setStatus(OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED); - } - } else { - Log.d(Constants.TAG, "Error! Invalid signature."); - result.setStatus(OpenPgpSignatureResult.SIGNATURE_ERROR); - } - } else { - result.setKeyId(mKeyId); - - Log.d(Constants.TAG, "SIGNATURE_KEY_MISSING"); - result.setStatus(OpenPgpSignatureResult.SIGNATURE_KEY_MISSING); - } + OpenPgpSignatureResult result = new OpenPgpSignatureResult(); + if (!mSignatureAvailable) { + Log.d(Constants.TAG, "RESULT_NO_SIGNATURE"); + result.setResult(OpenPgpSignatureResult.RESULT_NO_SIGNATURE); return result; - } else { - Log.d(Constants.TAG, "no signature found!"); + } + + if (!mKnownKey) { + result.setKeyId(mKeyId); - return null; + Log.d(Constants.TAG, "RESULT_KEY_MISSING"); + result.setResult(OpenPgpSignatureResult.RESULT_KEY_MISSING); + return result; + } + + if (!mValidSignature) { + Log.d(Constants.TAG, "RESULT_INVALID_SIGNATURE"); + result.setResult(OpenPgpSignatureResult.RESULT_INVALID_SIGNATURE); + return result; } + + result.setKeyId(mKeyId); + result.setPrimaryUserId(mPrimaryUserId); + result.setUserIds(mUserIds); + + if (mIsKeyRevoked) { + Log.d(Constants.TAG, "RESULT_INVALID_KEY_REVOKED"); + result.setResult(OpenPgpSignatureResult.RESULT_INVALID_KEY_REVOKED); + } else if (mIsKeyExpired) { + Log.d(Constants.TAG, "RESULT_INVALID_KEY_EXPIRED"); + result.setResult(OpenPgpSignatureResult.RESULT_INVALID_KEY_EXPIRED); + } else if (mInsecure) { + Log.d(Constants.TAG, "RESULT_INVALID_INSECURE"); + result.setResult(OpenPgpSignatureResult.RESULT_INVALID_INSECURE); + } else if (mIsSignatureKeyCertified) { + Log.d(Constants.TAG, "RESULT_VALID_CONFIRMED"); + result.setResult(OpenPgpSignatureResult.RESULT_VALID_CONFIRMED); + } else { + Log.d(Constants.TAG, "RESULT_VALID_UNCONFIRMED"); + result.setResult(OpenPgpSignatureResult.RESULT_VALID_UNCONFIRMED); + } + + return result; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConstants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConstants.java deleted file mode 100644 index f739b1e6d..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConstants.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2015 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.bcpg.CompressionAlgorithmTags; -import org.spongycastle.bcpg.HashAlgorithmTags; -import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags; - -import java.util.ArrayList; - -public class PgpConstants { - - public static ArrayList<Integer> sPreferredSymmetricAlgorithms = new ArrayList<>(); - public static ArrayList<Integer> sPreferredHashAlgorithms = new ArrayList<>(); - public static ArrayList<Integer> sPreferredCompressionAlgorithms = new ArrayList<>(); - - // TODO: use hashmaps for contains in O(1) and intersections! - - /* - * Most preferred is first - * These arrays are written as preferred algorithms into the keys on creation. - * Other implementations may choose to honor this selection. - * - * These lists also define the only algorithms which are used in OpenKeychain. - * We do not support algorithms such as MD5 - */ - static { - sPreferredSymmetricAlgorithms.add(SymmetricKeyAlgorithmTags.AES_256); - sPreferredSymmetricAlgorithms.add(SymmetricKeyAlgorithmTags.AES_192); - sPreferredSymmetricAlgorithms.add(SymmetricKeyAlgorithmTags.AES_128); - sPreferredSymmetricAlgorithms.add(SymmetricKeyAlgorithmTags.TWOFISH); - - // NOTE: some implementations do not support SHA512, thus we choose SHA256 as default (Mailvelope?) - sPreferredHashAlgorithms.add(HashAlgorithmTags.SHA256); - sPreferredHashAlgorithms.add(HashAlgorithmTags.SHA512); - sPreferredHashAlgorithms.add(HashAlgorithmTags.SHA384); - sPreferredHashAlgorithms.add(HashAlgorithmTags.SHA224); - sPreferredHashAlgorithms.add(HashAlgorithmTags.SHA1); - sPreferredHashAlgorithms.add(HashAlgorithmTags.RIPEMD160); - - /* - * Prefer ZIP - * "ZLIB provides no benefit over ZIP and is more malleable" - * - (OpenPGP WG mailinglist: "[openpgp] Intent to deprecate: Insecure primitives") - * BZIP2: very slow - */ - sPreferredCompressionAlgorithms.add(CompressionAlgorithmTags.ZIP); - sPreferredCompressionAlgorithms.add(CompressionAlgorithmTags.ZLIB); - sPreferredCompressionAlgorithms.add(CompressionAlgorithmTags.BZIP2); - } - - public static final int CERTIFY_HASH_ALGO = HashAlgorithmTags.SHA256; - - /* - * Note: s2kcount is a number between 0 and 0xff that controls the - * number of times to iterate the password hash before use. More - * iterations are useful against offline attacks, as it takes more - * time to check each password. The actual number of iterations is - * rather complex, and also depends on the hash function in use. - * Refer to Section 3.7.1.3 in rfc4880.txt. Bigger numbers give - * you more iterations. As a rough rule of thumb, when using - * SHA256 as the hashing function, 0x10 gives you about 64 - * iterations, 0x20 about 128, 0x30 about 256 and so on till 0xf0, - * or about 1 million iterations. The maximum you can go to is - * 0xff, or about 2 million iterations. - * from http://kbsriram.com/2013/01/generating-rsa-keys-with-bouncycastle.html - * - * Bouncy Castle default: 0x60 - * kbsriram proposes: 0xc0 - * OpenKeychain: 0x90 - */ - public static final int SECRET_KEY_ENCRYPTOR_S2K_COUNT = 0x90; - public static final int SECRET_KEY_ENCRYPTOR_HASH_ALGO = HashAlgorithmTags.SHA256; - public static final int SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO = SymmetricKeyAlgorithmTags.AES_256; - public static final int SECRET_KEY_SIGNATURE_HASH_ALGO = HashAlgorithmTags.SHA256; - // NOTE: only SHA1 is supported for key checksum calculations in OpenPGP, - // see http://tools.ietf.org/html/rfc488 0#section-5.5.3 - public static final int SECRET_KEY_SIGNATURE_CHECKSUM_HASH_ALGO = HashAlgorithmTags.SHA1; - - public static interface OpenKeychainSymmetricKeyAlgorithmTags extends SymmetricKeyAlgorithmTags { - public static final int USE_PREFERRED = -1; - } - - public static interface OpenKeychainHashAlgorithmTags extends HashAlgorithmTags { - public static final int USE_PREFERRED = -1; - } - - public static interface OpenKeychainCompressionAlgorithmTags extends CompressionAlgorithmTags { - public static final int USE_PREFERRED = -1; - } - - public static int[] getAsArray(ArrayList<Integer> list) { - int[] array = new int[list.size()]; - for (int i = 0; i < list.size(); i++) { - array[i] = list.get(i); - } - return array; - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java new file mode 100644 index 000000000..a6d65688c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * 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 java.util.HashSet; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +public class PgpDecryptVerifyInputParcel implements Parcelable { + + private Uri mInputUri; + private Uri mOutputUri; + private byte[] mInputBytes; + + private boolean mAllowSymmetricDecryption; + private HashSet<Long> mAllowedKeyIds; + private boolean mDecryptMetadataOnly; + private byte[] mDetachedSignature; + private String mRequiredSignerFingerprint; + private boolean mSignedLiteralData; + + public PgpDecryptVerifyInputParcel() { + } + + public PgpDecryptVerifyInputParcel(Uri inputUri, Uri outputUri) { + mInputUri = inputUri; + mOutputUri = outputUri; + } + + public PgpDecryptVerifyInputParcel(byte[] inputBytes) { + mInputBytes = inputBytes; + } + + PgpDecryptVerifyInputParcel(Parcel source) { + // we do all of those here, so the PgpSignEncryptInput class doesn't have to be parcelable + mInputUri = source.readParcelable(getClass().getClassLoader()); + mOutputUri = source.readParcelable(getClass().getClassLoader()); + mInputBytes = source.createByteArray(); + + mAllowSymmetricDecryption = source.readInt() != 0; + mAllowedKeyIds = (HashSet<Long>) source.readSerializable(); + mDecryptMetadataOnly = source.readInt() != 0; + mDetachedSignature = source.createByteArray(); + mRequiredSignerFingerprint = source.readString(); + mSignedLiteralData = source.readInt() != 0; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mInputUri, 0); + dest.writeParcelable(mOutputUri, 0); + dest.writeByteArray(mInputBytes); + + dest.writeInt(mAllowSymmetricDecryption ? 1 : 0); + dest.writeSerializable(mAllowedKeyIds); + dest.writeInt(mDecryptMetadataOnly ? 1 : 0); + dest.writeByteArray(mDetachedSignature); + dest.writeString(mRequiredSignerFingerprint); + dest.writeInt(mSignedLiteralData ? 1 : 0); + } + + byte[] getInputBytes() { + return mInputBytes; + } + + Uri getInputUri() { + return mInputUri; + } + + Uri getOutputUri() { + return mOutputUri; + } + + boolean isAllowSymmetricDecryption() { + return mAllowSymmetricDecryption; + } + + public PgpDecryptVerifyInputParcel setAllowSymmetricDecryption(boolean allowSymmetricDecryption) { + mAllowSymmetricDecryption = allowSymmetricDecryption; + return this; + } + + HashSet<Long> getAllowedKeyIds() { + return mAllowedKeyIds; + } + + public PgpDecryptVerifyInputParcel setAllowedKeyIds(HashSet<Long> allowedKeyIds) { + mAllowedKeyIds = allowedKeyIds; + return this; + } + + boolean isDecryptMetadataOnly() { + return mDecryptMetadataOnly; + } + + public PgpDecryptVerifyInputParcel setDecryptMetadataOnly(boolean decryptMetadataOnly) { + mDecryptMetadataOnly = decryptMetadataOnly; + return this; + } + + byte[] getDetachedSignature() { + return mDetachedSignature; + } + + public PgpDecryptVerifyInputParcel setDetachedSignature(byte[] detachedSignature) { + mDetachedSignature = detachedSignature; + return this; + } + + String getRequiredSignerFingerprint() { + return mRequiredSignerFingerprint; + } + + public PgpDecryptVerifyInputParcel setRequiredSignerFingerprint(String requiredSignerFingerprint) { + mRequiredSignerFingerprint = requiredSignerFingerprint; + return this; + } + + boolean isSignedLiteralData() { + return mSignedLiteralData; + } + + public PgpDecryptVerifyInputParcel setSignedLiteralData(boolean signedLiteralData) { + mSignedLiteralData = signedLiteralData; + return this; + } + + public static final Creator<PgpDecryptVerifyInputParcel> CREATOR = new Creator<PgpDecryptVerifyInputParcel>() { + public PgpDecryptVerifyInputParcel createFromParcel(final Parcel source) { + return new PgpDecryptVerifyInputParcel(source); + } + + public PgpDecryptVerifyInputParcel[] newArray(final int size) { + return new PgpDecryptVerifyInputParcel[size]; + } + }; + +} + diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java index f6580b85a..dd30156f9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java @@ -19,15 +19,19 @@ package org.sufficientlysecure.keychain.pgp; import android.content.Context; +import android.support.annotation.NonNull; import android.webkit.MimeTypeMap; +import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpSignatureResult; import org.spongycastle.bcpg.ArmoredInputStream; import org.spongycastle.openpgp.PGPCompressedData; +import org.spongycastle.openpgp.PGPDataValidationException; import org.spongycastle.openpgp.PGPEncryptedData; import org.spongycastle.openpgp.PGPEncryptedDataList; import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPKeyValidationException; import org.spongycastle.openpgp.PGPLiteralData; import org.spongycastle.openpgp.PGPOnePassSignature; import org.spongycastle.openpgp.PGPOnePassSignatureList; @@ -39,12 +43,13 @@ import org.spongycastle.openpgp.PGPUtil; import org.spongycastle.openpgp.jcajce.JcaPGPObjectFactory; import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory; import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.spongycastle.openpgp.operator.jcajce.CachingDataDecryptorFactory; 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.NfcSyncPublicKeyDataDecryptorFactoryBuilder; +import org.spongycastle.util.encoders.DecoderException; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Constants.key; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.BaseOperation; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; @@ -57,6 +62,7 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.Operat 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.FileHelper; import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; @@ -65,154 +71,99 @@ import org.sufficientlysecure.keychain.util.ProgressScaler; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.URLConnection; import java.security.SignatureException; import java.util.Date; import java.util.Iterator; -import java.util.Set; -/** - * This class uses a Builder pattern! - */ -public class PgpDecryptVerify extends BaseOperation { - - private InputData mData; - private OutputStream mOutStream; - - private boolean mAllowSymmetricDecryption; - private Set<Long> mAllowedKeyIds; - private boolean mDecryptMetadataOnly; - private byte[] mDetachedSignature; - private String mRequiredSignerFingerprint; - private boolean mSignedLiteralData; - - protected PgpDecryptVerify(Builder builder) { - super(builder.mContext, builder.mProviderHelper, builder.mProgressable); - - // private Constructor can only be called from Builder - this.mData = builder.mData; - this.mOutStream = builder.mOutStream; - - this.mAllowSymmetricDecryption = builder.mAllowSymmetricDecryption; - this.mAllowedKeyIds = builder.mAllowedKeyIds; - this.mDecryptMetadataOnly = builder.mDecryptMetadataOnly; - this.mDetachedSignature = builder.mDetachedSignature; - this.mSignedLiteralData = builder.mSignedLiteralData; - this.mRequiredSignerFingerprint = builder.mRequiredSignerFingerprint; - } +public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInputParcel> { - public static class Builder { - // mandatory parameter - private Context mContext; - private ProviderHelper mProviderHelper; - private InputData mData; - - // optional - private OutputStream mOutStream = null; - private Progressable mProgressable = null; - private boolean mAllowSymmetricDecryption = true; - private Set<Long> mAllowedKeyIds = null; - private boolean mDecryptMetadataOnly = false; - private byte[] mDetachedSignature = null; - private String mRequiredSignerFingerprint = null; - private boolean mSignedLiteralData = false; - - public Builder(Context context, ProviderHelper providerHelper, - Progressable progressable, - InputData data, OutputStream outStream) { - mContext = context; - mProviderHelper = providerHelper; - mProgressable = progressable; - mData = data; - mOutStream = outStream; - } + public PgpDecryptVerifyOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { + super(context, providerHelper, progressable); + } - /** - * This is used when verifying signed literals to check that they are signed with - * the required key - */ - public Builder setRequiredSignerFingerprint(String fingerprint) { - mRequiredSignerFingerprint = fingerprint; - return this; - } + /** Decrypts and/or verifies data based on parameters of PgpDecryptVerifyInputParcel. */ + @NonNull + public DecryptVerifyResult execute(PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput) { + InputData inputData; + OutputStream outputStream; - /** - * This is to force a mode where the message is just the signature key id and - * then a literal data packet; used in Keybase.io proofs - */ - public Builder setSignedLiteralData(boolean signedLiteralData) { - mSignedLiteralData = signedLiteralData; - return this; + if (input.getInputBytes() != null) { + byte[] inputBytes = input.getInputBytes(); + inputData = new InputData(new ByteArrayInputStream(inputBytes), inputBytes.length); + } else { + try { + InputStream inputStream = mContext.getContentResolver().openInputStream(input.getInputUri()); + long inputSize = FileHelper.getFileSize(mContext, input.getInputUri(), 0); + inputData = new InputData(inputStream, inputSize); + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "Input URI could not be opened: " + input.getInputUri(), e); + OperationLog log = new OperationLog(); + log.add(LogType.MSG_DC_ERROR_INPUT, 1); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } } - public Builder setAllowSymmetricDecryption(boolean allowSymmetricDecryption) { - mAllowSymmetricDecryption = allowSymmetricDecryption; - return this; + if (input.getOutputUri() == null) { + outputStream = new ByteArrayOutputStream(); + } else { + try { + outputStream = mContext.getContentResolver().openOutputStream(input.getOutputUri()); + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "Output URI could not be opened: " + input.getOutputUri(), e); + OperationLog log = new OperationLog(); + log.add(LogType.MSG_DC_ERROR_IO, 1); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } } - /** - * Allow these key ids alone for decryption. - * This means only ciphertexts encrypted for one of these private key can be decrypted. - */ - public Builder setAllowedKeyIds(Set<Long> allowedKeyIds) { - mAllowedKeyIds = allowedKeyIds; - return this; + DecryptVerifyResult result = executeInternal(input, cryptoInput, inputData, outputStream); + if (outputStream instanceof ByteArrayOutputStream) { + byte[] outputData = ((ByteArrayOutputStream) outputStream).toByteArray(); + result.setOutputBytes(outputData); } - /** - * If enabled, the actual decryption/verification of the content will not be executed. - * The metadata only will be decrypted and returned. - */ - public Builder setDecryptMetadataOnly(boolean decryptMetadataOnly) { - mDecryptMetadataOnly = decryptMetadataOnly; - return this; - } + return result; - /** - * If detachedSignature != null, it will be used exclusively to verify the signature - */ - public Builder setDetachedSignature(byte[] detachedSignature) { - mDetachedSignature = detachedSignature; - return this; - } + } - public PgpDecryptVerify build() { - return new PgpDecryptVerify(this); - } + @NonNull + public DecryptVerifyResult execute(PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput, + InputData inputData, OutputStream outputStream) { + return executeInternal(input, cryptoInput, inputData, outputStream); } - /** - * Decrypts and/or verifies data based on parameters of class - */ - public DecryptVerifyResult execute(CryptoInputParcel cryptoInput) { + @NonNull + private DecryptVerifyResult executeInternal(PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput, + InputData inputData, OutputStream outputStream) { try { - if (mDetachedSignature != null) { + if (input.getDetachedSignature() != null) { Log.d(Constants.TAG, "Detached signature present, verifying with this signature only"); - return verifyDetachedSignature(mData.getInputStream(), 0); + return verifyDetachedSignature(input, inputData, outputStream, 0); } else { // automatically works with PGP ascii armor and PGP binary - InputStream in = PGPUtil.getDecoderStream(mData.getInputStream()); + InputStream in = PGPUtil.getDecoderStream(inputData.getInputStream()); if (in instanceof ArmoredInputStream) { ArmoredInputStream aIn = (ArmoredInputStream) in; // it is ascii armored Log.d(Constants.TAG, "ASCII Armor Header Line: " + aIn.getArmorHeaderLine()); - if (mSignedLiteralData) { - return verifySignedLiteralData(aIn, 0); + if (input.isSignedLiteralData()) { + return verifySignedLiteralData(input, aIn, outputStream, 0); } else if (aIn.isClearText()) { // a cleartext signature, verify it with the other method - return verifyCleartextSignature(aIn, 0); + return verifyCleartextSignature(aIn, outputStream, 0); } else { // else: ascii armored encryption! go on... - return decryptVerify(cryptoInput, in, 0); + return decryptVerify(input, cryptoInput, in, outputStream, 0); } } else { - return decryptVerify(cryptoInput, in, 0); + return decryptVerify(input, cryptoInput, in, outputStream, 0); } } } catch (PGPException e) { @@ -220,6 +171,13 @@ public class PgpDecryptVerify extends BaseOperation { OperationLog log = new OperationLog(); log.add(LogType.MSG_DC_ERROR_PGP_EXCEPTION, 1); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } catch (DecoderException | ArrayIndexOutOfBoundsException e) { + // these can happen if assumptions in JcaPGPObjectFactory.nextObject() aren't + // fulfilled, so we need to catch them here to handle this gracefully + Log.d(Constants.TAG, "data error", e); + OperationLog log = new OperationLog(); + log.add(LogType.MSG_DC_ERROR_IO, 1); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } catch (IOException e) { Log.d(Constants.TAG, "IOException", e); OperationLog log = new OperationLog(); @@ -228,10 +186,10 @@ public class PgpDecryptVerify extends BaseOperation { } } - /** - * Verify Keybase.io style signed literal data - */ - private DecryptVerifyResult verifySignedLiteralData(InputStream in, int indent) + /**Verify signed plaintext data (PGP/INLINE). */ + @NonNull + private DecryptVerifyResult verifySignedLiteralData( + PgpDecryptVerifyInputParcel input, InputStream in, OutputStream out, int indent) throws IOException, PGPException { OperationLog log = new OperationLog(); log.add(LogType.MSG_VL, indent); @@ -282,9 +240,9 @@ public class PgpDecryptVerify extends BaseOperation { } String fingerprint = KeyFormattingUtils.convertFingerprintToHex(signingRing.getFingerprint()); - if (!(mRequiredSignerFingerprint.equals(fingerprint))) { + if (!(input.getRequiredSignerFingerprint().equals(fingerprint))) { log.add(LogType.MSG_VL_ERROR_MISSING_KEY, indent); - Log.d(Constants.TAG, "Fingerprint mismatch; wanted " + mRequiredSignerFingerprint + + Log.d(Constants.TAG, "Fingerprint mismatch; wanted " + input.getRequiredSignerFingerprint() + " got " + fingerprint + "!"); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } @@ -316,7 +274,7 @@ public class PgpDecryptVerify extends BaseOperation { int length; byte[] buffer = new byte[1 << 16]; while ((length = dataIn.read(buffer)) > 0) { - mOutStream.write(buffer, 0, length); + out.write(buffer, 0, length); signature.update(buffer, 0, length); } @@ -326,10 +284,6 @@ public class PgpDecryptVerify extends BaseOperation { PGPSignatureList signatureList = (PGPSignatureList) pgpF.nextObject(); PGPSignature messageSignature = signatureList.get(signatureIndex); - // these are not cleartext signatures! - // TODO: what about binary signatures? - signatureResultBuilder.setSignatureOnly(false); - // Verify signature and check binding signatures boolean validSignature = signature.verify(messageSignature); if (validSignature) { @@ -341,8 +295,8 @@ public class PgpDecryptVerify extends BaseOperation { OpenPgpSignatureResult signatureResult = signatureResultBuilder.build(); - if (signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED - && signatureResult.getStatus() != OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED) { + if (signatureResult.getResult() != OpenPgpSignatureResult.RESULT_VALID_CONFIRMED + && signatureResult.getResult() != OpenPgpSignatureResult.RESULT_VALID_UNCONFIRMED) { log.add(LogType.MSG_VL_ERROR_INTEGRITY_CHECK, indent); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } @@ -352,19 +306,22 @@ public class PgpDecryptVerify extends BaseOperation { log.add(LogType.MSG_VL_OK, indent); // Return a positive result, with metadata and verification info - DecryptVerifyResult result = - new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); + DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); result.setSignatureResult(signatureResult); + result.setDecryptionResult( + new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED)); return result; } - /** - * Decrypt and/or verifies binary or ascii armored pgp - */ - private DecryptVerifyResult decryptVerify(CryptoInputParcel cryptoInput, - InputStream in, int indent) throws IOException, PGPException { + /** Decrypt and/or verify binary or ascii armored pgp data. */ + @NonNull + private DecryptVerifyResult decryptVerify( + PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput, + InputStream in, OutputStream out, int indent) throws IOException, PGPException { + OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); + OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder(); OperationLog log = new OperationLog(); log.add(LogType.MSG_DC, indent); @@ -384,7 +341,7 @@ public class PgpDecryptVerify extends BaseOperation { } if (enc == null) { - log.add(LogType.MSG_DC_ERROR_INVALID_SIGLIST, indent); + log.add(LogType.MSG_DC_ERROR_INVALID_DATA, indent); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } @@ -419,6 +376,7 @@ public class PgpDecryptVerify extends BaseOperation { } Passphrase passphrase = null; + boolean skippedDisallowedKey = false; // go through all objects and find one we can decrypt while (it.hasNext()) { @@ -451,29 +409,31 @@ public class PgpDecryptVerify extends BaseOperation { log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1); continue; } - // get subkey which has been used for this encryption packet - secretEncryptionKey = secretKeyRing.getSecretKey(subKeyId); - if (secretEncryptionKey == null) { - // should actually never happen, so no need to be more specific. - log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1); - continue; - } // allow only specific keys for decryption? - if (mAllowedKeyIds != null) { + if (input.getAllowedKeyIds() != null) { long masterKeyId = secretKeyRing.getMasterKeyId(); Log.d(Constants.TAG, "encData.getKeyID(): " + subKeyId); - Log.d(Constants.TAG, "mAllowedKeyIds: " + mAllowedKeyIds); + Log.d(Constants.TAG, "mAllowedKeyIds: " + input.getAllowedKeyIds()); Log.d(Constants.TAG, "masterKeyId: " + masterKeyId); - if (!mAllowedKeyIds.contains(masterKeyId)) { + if (!input.getAllowedKeyIds().contains(masterKeyId)) { // this key is in our db, but NOT allowed! // continue with the next packet in the while loop + skippedDisallowedKey = true; log.add(LogType.MSG_DC_ASKIP_NOT_ALLOWED, indent + 1); continue; } } + // get subkey which has been used for this encryption packet + secretEncryptionKey = secretKeyRing.getSecretKey(subKeyId); + if (secretEncryptionKey == null) { + // should actually never happen, so no need to be more specific. + log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1); + continue; + } + /* secret key exists in database and is allowed! */ asymmetricPacketFound = true; @@ -499,10 +459,17 @@ public class PgpDecryptVerify extends BaseOperation { log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1); return new DecryptVerifyResult(log, RequiredInputParcel.createRequiredDecryptPassphrase( - secretKeyRing.getMasterKeyId(), secretEncryptionKey.getKeyId())); + secretKeyRing.getMasterKeyId(), secretEncryptionKey.getKeyId()), + cryptoInput); } } + // check for insecure encryption key + if ( ! PgpSecurityConstants.isSecureKey(secretEncryptionKey)) { + log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1); + decryptionResultBuilder.setInsecure(true); + } + // break out of while, only decrypt the first packet where we have a key break; @@ -511,7 +478,7 @@ public class PgpDecryptVerify extends BaseOperation { log.add(LogType.MSG_DC_SYM, indent); - if (!mAllowSymmetricDecryption) { + if (!input.isAllowSymmetricDecryption()) { log.add(LogType.MSG_DC_SYM_SKIP, indent + 1); continue; } @@ -527,12 +494,24 @@ public class PgpDecryptVerify extends BaseOperation { // if no passphrase is given, return here // indicating that a passphrase is missing! if (!cryptoInput.hasPassphrase()) { - log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1); - return new DecryptVerifyResult(log, - RequiredInputParcel.createRequiredSymmetricPassphrase()); - } - passphrase = cryptoInput.getPassphrase(); + try { + passphrase = getCachedPassphrase(key.symmetric); + log.add(LogType.MSG_DC_PASS_CACHED, indent + 1); + } catch (PassphraseCacheInterface.NoSecretKeyException e) { + // nvm + } + + if (passphrase == null) { + log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1); + return new DecryptVerifyResult(log, + RequiredInputParcel.createRequiredSymmetricPassphrase(), + cryptoInput); + } + + } else { + passphrase = cryptoInput.getPassphrase(); + } // break out of while, only decrypt the first packet break; @@ -568,7 +547,14 @@ public class PgpDecryptVerify extends BaseOperation { digestCalcProvider).setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( passphrase.getCharArray()); - clear = encryptedDataSymmetric.getDataStream(decryptorFactory); + try { + clear = encryptedDataSymmetric.getDataStream(decryptorFactory); + } catch (PGPDataValidationException e) { + log.add(LogType.MSG_DC_ERROR_SYM_PASSPHRASE, indent +1); + return new DecryptVerifyResult(log, + RequiredInputParcel.createRequiredSymmetricPassphrase(), cryptoInput); + } + encryptedData = encryptedDataSymmetric; symmetricEncryptionAlgo = encryptedDataSymmetric.getSymmetricAlgorithm(decryptorFactory); @@ -590,35 +576,60 @@ public class PgpDecryptVerify extends BaseOperation { currentProgress += 2; updateProgress(R.string.progress_preparing_streams, currentProgress, 100); - try { - PublicKeyDataDecryptorFactory decryptorFactory - = secretEncryptionKey.getDecryptorFactory(cryptoInput); - clear = encryptedDataAsymmetric.getDataStream(decryptorFactory); + CachingDataDecryptorFactory decryptorFactory + = secretEncryptionKey.getCachingDecryptorFactory(cryptoInput); + + // special case: if the decryptor does not have a session key cached for this encrypted + // data, and can't actually decrypt on its own, return a pending intent + if (!decryptorFactory.canDecrypt() + && !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) { - symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory); - } catch (NfcSyncPublicKeyDataDecryptorFactoryBuilder.NfcInteractionNeeded e) { log.add(LogType.MSG_DC_PENDING_NFC, indent + 1); return new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation( - e.encryptedSessionKey, secretEncryptionKey.getKeyId() - )); + secretEncryptionKey.getRing().getMasterKeyId(), + secretEncryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0] + ), + cryptoInput); + + } + + try { + clear = encryptedDataAsymmetric.getDataStream(decryptorFactory); + } catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) { + log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } + + symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory); + + cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys()); + encryptedData = encryptedDataAsymmetric; } else { - // If we didn't find any useful data, error out + // there wasn't even any useful data + if (!anyPacketFound) { + log.add(LogType.MSG_DC_ERROR_NO_DATA, indent + 1); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_NO_DATA, log); + } + // there was data but key wasn't allowed + if (skippedDisallowedKey) { + log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_KEY_DISALLOWED, log); + } // no packet has been found where we have the corresponding secret key in our db - log.add( - anyPacketFound ? LogType.MSG_DC_ERROR_NO_KEY : LogType.MSG_DC_ERROR_NO_DATA, indent + 1); + log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } + decryptionResultBuilder.setEncrypted(true); - // Warn about old encryption algorithms! - if (!PgpConstants.sPreferredSymmetricAlgorithms.contains(symmetricEncryptionAlgo)) { - log.add(LogType.MSG_DC_OLD_SYMMETRIC_ENCRYPTION_ALGO, indent + 1); + // Check for insecure encryption algorithms! + if (!PgpSecurityConstants.isSecureSymmetricAlgorithm(symmetricEncryptionAlgo)) { + log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1); + decryptionResultBuilder.setInsecure(true); } JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear); Object dataChunk = plainFact.nextObject(); - OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); int signatureIndex = -1; CanonicalizedPublicKeyRing signingRing = null; CanonicalizedPublicKey signingKey = null; @@ -682,6 +693,13 @@ public class PgpDecryptVerify extends BaseOperation { } } + // check for insecure signing key + // TODO: checks on signingRing ? + if (signingKey != null && ! PgpSecurityConstants.isSecureKey(signingKey)) { + log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1); + signatureResultBuilder.setInsecure(true); + } + dataChunk = plainFact.nextObject(); } @@ -700,17 +718,12 @@ public class PgpDecryptVerify extends BaseOperation { PGPLiteralData literalData = (PGPLiteralData) dataChunk; - // reported size may be null if partial packets are involved (highly unlikely though) - Long originalSize = literalData.getDataLengthIfAvailable(); - String originalFilename = literalData.getFileName(); String mimeType = null; if (literalData.getFormat() == PGPLiteralData.TEXT || literalData.getFormat() == PGPLiteralData.UTF8) { mimeType = "text/plain"; } else { - // TODO: better would be: https://github.com/open-keychain/open-keychain/issues/753 - // try to guess from file ending String extension = MimeTypeMap.getFileExtensionFromUrl(originalFilename); if (extension != null) { @@ -718,19 +731,10 @@ public class PgpDecryptVerify extends BaseOperation { mimeType = mime.getMimeTypeFromExtension(extension); } if (mimeType == null) { - mimeType = URLConnection.guessContentTypeFromName(originalFilename); - } - if (mimeType == null) { - mimeType = "*/*"; + mimeType = "application/octet-stream"; } } - metadata = new OpenPgpMetadata( - originalFilename, - mimeType, - literalData.getModificationTime().getTime(), - originalSize == null ? 0 : originalSize); - if (!"".equals(originalFilename)) { log.add(LogType.MSG_DC_CLEAR_META_FILE, indent + 1, originalFilename); } @@ -738,20 +742,31 @@ public class PgpDecryptVerify extends BaseOperation { mimeType); log.add(LogType.MSG_DC_CLEAR_META_TIME, indent + 1, new Date(literalData.getModificationTime().getTime()).toString()); - if (originalSize != null) { - log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1, - Long.toString(originalSize)); - } else { - log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1); - } // return here if we want to decrypt the metadata only - if (mDecryptMetadataOnly) { + if (input.isDecryptMetadataOnly()) { + + // this operation skips the entire stream to find the data length! + Long originalSize = literalData.findDataLength(); + + if (originalSize != null) { + log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1, + Long.toString(originalSize)); + } else { + log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1); + } + + metadata = new OpenPgpMetadata( + originalFilename, + mimeType, + literalData.getModificationTime().getTime(), + originalSize == null ? 0 : originalSize); + log.add(LogType.MSG_DC_OK_META_ONLY, indent); DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); result.setCharset(charset); - result.setDecryptMetadata(metadata); + result.setDecryptionMetadata(metadata); return result; } @@ -769,13 +784,13 @@ public class PgpDecryptVerify extends BaseOperation { InputStream dataIn = literalData.getInputStream(); long alreadyWritten = 0; - long wholeSize = mData.getSize() - mData.getStreamPosition(); + long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition(); int length; byte[] buffer = new byte[1 << 16]; while ((length = dataIn.read(buffer)) > 0) { - Log.d(Constants.TAG, "read bytes: " + length); - if (mOutStream != null) { - mOutStream.write(buffer, 0, length); + // Log.d(Constants.TAG, "read bytes: " + length); + if (out != null) { + out.write(buffer, 0, length); } // update signature buffer if signature is also present @@ -795,6 +810,12 @@ public class PgpDecryptVerify extends BaseOperation { // TODO: slow annealing to fake a progress? } + metadata = new OpenPgpMetadata( + originalFilename, + mimeType, + literalData.getModificationTime().getTime(), + alreadyWritten); + if (signature != null) { updateProgress(R.string.progress_verifying_signature, 90, 100); log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); @@ -802,11 +823,9 @@ public class PgpDecryptVerify extends BaseOperation { PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject(); PGPSignature messageSignature = signatureList.get(signatureIndex); - // these are not cleartext signatures! // TODO: what about binary signatures? - signatureResultBuilder.setSignatureOnly(false); - // Verify signature and check binding signatures + // Verify signature boolean validSignature = signature.verify(messageSignature); if (validSignature) { log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); @@ -814,10 +833,10 @@ public class PgpDecryptVerify extends BaseOperation { log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); } - // Don't allow verification of old hash algorithms! - if (!PgpConstants.sPreferredHashAlgorithms.contains(signature.getHashAlgorithm())) { - validSignature = false; - log.add(LogType.MSG_DC_ERROR_UNSUPPORTED_HASH_ALGO, indent + 1); + // check for insecure hash algorithms + if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { + log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); + signatureResultBuilder.setInsecure(true); } signatureResultBuilder.setValidSignature(validSignature); @@ -844,8 +863,8 @@ public class PgpDecryptVerify extends BaseOperation { // The MDC packet can be stripped by an attacker! Log.d(Constants.TAG, "MDC fail"); if (!signatureResultBuilder.isValidSignature()) { - log.add(LogType.MSG_DC_ERROR_INTEGRITY_MISSING, indent); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + log.add(LogType.MSG_DC_INSECURE_MDC_MISSING, indent); + decryptionResultBuilder.setInsecure(true); } } @@ -854,11 +873,12 @@ public class PgpDecryptVerify extends BaseOperation { log.add(LogType.MSG_DC_OK, indent); // Return a positive result, with metadata and verification info - DecryptVerifyResult result = - new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); - result.setDecryptMetadata(metadata); + DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); + result.setCachedCryptoInputParcel(cryptoInput); result.setSignatureResult(signatureResultBuilder.build()); result.setCharset(charset); + result.setDecryptionResult(decryptionResultBuilder.build()); + result.setDecryptionMetadata(metadata); return result; } @@ -870,14 +890,13 @@ public class PgpDecryptVerify extends BaseOperation { * The method is heavily based on * pg/src/main/java/org/spongycastle/openpgp/examples/ClearSignedFileProcessor.java */ + @NonNull private DecryptVerifyResult verifyCleartextSignature( - ArmoredInputStream aIn, int indent) throws IOException, PGPException { + ArmoredInputStream aIn, OutputStream outputStream, int indent) throws IOException, PGPException { OperationLog log = new OperationLog(); OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); - // cleartext signatures are never encrypted ;) - signatureResultBuilder.setSignatureOnly(true); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -901,8 +920,9 @@ public class PgpDecryptVerify extends BaseOperation { out.close(); byte[] clearText = out.toByteArray(); - if (mOutStream != null) { - mOutStream.write(clearText); + if (outputStream != null) { + outputStream.write(clearText); + outputStream.close(); } updateProgress(R.string.progress_processing_signature, 60, 100); @@ -910,11 +930,11 @@ public class PgpDecryptVerify extends BaseOperation { PGPSignatureList sigList = (PGPSignatureList) pgpFact.nextObject(); if (sigList == null) { - log.add(LogType.MSG_DC_ERROR_INVALID_SIGLIST, 0); + log.add(LogType.MSG_DC_ERROR_INVALID_DATA, 0); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } - PGPSignature signature = processPGPSignatureList(sigList, signatureResultBuilder); + PGPSignature signature = processPGPSignatureList(sigList, signatureResultBuilder, log, indent); if (signature != null) { try { @@ -946,10 +966,10 @@ public class PgpDecryptVerify extends BaseOperation { log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); } - // Don't allow verification of old hash algorithms! - if (!PgpConstants.sPreferredHashAlgorithms.contains(signature.getHashAlgorithm())) { - validSignature = false; - log.add(LogType.MSG_DC_ERROR_UNSUPPORTED_HASH_ALGO, indent + 1); + // check for insecure hash algorithms + if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { + log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); + signatureResultBuilder.setInsecure(true); } signatureResultBuilder.setValidSignature(validSignature); @@ -964,22 +984,31 @@ public class PgpDecryptVerify extends BaseOperation { log.add(LogType.MSG_DC_OK, indent); + OpenPgpMetadata metadata = new OpenPgpMetadata( + "", + "text/plain", + -1, + clearText.length); + DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); result.setSignatureResult(signatureResultBuilder.build()); + result.setDecryptionResult( + new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED)); + result.setDecryptionMetadata(metadata); return result; } - private DecryptVerifyResult verifyDetachedSignature(InputStream in, int indent) + @NonNull + private DecryptVerifyResult verifyDetachedSignature( + PgpDecryptVerifyInputParcel input, InputData inputData, OutputStream out, int indent) throws IOException, PGPException { OperationLog log = new OperationLog(); OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); - // detached signatures are never encrypted - signatureResultBuilder.setSignatureOnly(true); updateProgress(R.string.progress_processing_signature, 0, 100); - InputStream detachedSigIn = new ByteArrayInputStream(mDetachedSignature); + InputStream detachedSigIn = new ByteArrayInputStream(input.getDetachedSignature()); detachedSigIn = PGPUtil.getDecoderStream(detachedSigIn); JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(detachedSigIn); @@ -993,23 +1022,24 @@ public class PgpDecryptVerify extends BaseOperation { } else if (o instanceof PGPSignatureList) { sigList = (PGPSignatureList) o; } else { - log.add(LogType.MSG_DC_ERROR_INVALID_SIGLIST, 0); + log.add(LogType.MSG_DC_ERROR_INVALID_DATA, 0); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } - PGPSignature signature = processPGPSignatureList(sigList, signatureResultBuilder); + PGPSignature signature = processPGPSignatureList(sigList, signatureResultBuilder, log, indent); if (signature != null) { updateProgress(R.string.progress_reading_data, 60, 100); ProgressScaler progressScaler = new ProgressScaler(mProgressable, 60, 90, 100); long alreadyWritten = 0; - long wholeSize = mData.getSize() - mData.getStreamPosition(); + long wholeSize = inputData.getSize() - inputData.getStreamPosition(); int length; byte[] buffer = new byte[1 << 16]; + InputStream in = inputData.getInputStream(); while ((length = in.read(buffer)) > 0) { - if (mOutStream != null) { - mOutStream.write(buffer, 0, length); + if (out != null) { + out.write(buffer, 0, length); } // update signature buffer if signature is also present @@ -1030,9 +1060,6 @@ public class PgpDecryptVerify extends BaseOperation { updateProgress(R.string.progress_verifying_signature, 90, 100); log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); - // these are not cleartext signatures! - signatureResultBuilder.setSignatureOnly(false); - // Verify signature and check binding signatures boolean validSignature = signature.verify(); if (validSignature) { @@ -1041,10 +1068,10 @@ public class PgpDecryptVerify extends BaseOperation { log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); } - // Don't allow verification of old hash algorithms! - if (!PgpConstants.sPreferredHashAlgorithms.contains(signature.getHashAlgorithm())) { - validSignature = false; - log.add(LogType.MSG_DC_ERROR_UNSUPPORTED_HASH_ALGO, indent + 1); + // check for insecure hash algorithms + if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { + log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); + signatureResultBuilder.setInsecure(true); } signatureResultBuilder.setValidSignature(validSignature); @@ -1056,10 +1083,15 @@ public class PgpDecryptVerify extends BaseOperation { DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); result.setSignatureResult(signatureResultBuilder.build()); + result.setDecryptionResult( + new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED)); return result; } - private PGPSignature processPGPSignatureList(PGPSignatureList sigList, OpenPgpSignatureResultBuilder signatureResultBuilder) throws PGPException { + private PGPSignature processPGPSignatureList( + PGPSignatureList sigList, OpenPgpSignatureResultBuilder signatureResultBuilder, + OperationLog log, int indent) + throws PGPException { CanonicalizedPublicKeyRing signingRing = null; CanonicalizedPublicKey signingKey = null; int signatureIndex = -1; @@ -1100,6 +1132,13 @@ public class PgpDecryptVerify extends BaseOperation { } } + // check for insecure signing key + // TODO: checks on signingRing ? + if (signingKey != null && ! PgpSecurityConstants.isSecureKey(signingKey)) { + log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1); + signatureResultBuilder.setInsecure(true); + } + return signature; } @@ -1195,12 +1234,6 @@ public class PgpDecryptVerify extends BaseOperation { 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; + return nl.getBytes(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java index d8b86a18c..e8d1d3111 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java @@ -21,6 +21,8 @@ package org.sufficientlysecure.keychain.pgp; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; +import android.support.annotation.NonNull; +import android.text.TextUtils; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; @@ -31,6 +33,7 @@ import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.security.SecureRandom; +import java.util.regex.Matcher; import java.util.regex.Pattern; public class PgpHelper { @@ -52,9 +55,6 @@ public class PgpHelper { * <p/> * TODO: Does this really help on flash storage? * - * @param context - * @param progressable - * @param file * @throws IOException */ public static void deleteFileSecurely(Context context, Progressable progressable, File file) @@ -78,4 +78,67 @@ public class PgpHelper { raf.close(); file.delete(); } + + /** + * Fixing broken PGP MESSAGE Strings coming from GMail/AOSP Mail + */ + public static String fixPgpMessage(String message) { + // windows newline -> unix newline + message = message.replaceAll("\r\n", "\n"); + // Mac OS before X newline -> unix newline + message = message.replaceAll("\r", "\n"); + + // remove whitespaces before newline + message = message.replaceAll(" +\n", "\n"); + // only two consecutive newlines are allowed + message = message.replaceAll("\n\n+", "\n\n"); + + // replace non breakable spaces + message = message.replaceAll("\\xa0", " "); + + return message; + } + + /** + * Fixing broken PGP SIGNED MESSAGE Strings coming from GMail/AOSP Mail + */ + public static String fixPgpCleartextSignature(CharSequence input) { + if (!TextUtils.isEmpty(input)) { + String text = input.toString(); + + // windows newline -> unix newline + text = text.replaceAll("\r\n", "\n"); + // Mac OS before X newline -> unix newline + text = text.replaceAll("\r", "\n"); + + return text; + } else { + return null; + } + } + + public static String getPgpContent(@NonNull CharSequence input) { + Log.dEscaped(Constants.TAG, "input: " + input); + + Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input); + if (matcher.matches()) { + String text = matcher.group(1); + text = fixPgpMessage(text); + + Log.dEscaped(Constants.TAG, "input fixed: " + text); + return text; + } else { + matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input); + if (matcher.matches()) { + String text = matcher.group(1); + text = fixPgpCleartextSignature(text); + + Log.dEscaped(Constants.TAG, "input fixed: " + text); + return text; + } else { + return null; + } + } + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java index 89db378a9..6f156c201 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java @@ -18,9 +18,11 @@ package org.sufficientlysecure.keychain.pgp; +import org.spongycastle.bcpg.PublicKeyAlgorithmTags; import org.spongycastle.bcpg.S2K; import org.spongycastle.bcpg.sig.Features; import org.spongycastle.bcpg.sig.KeyFlags; +import org.spongycastle.bcpg.sig.RevocationReasonTags; import org.spongycastle.jce.spec.ElGamalParameterSpec; import org.spongycastle.openpgp.PGPException; import org.spongycastle.openpgp.PGPKeyFlags; @@ -45,8 +47,10 @@ import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder; import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded; +import org.spongycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.EditKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; @@ -59,6 +63,7 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcKeyToCardOperationsBuilder; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.Log; @@ -68,6 +73,7 @@ import org.sufficientlysecure.keychain.util.ProgressScaler; import java.io.IOException; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; @@ -151,7 +157,7 @@ public class PgpKeyOperation { } /** Creates new secret key. */ - private PGPKeyPair createKey(SubkeyAdd add, OperationLog log, int indent) { + private PGPKeyPair createKey(SubkeyAdd add, Date creationTime, OperationLog log, int indent) { try { // Some safety checks @@ -249,7 +255,7 @@ public class PgpKeyOperation { } // build new key pair - return new JcaPGPKeyPair(algorithm, keyGen.generateKeyPair(), new Date()); + return new JcaPGPKeyPair(algorithm, keyGen.generateKeyPair(), creationTime); } catch(NoSuchProviderException | InvalidAlgorithmParameterException e) { throw new RuntimeException(e); @@ -295,8 +301,10 @@ public class PgpKeyOperation { return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); } + Date creationTime = new Date(); + subProgressPush(10, 30); - PGPKeyPair keyPair = createKey(add, log, indent); + PGPKeyPair keyPair = createKey(add, creationTime, log, indent); subProgressPop(); // return null if this failed (an error will already have been logged by createKey) @@ -308,14 +316,14 @@ public class PgpKeyOperation { // Build key encrypter and decrypter based on passphrase PGPDigestCalculator encryptorHashCalc = new JcaPGPDigestCalculatorProviderBuilder() - .build().get(PgpConstants.SECRET_KEY_ENCRYPTOR_HASH_ALGO); + .build().get(PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_HASH_ALGO); PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder( - PgpConstants.SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO, - encryptorHashCalc, PgpConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT) + PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO, + encryptorHashCalc, PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT) .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray()); PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder() - .build().get(PgpConstants.SECRET_KEY_SIGNATURE_CHECKSUM_HASH_ALGO); + .build().get(PgpSecurityConstants.SECRET_KEY_SIGNATURE_CHECKSUM_HASH_ALGO); PGPSecretKey masterSecretKey = new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(), sha1Calc, true, keyEncryptor); @@ -323,8 +331,8 @@ public class PgpKeyOperation { masterSecretKey.getEncoded(), new JcaKeyFingerprintCalculator()); subProgressPush(50, 100); - CryptoInputParcel cryptoInput = new CryptoInputParcel(new Date(), new Passphrase("")); - return internal(sKR, masterSecretKey, add.mFlags, add.mExpiry, cryptoInput, saveParcel, log); + CryptoInputParcel cryptoInput = new CryptoInputParcel(creationTime, new Passphrase("")); + return internal(sKR, masterSecretKey, add.mFlags, add.mExpiry, cryptoInput, saveParcel, log, indent); } catch (PGPException e) { log.add(LogType.MSG_CR_ERROR_INTERNAL_PGP, indent); @@ -356,8 +364,8 @@ public class PgpKeyOperation { * */ public PgpEditKeyResult modifySecretKeyRing(CanonicalizedSecretKeyRing wsKR, - CryptoInputParcel cryptoInput, - SaveKeyringParcel saveParcel) { + CryptoInputParcel cryptoInput, + SaveKeyringParcel saveParcel) { OperationLog log = new OperationLog(); int indent = 0; @@ -400,9 +408,61 @@ public class PgpKeyOperation { return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); } + // Ensure we don't have multiple keys for the same slot. + boolean hasSign = false; + boolean hasEncrypt = false; + boolean hasAuth = false; + for(SaveKeyringParcel.SubkeyChange change : saveParcel.mChangeSubKeys) { + if (change.mMoveKeyToCard) { + // If this is a keytocard operation, see if it was completed: look for a hash + // matching the given subkey ID in cryptoData. + byte[] subKeyId = new byte[8]; + ByteBuffer buf = ByteBuffer.wrap(subKeyId); + buf.putLong(change.mKeyId).rewind(); + + byte[] serialNumber = cryptoInput.getCryptoData().get(buf); + if (serialNumber != null) { + change.mMoveKeyToCard = false; + change.mDummyDivert = serialNumber; + } + } + + if (change.mMoveKeyToCard) { + // Pending keytocard operation. Need to make sure that we don't have multiple + // subkeys pending for the same slot. + CanonicalizedSecretKey wsK = wsKR.getSecretKey(change.mKeyId); + + if ((wsK.canSign() || wsK.canCertify())) { + if (hasSign) { + log.add(LogType.MSG_MF_ERROR_DUPLICATE_KEYTOCARD_FOR_SLOT, indent + 1); + return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); + } else { + hasSign = true; + } + } else if ((wsK.canEncrypt())) { + if (hasEncrypt) { + log.add(LogType.MSG_MF_ERROR_DUPLICATE_KEYTOCARD_FOR_SLOT, indent + 1); + return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); + } else { + hasEncrypt = true; + } + } else if ((wsK.canAuthenticate())) { + if (hasAuth) { + log.add(LogType.MSG_MF_ERROR_DUPLICATE_KEYTOCARD_FOR_SLOT, indent + 1); + return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); + } else { + hasAuth = true; + } + } else { + log.add(LogType.MSG_MF_ERROR_INVALID_FLAGS_FOR_KEYTOCARD, indent + 1); + return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); + } + } + } + if (isDummy(masterSecretKey) || saveParcel.isRestrictedOnly()) { log.add(LogType.MSG_MF_RESTRICTED_MODE, indent); - return internalRestricted(sKR, saveParcel, log); + return internalRestricted(sKR, saveParcel, log, indent + 1); } // Do we require a passphrase? If so, pass it along @@ -410,7 +470,7 @@ public class PgpKeyOperation { log.add(LogType.MSG_MF_REQUIRE_PASSPHRASE, indent); return new PgpEditKeyResult(log, RequiredInputParcel.createRequiredSignPassphrase( masterSecretKey.getKeyID(), masterSecretKey.getKeyID(), - cryptoInput.getSignatureTime())); + cryptoInput.getSignatureTime()), cryptoInput); } // read masterKeyFlags, and use the same as before. @@ -420,7 +480,7 @@ public class PgpKeyOperation { Date expiryTime = wsKR.getPublicKey().getExpiryTime(); long masterKeyExpiry = expiryTime != null ? expiryTime.getTime() / 1000 : 0L; - return internal(sKR, masterSecretKey, masterKeyFlags, masterKeyExpiry, cryptoInput, saveParcel, log); + return internal(sKR, masterSecretKey, masterKeyFlags, masterKeyExpiry, cryptoInput, saveParcel, log, indent); } @@ -428,13 +488,14 @@ public class PgpKeyOperation { int masterKeyFlags, long masterKeyExpiry, CryptoInputParcel cryptoInput, SaveKeyringParcel saveParcel, - OperationLog log) { - - int indent = 1; + OperationLog log, + int indent) { NfcSignOperationsBuilder nfcSignOps = new NfcSignOperationsBuilder( cryptoInput.getSignatureTime(), masterSecretKey.getKeyID(), masterSecretKey.getKeyID()); + NfcKeyToCardOperationsBuilder nfcKeyToCardOps = new NfcKeyToCardOperationsBuilder( + masterSecretKey.getKeyID()); progress(R.string.progress_modify, 0); @@ -553,7 +614,8 @@ public class PgpKeyOperation { PGPSignature cert = generateUserAttributeSignature( getSignatureGenerator(masterSecretKey, cryptoInput), cryptoInput.getSignatureTime(), - masterPrivateKey, masterPublicKey, vector); + masterPrivateKey, masterPublicKey, vector, + masterKeyFlags, masterKeyExpiry); modifiedPublicKey = PGPPublicKey.addCertification(modifiedPublicKey, vector, cert); } catch (NfcInteractionNeeded e) { nfcSignOps.addHash(e.hashToSign, e.hashAlgo); @@ -743,22 +805,36 @@ public class PgpKeyOperation { return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); } - if (change.mDummyStrip || change.mDummyDivert != null) { + if (change.mDummyStrip) { // IT'S DANGEROUS~ // no really, it is. this operation irrevocably removes the private key data from the key - if (change.mDummyStrip) { - sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey()); + sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey()); + sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey); + } else if (change.mMoveKeyToCard) { + if (checkSmartCardCompatibility(sKey, log, indent + 1)) { + log.add(LogType.MSG_MF_KEYTOCARD_START, indent + 1, + KeyFormattingUtils.convertKeyIdToHex(change.mKeyId)); + nfcKeyToCardOps.addSubkey(change.mKeyId); } else { - // the serial number must be 16 bytes in length - if (change.mDummyDivert.length != 16) { - log.add(LogType.MSG_MF_ERROR_DIVERT_SERIAL, - indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.mKeyId)); - return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); - } + // Appropriate log message already set by checkSmartCardCompatibility + return new PgpEditKeyResult(EditKeyResult.RESULT_ERROR, log, null); } + } else if (change.mDummyDivert != null) { + // NOTE: Does this code get executed? Or always handled in internalRestricted? + if (change.mDummyDivert.length != 16) { + log.add(LogType.MSG_MF_ERROR_DIVERT_SERIAL, + indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.mKeyId)); + return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); + } + log.add(LogType.MSG_MF_KEYTOCARD_FINISH, indent + 1, + KeyFormattingUtils.convertKeyIdToHex(change.mKeyId), + Hex.toHexString(change.mDummyDivert, 8, 6)); + sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey(), change.mDummyDivert); sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey); } + + // This doesn't concern us any further if (!change.mRecertify && (change.mExpiry == null && change.mFlags == null)) { continue; @@ -822,18 +898,35 @@ public class PgpKeyOperation { pKey = PGPPublicKey.removeCertification(pKey, sig); } - PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder() - .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( - cryptoInput.getPassphrase().getCharArray()); - PGPPrivateKey subPrivateKey = sKey.extractPrivateKey(keyDecryptor); - PGPSignature sig = generateSubkeyBindingSignature( - getSignatureGenerator(masterSecretKey, cryptoInput), - cryptoInput.getSignatureTime(), - masterPublicKey, masterPrivateKey, subPrivateKey, pKey, flags, expiry); + PGPPrivateKey subPrivateKey; + if (!isDivertToCard(sKey)) { + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + cryptoInput.getPassphrase().getCharArray()); + subPrivateKey = sKey.extractPrivateKey(keyDecryptor); + // super special case: subkey is allowed to sign, but isn't available + if (subPrivateKey == null) { + log.add(LogType.MSG_MF_ERROR_SUB_STRIPPED, + indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.mKeyId)); + return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); + } + } else { + subPrivateKey = null; + } + try { + PGPSignature sig = generateSubkeyBindingSignature( + getSignatureGenerator(masterSecretKey, cryptoInput), + cryptoInput.getSignatureTime(), masterPublicKey, masterPrivateKey, + getSignatureGenerator(sKey, cryptoInput), subPrivateKey, + pKey, flags, expiry); + + // generate and add new signature + pKey = PGPPublicKey.addCertification(pKey, sig); + sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey)); + } catch (NfcInteractionNeeded e) { + nfcSignOps.addHash(e.hashToSign, e.hashAlgo); + } - // generate and add new signature - pKey = PGPPublicKey.addCertification(pKey, sig); - sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey)); } subProgressPop(); @@ -884,6 +977,11 @@ public class PgpKeyOperation { log.add(LogType.MSG_MF_SUBKEY_NEW, indent, KeyFormattingUtils.getAlgorithmInfo(add.mAlgorithm, add.mKeySize, add.mCurve) ); + if (isDivertToCard(masterSecretKey)) { + log.add(LogType.MSG_MF_ERROR_DIVERT_NEWSUB, indent +1); + return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); + } + if (add.mExpiry == null) { log.add(LogType.MSG_MF_ERROR_NULL_EXPIRY, indent +1); return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); @@ -899,7 +997,7 @@ public class PgpKeyOperation { (i-1) * (100 / saveParcel.mAddSubKeys.size()), i * (100 / saveParcel.mAddSubKeys.size()) ); - PGPKeyPair keyPair = createKey(add, log, indent); + PGPKeyPair keyPair = createKey(add, cryptoInput.getSignatureTime(), log, indent); subProgressPop(); if (keyPair == null) { log.add(LogType.MSG_MF_ERROR_PGP, indent +1); @@ -912,7 +1010,8 @@ public class PgpKeyOperation { PGPSignature cert = generateSubkeyBindingSignature( getSignatureGenerator(masterSecretKey, cryptoInput), cryptoInput.getSignatureTime(), - masterPublicKey, masterPrivateKey, keyPair.getPrivateKey(), pKey, + masterPublicKey, masterPrivateKey, + getSignatureGenerator(pKey, cryptoInput, false), keyPair.getPrivateKey(), pKey, add.mFlags, add.mExpiry); pKey = PGPPublicKey.addSubkeyBindingCertification(pKey, cert); } catch (NfcInteractionNeeded e) { @@ -922,15 +1021,15 @@ public class PgpKeyOperation { PGPSecretKey sKey; { // Build key encrypter and decrypter based on passphrase PGPDigestCalculator encryptorHashCalc = new JcaPGPDigestCalculatorProviderBuilder() - .build().get(PgpConstants.SECRET_KEY_ENCRYPTOR_HASH_ALGO); + .build().get(PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_HASH_ALGO); PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder( - PgpConstants.SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO, encryptorHashCalc, - PgpConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT) + PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO, encryptorHashCalc, + PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT) .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( cryptoInput.getPassphrase().getCharArray()); PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder() - .build().get(PgpConstants.SECRET_KEY_SIGNATURE_CHECKSUM_HASH_ALGO); + .build().get(PgpSecurityConstants.SECRET_KEY_SIGNATURE_CHECKSUM_HASH_ALGO); sKey = new PGPSecretKey(keyPair.getPrivateKey(), pKey, sha1Calc, false, keyEncryptor); } @@ -964,6 +1063,26 @@ public class PgpKeyOperation { indent -= 1; } + // 7. if requested, change PIN and/or Admin PIN on card + if (saveParcel.mCardPin != null) { + progress(R.string.progress_modify_pin, 90); + log.add(LogType.MSG_MF_PIN, indent); + indent += 1; + + nfcKeyToCardOps.setPin(saveParcel.mCardPin); + + indent -= 1; + } + if (saveParcel.mCardAdminPin != null) { + progress(R.string.progress_modify_admin_pin, 90); + log.add(LogType.MSG_MF_ADMIN_PIN, indent); + indent += 1; + + nfcKeyToCardOps.setAdminPin(saveParcel.mCardAdminPin); + + indent -= 1; + } + } catch (IOException e) { Log.e(Constants.TAG, "encountered IOException while modifying key", e); log.add(LogType.MSG_MF_ERROR_ENCODE, indent+1); @@ -980,9 +1099,19 @@ public class PgpKeyOperation { progress(R.string.progress_done, 100); + if (!nfcSignOps.isEmpty() && !nfcKeyToCardOps.isEmpty()) { + log.add(LogType.MSG_MF_ERROR_CONFLICTING_NFC_COMMANDS, indent+1); + return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); + } + if (!nfcSignOps.isEmpty()) { log.add(LogType.MSG_MF_REQUIRE_DIVERT, indent); - return new PgpEditKeyResult(log, nfcSignOps.build()); + return new PgpEditKeyResult(log, nfcSignOps.build(), cryptoInput); + } + + if (!nfcKeyToCardOps.isEmpty()) { + log.add(LogType.MSG_MF_REQUIRE_DIVERT, indent); + return new PgpEditKeyResult(log, nfcKeyToCardOps.build(), cryptoInput); } log.add(LogType.MSG_MF_SUCCESS, indent); @@ -995,9 +1124,7 @@ public class PgpKeyOperation { * otherwise. */ private PgpEditKeyResult internalRestricted(PGPSecretKeyRing sKR, SaveKeyringParcel saveParcel, - OperationLog log) { - - int indent = 1; + OperationLog log, int indent) { progress(R.string.progress_modify, 0); @@ -1042,6 +1169,9 @@ public class PgpKeyOperation { indent + 1, KeyFormattingUtils.convertKeyIdToHex(change.mKeyId)); return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); } + log.add(LogType.MSG_MF_KEYTOCARD_FINISH, indent + 1, + KeyFormattingUtils.convertKeyIdToHex(change.mKeyId), + Hex.toHexString(change.mDummyDivert, 8, 6)); sKey = PGPSecretKey.constructGnuDummyKey(sKey.getPublicKey(), change.mDummyDivert); } sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey); @@ -1076,7 +1206,7 @@ public class PgpKeyOperation { // add packet with EMPTY notation data (updates old one, but will be stripped later) PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( masterPrivateKey.getPublicKeyPacket().getAlgorithm(), - PgpConstants.SECRET_KEY_SIGNATURE_HASH_ALGO) + PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO) .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder); { // set subpackets @@ -1103,7 +1233,7 @@ public class PgpKeyOperation { // add packet with "pin" notation data PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( masterPrivateKey.getPublicKeyPacket().getAlgorithm(), - PgpConstants.SECRET_KEY_SIGNATURE_HASH_ALGO) + PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO) .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder); { // set subpackets @@ -1150,13 +1280,13 @@ public class PgpKeyOperation { OperationLog log, int indent) throws PGPException { PGPDigestCalculator encryptorHashCalc = new JcaPGPDigestCalculatorProviderBuilder().build() - .get(PgpConstants.SECRET_KEY_ENCRYPTOR_HASH_ALGO); + .get(PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_HASH_ALGO); PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.getCharArray()); // Build key encryptor based on new passphrase PBESecretKeyEncryptor keyEncryptorNew = new JcePBESecretKeyEncryptorBuilder( - PgpConstants.SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO, encryptorHashCalc, - PgpConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT) + PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO, encryptorHashCalc, + PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT) .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(newPassphrase.getCharArray()); // noinspection unchecked @@ -1296,22 +1426,27 @@ public class PgpKeyOperation { static PGPSignatureGenerator getSignatureGenerator( PGPSecretKey secretKey, CryptoInputParcel cryptoInput) { - PGPContentSignerBuilder builder; - S2K s2k = secretKey.getS2K(); - if (s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K - && s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + boolean isDivertToCard = s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K + && s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD; + + return getSignatureGenerator(secretKey.getPublicKey(), cryptoInput, isDivertToCard); + } + + static PGPSignatureGenerator getSignatureGenerator( + PGPPublicKey pKey, CryptoInputParcel cryptoInput, boolean divertToCard) { + + PGPContentSignerBuilder builder; + if (divertToCard) { // use synchronous "NFC based" SignerBuilder builder = new NfcSyncPGPContentSignerBuilder( - secretKey.getPublicKey().getAlgorithm(), - PgpConstants.SECRET_KEY_SIGNATURE_HASH_ALGO, - secretKey.getKeyID(), cryptoInput.getCryptoData()) + pKey.getAlgorithm(), PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO, + pKey.getKeyID(), cryptoInput.getCryptoData()) .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); } else { // content signer based on signing key algorithm and chosen hash algorithm builder = new JcaPGPContentSignerBuilder( - secretKey.getPublicKey().getAlgorithm(), - PgpConstants.SECRET_KEY_SIGNATURE_HASH_ALGO) + pKey.getAlgorithm(), PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO) .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); } @@ -1319,11 +1454,9 @@ public class PgpKeyOperation { } - private PGPSignature generateUserIdSignature( - PGPSignatureGenerator sGen, Date creationTime, - PGPPrivateKey masterPrivateKey, PGPPublicKey pKey, String userId, boolean primary, - int flags, long expiry) - throws IOException, PGPException, SignatureException { + private static PGPSignatureSubpacketGenerator generateHashedSelfSigSubpackets( + Date creationTime, PGPPublicKey pKey, boolean primary, int flags, long expiry + ) { PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator(); { @@ -1339,11 +1472,11 @@ public class PgpKeyOperation { */ /* non-critical subpackets: */ hashedPacketsGen.setPreferredSymmetricAlgorithms(false, - PgpConstants.getAsArray(PgpConstants.sPreferredSymmetricAlgorithms)); + PgpSecurityConstants.PREFERRED_SYMMETRIC_ALGORITHMS); hashedPacketsGen.setPreferredHashAlgorithms(false, - PgpConstants.getAsArray(PgpConstants.sPreferredHashAlgorithms)); + PgpSecurityConstants.PREFERRED_HASH_ALGORITHMS); hashedPacketsGen.setPreferredCompressionAlgorithms(false, - PgpConstants.getAsArray(PgpConstants.sPreferredCompressionAlgorithms)); + PgpSecurityConstants.PREFERRED_COMPRESSION_ALGORITHMS); hashedPacketsGen.setPrimaryUserID(false, primary); /* critical subpackets: we consider those important for a modern pgp implementation */ @@ -1357,6 +1490,17 @@ public class PgpKeyOperation { } } + return hashedPacketsGen; + } + + private static PGPSignature generateUserIdSignature( + PGPSignatureGenerator sGen, Date creationTime, + PGPPrivateKey masterPrivateKey, PGPPublicKey pKey, String userId, boolean primary, + int flags, long expiry) + throws IOException, PGPException, SignatureException { + + PGPSignatureSubpacketGenerator hashedPacketsGen = + generateHashedSelfSigSubpackets(creationTime, pKey, primary, flags, expiry); sGen.setHashedSubpackets(hashedPacketsGen.generate()); sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey); return sGen.generateCertification(userId, pKey); @@ -1365,15 +1509,12 @@ public class PgpKeyOperation { private static PGPSignature generateUserAttributeSignature( PGPSignatureGenerator sGen, Date creationTime, PGPPrivateKey masterPrivateKey, PGPPublicKey pKey, - PGPUserAttributeSubpacketVector vector) + PGPUserAttributeSubpacketVector vector, + int flags, long expiry) throws IOException, PGPException, SignatureException { - PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator(); - { - /* critical subpackets: we consider those important for a modern pgp implementation */ - hashedPacketsGen.setSignatureCreationTime(true, creationTime); - } - + PGPSignatureSubpacketGenerator hashedPacketsGen = + generateHashedSelfSigSubpackets(creationTime, pKey, false, flags, expiry); sGen.setHashedSubpackets(hashedPacketsGen.generate()); sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey); return sGen.generateCertification(vector, pKey); @@ -1385,6 +1526,9 @@ public class PgpKeyOperation { throws IOException, PGPException, SignatureException { PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator(); + // we use the tag NO_REASON since gnupg does not care about the tag while verifying + // signatures with a revoked key, the warning is the same + subHashedPacketsGen.setRevocationReason(true, RevocationReasonTags.NO_REASON, ""); subHashedPacketsGen.setSignatureCreationTime(true, creationTime); sGen.setHashedSubpackets(subHashedPacketsGen.generate()); sGen.init(PGPSignature.CERTIFICATION_REVOCATION, masterPrivateKey); @@ -1397,6 +1541,9 @@ public class PgpKeyOperation { throws IOException, PGPException, SignatureException { PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator(); + // we use the tag NO_REASON since gnupg does not care about the tag while verifying + // signatures with a revoked key, the warning is the same + subHashedPacketsGen.setRevocationReason(true, RevocationReasonTags.NO_REASON, ""); subHashedPacketsGen.setSignatureCreationTime(true, creationTime); sGen.setHashedSubpackets(subHashedPacketsGen.generate()); // Generate key revocation or subkey revocation, depending on master/subkey-ness @@ -1412,7 +1559,8 @@ public class PgpKeyOperation { static PGPSignature generateSubkeyBindingSignature( PGPSignatureGenerator sGen, Date creationTime, PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey, - PGPPrivateKey subPrivateKey, PGPPublicKey pKey, int flags, long expiry) + PGPSignatureGenerator subSigGen, PGPPrivateKey subPrivateKey, PGPPublicKey pKey, + int flags, long expiry) throws IOException, PGPException, SignatureException { PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator(); @@ -1422,10 +1570,6 @@ public class PgpKeyOperation { // cross-certify signing keys PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator(); subHashedPacketsGen.setSignatureCreationTime(false, creationTime); - PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( - pKey.getAlgorithm(), PgpConstants.SECRET_KEY_SIGNATURE_HASH_ALGO) - .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); - PGPSignatureGenerator subSigGen = new PGPSignatureGenerator(signerBuilder); subSigGen.init(PGPSignature.PRIMARYKEY_BINDING, subPrivateKey); subSigGen.setHashedSubpackets(subHashedPacketsGen.generate()); PGPSignature certification = subSigGen.generateCertification(masterPublicKey, pKey); @@ -1471,14 +1615,39 @@ public class PgpKeyOperation { private static boolean isDummy(PGPSecretKey secretKey) { S2K s2k = secretKey.getS2K(); - return s2k.getType() == S2K.GNU_DUMMY_S2K - && s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY; + return s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K + && s2k.getProtectionMode() != S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD; } private static boolean isDivertToCard(PGPSecretKey secretKey) { S2K s2k = secretKey.getS2K(); - return s2k.getType() == S2K.GNU_DUMMY_S2K + return s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K && s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD; } + private static boolean checkSmartCardCompatibility(PGPSecretKey key, OperationLog log, int indent) { + PGPPublicKey publicKey = key.getPublicKey(); + int algorithm = publicKey.getAlgorithm(); + if (algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT && + algorithm != PublicKeyAlgorithmTags.RSA_SIGN && + algorithm != PublicKeyAlgorithmTags.RSA_GENERAL) { + log.add(LogType.MSG_MF_ERROR_BAD_NFC_ALGO, indent + 1); + return false; + } + + // Key size must be 2048 + int keySize = publicKey.getBitStrength(); + if (keySize != 2048) { + log.add(LogType.MSG_MF_ERROR_BAD_NFC_SIZE, indent + 1); + return false; + } + + // Secret key parts must be available + if (isDivertToCard(key) || isDummy(key)) { + log.add(LogType.MSG_MF_ERROR_BAD_NFC_STRIPPED, indent + 1); + return false; + } + + return true; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java new file mode 100644 index 000000000..3fa549946 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2015 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.asn1.nist.NISTNamedCurves; +import org.spongycastle.bcpg.CompressionAlgorithmTags; +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.bcpg.PublicKeyAlgorithmTags; +import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags; + +import java.util.HashSet; + +/** + * NIST requirements for 2011-2030 (http://www.keylength.com/en/4/): + * - RSA: 2048 bit + * - ECC: 224 bit + * - Symmetric: 3TDEA + * - Digital Signature (hash A): SHA-224 - SHA-512 + * + * Extreme Decisions for Yahoo's End-to-End: + * https://github.com/yahoo/end-to-end/issues/31 + * https://gist.github.com/coruus/68a8c65571e2b4225a69 + */ +public class PgpSecurityConstants { + + /** + * Whitelist of accepted symmetric encryption algorithms + * all other algorithms are rejected with OpenPgpDecryptionResult.RESULT_INSECURE + */ + private static HashSet<Integer> sSymmetricAlgorithmsWhitelist = new HashSet<>(); + static { + // General remarks: We try to keep the whitelist short to reduce attack surface + // TODO: block IDEA?: Bad key schedule (weak keys), implementation difficulties (easy to make errors) + sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.IDEA); + sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.TRIPLE_DES); // a MUST in RFC + sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.CAST5); // default in many gpg, pgp versions, 128 bit key + // BLOWFISH: Twofish is the successor + // SAFER: not used widely + // DES: < 128 bit security + sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_128); + sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_192); + sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_256); + sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.TWOFISH); // 128 bit + // CAMELLIA_128: not used widely + // CAMELLIA_192: not used widely + // CAMELLIA_256: not used widely + } + + public static boolean isSecureSymmetricAlgorithm(int id) { + return sSymmetricAlgorithmsWhitelist.contains(id); + } + + /** + * Whitelist of accepted hash algorithms + * all other algorithms are rejected with OpenPgpSignatureResult.RESULT_INSECURE + * + * coorus: + * Implementations SHOULD use SHA-512 for RSA or DSA signatures. They SHOULD NOT use SHA-384. + * ((cite to affine padding attacks; unproven status of RSA-PKCSv15)) + * + * Implementations MUST NOT sign SHA-224 hashes. They SHOULD NOT accept signatures over SHA-224 hashes. + * ((collision resistance of 112-bits)) + * Implementations SHOULD NOT sign SHA-256 hashes. They MUST NOT default to signing SHA-256 hashes. + */ + private static HashSet<Integer> sHashAlgorithmsWhitelist = new HashSet<>(); + static { + // MD5: broken + // SHA1: broken + // RIPEMD160: same security properties as SHA1 + // DOUBLE_SHA: not used widely + // MD2: not used widely + // TIGER_192: not used widely + // HAVAL_5_160: not used widely + sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA256); // compatibility for old Mailvelope versions + sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA384); + sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA512); + // SHA224: Not used widely, Yahoo argues against it + } + + public static boolean isSecureHashAlgorithm(int id) { + return sHashAlgorithmsWhitelist.contains(id); + } + + /** + * Whitelist of accepted asymmetric algorithms in switch statement + * all other algorithms are rejected with OpenPgpSignatureResult.RESULT_INSECURE or + * OpenPgpDecryptionResult.RESULT_INSECURE + * + * coorus: + * Implementations MUST NOT accept, or treat any signature as valid, by an RSA key with + * bitlength less than 1023 bits. + * Implementations MUST NOT accept any RSA keys with bitlength less than 2047 bits after January 1, 2016. + */ + private static HashSet<String> sCurveWhitelist = new HashSet<>(); + static { + sCurveWhitelist.add(NISTNamedCurves.getOID("P-256").getId()); + sCurveWhitelist.add(NISTNamedCurves.getOID("P-384").getId()); + sCurveWhitelist.add(NISTNamedCurves.getOID("P-521").getId()); + } + + public static boolean isSecureKey(CanonicalizedPublicKey key) { + switch (key.getAlgorithm()) { + case PublicKeyAlgorithmTags.RSA_GENERAL: { + return (key.getBitStrength() >= 2048); + } + // RSA_ENCRYPT, RSA_SIGN: deprecated in RFC 4880, use RSA_GENERAL with key flags + case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT: { + return (key.getBitStrength() >= 2048); + } + case PublicKeyAlgorithmTags.DSA: { + return (key.getBitStrength() >= 2048); + } + case PublicKeyAlgorithmTags.ECDH: + case PublicKeyAlgorithmTags.ECDSA: { + return PgpSecurityConstants.sCurveWhitelist.contains(key.getCurveOid()); + } + // ELGAMAL_GENERAL: deprecated in RFC 4880, use ELGAMAL_ENCRYPT + // DIFFIE_HELLMAN: unsure + default: + return false; + } + } + + /** + * These array is written as a list of preferred encryption algorithms into keys created by us. + * Other implementations may choose to honor this selection. + * (Most preferred is first) + * + * REASON: See corresponding whitelist. AES received most cryptanalysis over the years + * and is still secure! + */ + public static final int[] PREFERRED_SYMMETRIC_ALGORITHMS = new int[]{ + SymmetricKeyAlgorithmTags.AES_256, + SymmetricKeyAlgorithmTags.AES_192, + SymmetricKeyAlgorithmTags.AES_128, + }; + + /** + * These array is written as a list of preferred hash algorithms into keys created by us. + * Other implementations may choose to honor this selection. + * (Most preferred is first) + * + * REASON: See corresponding whitelist. If possible use SHA-512, this is state of the art! + */ + public static final int[] PREFERRED_HASH_ALGORITHMS = new int[]{ + HashAlgorithmTags.SHA512, + }; + + /** + * These array is written as a list of preferred compression algorithms into keys created by us. + * Other implementations may choose to honor this selection. + * (Most preferred is first) + * + * REASON: See DEFAULT_COMPRESSION_ALGORITHM + */ + public static final int[] PREFERRED_COMPRESSION_ALGORITHMS = new int[]{ + CompressionAlgorithmTags.ZIP, + }; + + /** + * Hash algorithm used to certify public keys + */ + public static final int CERTIFY_HASH_ALGO = HashAlgorithmTags.SHA512; + + + /** + * Always use AES-256! We always ignore the preferred encryption algos of the recipient! + * + * coorus: + * Implementations SHOULD ignore the symmetric algorithm preferences of a recipient's public key; + * in particular, implementations MUST NOT choose an algorithm forbidden by this + * document because a recipient prefers it. + * + * NEEDCITE downgrade attacks on TLS, other protocols + */ + public static final int DEFAULT_SYMMETRIC_ALGORITHM = SymmetricKeyAlgorithmTags.AES_256; + + public interface OpenKeychainSymmetricKeyAlgorithmTags extends SymmetricKeyAlgorithmTags { + int USE_DEFAULT = -1; + } + + /** + * Always use SHA-512! We always ignore the preferred hash algos of the recipient! + * + * coorus: + * Implementations MUST ignore the hash algorithm preferences of a recipient when signing + * a message to a recipient. The difficulty of forging a signature under a given key, + * using generic attacks on hash functions, is the difficulty of the weakest hash signed by that key. + * + * Implementations MUST default to using SHA-512 for RSA signatures, + * + * and either SHA-512 or the matched instance of SHA-2 for ECDSA signatures. + * TODO: Ed25519 + * CITE: zooko's hash function table CITE: distinguishers on SHA-256 + */ + public static final int DEFAULT_HASH_ALGORITHM = HashAlgorithmTags.SHA512; + + public interface OpenKeychainHashAlgorithmTags extends HashAlgorithmTags { + int USE_DEFAULT = -1; + } + + /** + * Compression is disabled by default. + * + * The default compression algorithm is only used if explicitly enabled in the activity's + * overflow menu or via the OpenPGP API's extra OpenPgpApi.EXTRA_ENABLE_COMPRESSION + * + * REASON: Enabling compression can lead to a sidechannel. Consider a voting that is done via + * OpenPGP. Compression can lead to different ciphertext lengths based on the user's voting. + * This has happened in a voting done by Wikipedia (Google it). + * + * ZLIB: the format provides no benefits over DEFLATE, and is more malleable + * BZIP2: very slow + */ + public static final int DEFAULT_COMPRESSION_ALGORITHM = CompressionAlgorithmTags.ZIP; + + public interface OpenKeychainCompressionAlgorithmTags extends CompressionAlgorithmTags { + int USE_DEFAULT = -1; + } + + /** + * Note: s2kcount is a number between 0 and 0xff that controls the + * number of times to iterate the password hash before use. More + * iterations are useful against offline attacks, as it takes more + * time to check each password. The actual number of iterations is + * rather complex, and also depends on the hash function in use. + * Refer to Section 3.7.1.3 in rfc4880.txt. Bigger numbers give + * you more iterations. As a rough rule of thumb, when using + * SHA256 as the hashing function, 0x10 gives you about 64 + * iterations, 0x20 about 128, 0x30 about 256 and so on till 0xf0, + * or about 1 million iterations. The maximum you can go to is + * 0xff, or about 2 million iterations. + * from http://kbsriram.com/2013/01/generating-rsa-keys-with-bouncycastle.html + * + * Bouncy Castle default: 0x60 + * kbsriram proposes: 0xc0 + * Yahoo's End-to-End: 96=0x60 (65536 iterations) (https://github.com/yahoo/end-to-end/blob/master/src/javascript/crypto/e2e/openpgp/keyring.js) + */ + public static final int SECRET_KEY_ENCRYPTOR_S2K_COUNT = 0x90; + public static final int SECRET_KEY_ENCRYPTOR_HASH_ALGO = HashAlgorithmTags.SHA512; + public static final int SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO = SymmetricKeyAlgorithmTags.AES_256; + public static final int SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO = HashAlgorithmTags.SHA512; + // NOTE: only SHA1 is supported for key checksum calculations in OpenPGP, + // see http://tools.ietf.org/html/rfc488 0#section-5.5.3 + public static final int SECRET_KEY_SIGNATURE_CHECKSUM_HASH_ALGO = HashAlgorithmTags.SHA1; + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java index fd3c4910c..36d1a07cb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java @@ -20,13 +20,8 @@ package org.sufficientlysecure.keychain.pgp; import org.spongycastle.bcpg.CompressionAlgorithmTags; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.util.Passphrase; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.Map; - import android.os.Parcel; import android.os.Parcelable; @@ -35,19 +30,20 @@ public class PgpSignEncryptInputParcel implements Parcelable { protected String mVersionHeader = null; protected boolean mEnableAsciiArmorOutput = false; - protected int mCompressionId = CompressionAlgorithmTags.UNCOMPRESSED; + protected int mCompressionAlgorithm = CompressionAlgorithmTags.UNCOMPRESSED; protected long[] mEncryptionMasterKeyIds = null; protected Passphrase mSymmetricPassphrase = null; - protected int mSymmetricEncryptionAlgorithm = PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED; + protected int mSymmetricEncryptionAlgorithm = PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT; protected long mSignatureMasterKeyId = Constants.key.none; protected Long mSignatureSubKeyId = null; - protected int mSignatureHashAlgorithm = PgpConstants.OpenKeychainHashAlgorithmTags.USE_PREFERRED; + protected int mSignatureHashAlgorithm = PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT; protected long mAdditionalEncryptId = Constants.key.none; protected boolean mFailOnMissingEncryptionKeyIds = false; protected String mCharset; protected boolean mCleartextSignature; protected boolean mDetachedSignature = false; protected boolean mHiddenRecipients = false; + protected boolean mIntegrityProtected = true; public PgpSignEncryptInputParcel() { @@ -60,7 +56,7 @@ public class PgpSignEncryptInputParcel implements Parcelable { // we do all of those here, so the PgpSignEncryptInput class doesn't have to be parcelable mVersionHeader = source.readString(); mEnableAsciiArmorOutput = source.readInt() == 1; - mCompressionId = source.readInt(); + mCompressionAlgorithm = source.readInt(); mEncryptionMasterKeyIds = source.createLongArray(); mSymmetricPassphrase = source.readParcelable(loader); mSymmetricEncryptionAlgorithm = source.readInt(); @@ -73,6 +69,7 @@ public class PgpSignEncryptInputParcel implements Parcelable { mCleartextSignature = source.readInt() == 1; mDetachedSignature = source.readInt() == 1; mHiddenRecipients = source.readInt() == 1; + mIntegrityProtected = source.readInt() == 1; } @Override @@ -84,7 +81,7 @@ public class PgpSignEncryptInputParcel implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeString(mVersionHeader); dest.writeInt(mEnableAsciiArmorOutput ? 1 : 0); - dest.writeInt(mCompressionId); + dest.writeInt(mCompressionAlgorithm); dest.writeLongArray(mEncryptionMasterKeyIds); dest.writeParcelable(mSymmetricPassphrase, 0); dest.writeInt(mSymmetricEncryptionAlgorithm); @@ -102,6 +99,7 @@ public class PgpSignEncryptInputParcel implements Parcelable { dest.writeInt(mCleartextSignature ? 1 : 0); dest.writeInt(mDetachedSignature ? 1 : 0); dest.writeInt(mHiddenRecipients ? 1 : 0); + dest.writeInt(mIntegrityProtected ? 1 : 0); } public String getCharset() { @@ -179,12 +177,12 @@ public class PgpSignEncryptInputParcel implements Parcelable { return this; } - public int getCompressionId() { - return mCompressionId; + public int getCompressionAlgorithm() { + return mCompressionAlgorithm; } - public PgpSignEncryptInputParcel setCompressionId(int compressionId) { - mCompressionId = compressionId; + public PgpSignEncryptInputParcel setCompressionAlgorithm(int compressionAlgorithm) { + mCompressionAlgorithm = compressionAlgorithm; return this; } @@ -234,6 +232,18 @@ public class PgpSignEncryptInputParcel implements Parcelable { return this; } + public boolean isIntegrityProtected() { + return mIntegrityProtected; + } + + /** + * Only use for testing! Never disable integrity protection! + */ + public PgpSignEncryptInputParcel setIntegrityProtected(boolean integrityProtected) { + this.mIntegrityProtected = integrityProtected; + return this; + } + public boolean isHiddenRecipients() { return mHiddenRecipients; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java index 9073e81b9..29b2ef727 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java @@ -20,6 +20,8 @@ package org.sufficientlysecure.keychain.pgp; import android.content.Context; +import android.os.Parcelable; +import android.support.annotation.NonNull; import org.spongycastle.bcpg.ArmoredOutputStream; import org.spongycastle.bcpg.BCPGOutputStream; @@ -36,11 +38,11 @@ import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.BaseOperation; +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.PgpSignEncryptResult; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; @@ -60,9 +62,9 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.security.SignatureException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -99,6 +101,13 @@ public class PgpSignEncryptOperation extends BaseOperation { super(context, providerHelper, progressable); } + @NonNull + @Override + // TODO this is horrible, refactor ASAP!! + public OperationResult execute(Parcelable input, CryptoInputParcel cryptoInput) { + return null; + } + /** * Signs and/or encrypts data based on parameters of class */ @@ -114,7 +123,7 @@ public class PgpSignEncryptOperation extends BaseOperation { boolean enableSignature = input.getSignatureMasterKeyId() != Constants.key.none; boolean enableEncryption = ((input.getEncryptionMasterKeyIds() != null && input.getEncryptionMasterKeyIds().length > 0) || input.getSymmetricPassphrase() != null); - boolean enableCompression = (input.getCompressionId() != CompressionAlgorithmTags.UNCOMPRESSED); + boolean enableCompression = (input.getCompressionAlgorithm() != CompressionAlgorithmTags.UNCOMPRESSED); Log.d(Constants.TAG, "enableSignature:" + enableSignature + "\nenableEncryption:" + enableEncryption @@ -189,7 +198,7 @@ public class PgpSignEncryptOperation extends BaseOperation { log.add(LogType.MSG_PSE_PENDING_PASSPHRASE, indent + 1); return new PgpSignEncryptResult(log, RequiredInputParcel.createRequiredSignPassphrase( signingKeyRing.getMasterKeyId(), signingKey.getKeyId(), - cryptoInput.getSignatureTime())); + cryptoInput.getSignatureTime()), cryptoInput); } if (!signingKey.unlock(localPassphrase)) { log.add(LogType.MSG_PSE_ERROR_BAD_PASSPHRASE, indent); @@ -216,15 +225,10 @@ public class PgpSignEncryptOperation extends BaseOperation { return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); } - // Use preferred hash algo + // Use requested hash algo int requestedAlgorithm = input.getSignatureHashAlgorithm(); - ArrayList<Integer> supported = signingKey.getSupportedHashAlgorithms(); - if (requestedAlgorithm == PgpConstants.OpenKeychainHashAlgorithmTags.USE_PREFERRED) { - // get most preferred - input.setSignatureHashAlgorithm(supported.get(0)); - } else if (!supported.contains(requestedAlgorithm)) { - log.add(LogType.MSG_PSE_ERROR_HASH_ALGO, indent); - return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + if (requestedAlgorithm == PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT) { + input.setSignatureHashAlgorithm(PgpSecurityConstants.DEFAULT_HASH_ALGORITHM); } } updateProgress(R.string.progress_preparing_streams, 2, 100); @@ -233,18 +237,15 @@ public class PgpSignEncryptOperation extends BaseOperation { PGPEncryptedDataGenerator cPk = null; if (enableEncryption) { - // Use preferred encryption algo + // Use requested encryption algo int algo = input.getSymmetricEncryptionAlgorithm(); - if (algo == PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED) { - // get most preferred - // TODO: get from recipients - algo = PgpConstants.sPreferredSymmetricAlgorithms.get(0); + if (algo == PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT) { + algo = PgpSecurityConstants.DEFAULT_SYMMETRIC_ALGORITHM; } - // has Integrity packet enabled! JcePGPDataEncryptorBuilder encryptorBuilder = new JcePGPDataEncryptorBuilder(algo) .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME) - .setWithIntegrityPacket(true); + .setWithIntegrityPacket(input.isIntegrityProtected()); cPk = new PGPEncryptedDataGenerator(encryptorBuilder); @@ -263,15 +264,19 @@ public class PgpSignEncryptOperation extends BaseOperation { try { CanonicalizedPublicKeyRing keyRing = mProviderHelper.getCanonicalizedPublicKeyRing( KeyRings.buildUnifiedKeyRingUri(id)); - CanonicalizedPublicKey key = keyRing.getEncryptionSubKey(); - cPk.addMethod(key.getPubKeyEncryptionGenerator(input.isHiddenRecipients())); - log.add(LogType.MSG_PSE_KEY_OK, indent + 1, - KeyFormattingUtils.convertKeyIdToHex(id)); - } catch (PgpKeyNotFoundException e) { - log.add(LogType.MSG_PSE_KEY_WARN, indent + 1, - KeyFormattingUtils.convertKeyIdToHex(id)); - if (input.isFailOnMissingEncryptionKeyIds()) { - return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + Set<Long> encryptSubKeyIds = keyRing.getEncryptIds(); + for (Long subKeyId : encryptSubKeyIds) { + CanonicalizedPublicKey key = keyRing.getPublicKey(subKeyId); + cPk.addMethod(key.getPubKeyEncryptionGenerator(input.isHiddenRecipients())); + log.add(LogType.MSG_PSE_KEY_OK, indent + 1, + KeyFormattingUtils.convertKeyIdToHex(subKeyId)); + } + if (encryptSubKeyIds.isEmpty()) { + log.add(LogType.MSG_PSE_KEY_WARN, indent + 1, + KeyFormattingUtils.convertKeyIdToHex(id)); + if (input.isFailOnMissingEncryptionKeyIds()) { + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } } } catch (ProviderHelper.NotFoundException e) { log.add(LogType.MSG_PSE_KEY_UNKNOWN, indent + 1, @@ -327,7 +332,13 @@ public class PgpSignEncryptOperation extends BaseOperation { if (enableCompression) { log.add(LogType.MSG_PSE_COMPRESSING, indent); - compressGen = new PGPCompressedDataGenerator(input.getCompressionId()); + + // Use preferred compression algo + int algo = input.getCompressionAlgorithm(); + if (algo == PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.USE_DEFAULT) { + algo = PgpSecurityConstants.DEFAULT_COMPRESSION_ALGORITHM; + } + compressGen = new PGPCompressedDataGenerator(algo); bcpgOut = new BCPGOutputStream(compressGen.open(encryptionOut)); } else { bcpgOut = new BCPGOutputStream(encryptionOut); @@ -450,7 +461,7 @@ public class PgpSignEncryptOperation extends BaseOperation { InputStream in = inputData.getInputStream(); if (enableCompression) { - compressGen = new PGPCompressedDataGenerator(input.getCompressionId()); + compressGen = new PGPCompressedDataGenerator(input.getCompressionAlgorithm()); bcpgOut = new BCPGOutputStream(compressGen.open(out)); } else { bcpgOut = new BCPGOutputStream(out); @@ -497,7 +508,8 @@ public class PgpSignEncryptOperation extends BaseOperation { // this secret key diverts to a OpenPGP card, throw exception with hash that will be signed log.add(LogType.MSG_PSE_PENDING_NFC, indent); return new PgpSignEncryptResult(log, RequiredInputParcel.createNfcSignOperation( - e.hashToSign, e.hashAlgo, cryptoInput.getSignatureTime())); + signingKey.getRing().getMasterKeyId(), signingKey.getKeyId(), + e.hashToSign, e.hashAlgo, cryptoInput.getSignatureTime()), cryptoInput); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SignEncryptParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SignEncryptParcel.java index 464de37f5..8f80a4802 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SignEncryptParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/SignEncryptParcel.java @@ -57,6 +57,10 @@ public class SignEncryptParcel extends PgpSignEncryptInputParcel { } + public boolean isIncomplete() { + return mInputUris.size() > mOutputUris.size(); + } + public byte[] getBytes() { return mBytes; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java index 2bb4f7dc4..a7baddf8b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java @@ -48,6 +48,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -78,7 +79,7 @@ import java.util.TreeSet; * */ @SuppressWarnings("unchecked") -public class UncachedKeyRing { +public class UncachedKeyRing implements Serializable { final PGPKeyRing mRing; final boolean mIsSecret; @@ -219,7 +220,7 @@ public class UncachedKeyRing { Iterator<PGPPublicKey> it = mRing.getPublicKeys(); while (it.hasNext()) { if (KeyFormattingUtils.convertFingerprintToHex( - it.next().getFingerprint()).equals(expectedFingerprint)) { + it.next().getFingerprint()).equalsIgnoreCase(expectedFingerprint)) { return true; } } @@ -820,6 +821,15 @@ public class UncachedKeyRing { continue; } + Date keyCreationTime = key.getCreationTime(), keyCreationTimeLenient; + { + Calendar keyCreationCal = Calendar.getInstance(); + keyCreationCal.setTime(keyCreationTime); + // allow for diverging clocks up to one day when checking creation time + keyCreationCal.add(Calendar.MINUTE, -5); + keyCreationTimeLenient = keyCreationCal.getTime(); + } + // A subkey needs exactly one subkey binding certificate, and optionally one revocation // certificate. PGPPublicKey modified = key; @@ -851,6 +861,18 @@ public class UncachedKeyRing { continue; } + if (cert.getCreationTime().before(keyCreationTime)) { + // Signature is earlier than key creation time + log.add(LogType.MSG_KC_SUB_BAD_TIME_EARLY, indent); + // due to an earlier accident, we generated keys which had creation timestamps + // a few seconds after their signature timestamp. for compatibility, we only + // error out with some margin of error + if (cert.getCreationTime().before(keyCreationTimeLenient)) { + badCerts += 1; + continue; + } + } + if (cert.isLocal()) { // Creation date in the future? No way! log.add(LogType.MSG_KC_SUB_BAD_LOCAL, indent); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java index 0173a1d83..013a6bf14 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java @@ -211,12 +211,19 @@ public class UncachedPublicKey { return getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT; } + public boolean isRSA() { + return getAlgorithm() == PGPPublicKey.RSA_GENERAL + || getAlgorithm() == PGPPublicKey.RSA_ENCRYPT + || getAlgorithm() == PGPPublicKey.RSA_SIGN; + } + public boolean isDSA() { return getAlgorithm() == PGPPublicKey.DSA; } public boolean isEC() { - return getAlgorithm() == PGPPublicKey.ECDH || getAlgorithm() == PGPPublicKey.ECDSA; + return getAlgorithm() == PGPPublicKey.ECDH + || getAlgorithm() == PGPPublicKey.ECDSA; } public byte[] getFingerprint() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java index 11d6728e2..ee28b5f36 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -51,6 +51,11 @@ public class KeychainContract { String EXPIRY = "expiry"; } + interface UpdatedKeysColumns { + String MASTER_KEY_ID = "master_key_id"; // not a database id + String LAST_UPDATED = "last_updated"; // time since epoch in seconds + } + interface UserPacketsColumns { String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID String TYPE = "type"; // not a database id @@ -90,13 +95,15 @@ public class KeychainContract { String PACKAGE_NAME = "package_name"; // foreign key to api_apps.package_name } - public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".provider"; + public static final String CONTENT_AUTHORITY = Constants.PROVIDER_AUTHORITY; private static final Uri BASE_CONTENT_URI_INTERNAL = Uri .parse("content://" + CONTENT_AUTHORITY); public static final String BASE_KEY_RINGS = "key_rings"; + public static final String BASE_UPDATED_KEYS = "updated_keys"; + public static final String PATH_UNIFIED = "unified"; public static final String PATH_FIND = "find"; @@ -235,6 +242,16 @@ public class KeychainContract { } + public static class UpdatedKeys implements UpdatedKeysColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_UPDATED_KEYS).build(); + + public static final String CONTENT_TYPE + = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.updated_keys"; + public static final String CONTENT_ITEM_TYPE + = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.updated_keys"; + } + public static class UserPackets implements UserPacketsColumns, BaseColumns { public static final String VERIFIED = "verified"; public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java index ff661e494..d7fb738fc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -34,6 +34,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns; +import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeysColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns; import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity; import org.sufficientlysecure.keychain.util.Log; @@ -53,7 +54,7 @@ import java.io.IOException; */ public class KeychainDatabase extends SQLiteOpenHelper { private static final String DATABASE_NAME = "openkeychain.db"; - private static final int DATABASE_VERSION = 9; + private static final int DATABASE_VERSION = 12; static Boolean apgHack = false; private Context mContext; @@ -61,6 +62,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { String KEY_RINGS_PUBLIC = "keyrings_public"; String KEY_RINGS_SECRET = "keyrings_secret"; String KEYS = "keys"; + String UPDATED_KEYS = "updated_keys"; String USER_PACKETS = "user_packets"; String CERTS = "certs"; String API_APPS = "api_apps"; @@ -144,6 +146,14 @@ public class KeychainDatabase extends SQLiteOpenHelper { + Tables.USER_PACKETS + "(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + ") ON DELETE CASCADE" + ")"; + private static final String CREATE_UPDATE_KEYS = + "CREATE TABLE IF NOT EXISTS " + Tables.UPDATED_KEYS + " (" + + UpdatedKeysColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY, " + + UpdatedKeysColumns.LAST_UPDATED + " INTEGER, " + + "FOREIGN KEY(" + UpdatedKeysColumns.MASTER_KEY_ID + ") REFERENCES " + + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" + + ")"; + private static final String CREATE_API_APPS = "CREATE TABLE IF NOT EXISTS " + Tables.API_APPS + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " @@ -179,7 +189,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { + Tables.API_APPS + "(" + ApiAppsAllowedKeysColumns.PACKAGE_NAME + ") ON DELETE CASCADE" + ")"; - KeychainDatabase(Context context) { + public KeychainDatabase(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; @@ -206,6 +216,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL(CREATE_KEYS); db.execSQL(CREATE_USER_PACKETS); db.execSQL(CREATE_CERTS); + db.execSQL(CREATE_UPDATE_KEYS); db.execSQL(CREATE_API_APPS); db.execSQL(CREATE_API_APPS_ACCOUNTS); db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); @@ -272,6 +283,19 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL("DROP TABLE IF EXISTS user_ids"); db.execSQL(CREATE_USER_PACKETS); db.execSQL(CREATE_CERTS); + case 10: + // do nothing here, just consolidate + case 11: + // fix problems in database, see #1402 for details + // https://github.com/open-keychain/open-keychain/issues/1402 + db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3"); + case 12: + db.execSQL(CREATE_UPDATE_KEYS); + if (oldVersion == 10) { + // no consolidate if we are updating from 10, we're just here for + // the api_accounts fix and the new update keys table + return; + } } @@ -295,10 +319,11 @@ public class KeychainDatabase extends SQLiteOpenHelper { // It's the Java way =( String[] dbs = context.databaseList(); for (String db : dbs) { - if (db.equals("apg.db")) { + if ("apg.db".equals(db)) { hasApgDb = true; - } else if (db.equals("apg_old.db")) { + } else if ("apg_old.db".equals(db)) { Log.d(Constants.TAG, "Found apg_old.db, delete it!"); + // noinspection ResultOfMethodCallIgnored - if it doesn't happen, it doesn't happen. context.getDatabasePath("apg_old.db").delete(); } } @@ -382,17 +407,22 @@ public class KeychainDatabase extends SQLiteOpenHelper { } } - // delete old database + // noinspection ResultOfMethodCallIgnored - not much we can do if this doesn't work context.getDatabasePath("apg.db").delete(); } private static void copy(File in, File out) throws IOException { FileInputStream is = new FileInputStream(in); FileOutputStream os = new FileOutputStream(out); - byte[] buf = new byte[512]; - while (is.available() > 0) { - int count = is.read(buf, 0, 512); - os.write(buf, 0, count); + try { + byte[] buf = new byte[512]; + while (is.available() > 0) { + int count = is.read(buf, 0, 512); + os.write(buf, 0, count); + } + } finally { + is.close(); + os.close(); } } @@ -409,6 +439,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { } else { in = context.getDatabasePath(DATABASE_NAME); out = context.getDatabasePath("debug_backup.db"); + // noinspection ResultOfMethodCallIgnored - this is a pure debug feature, anyways out.createNewFile(); } if (!in.canRead()) { @@ -423,6 +454,9 @@ public class KeychainDatabase extends SQLiteOpenHelper { // DANGEROUS, use in test code ONLY! public void clearDatabase() { getWritableDatabase().execSQL("delete from " + Tables.KEY_RINGS_PUBLIC); + getWritableDatabase().execSQL("delete from " + Tables.API_ACCOUNTS); + getWritableDatabase().execSQL("delete from " + Tables.API_ALLOWED_KEYS); + getWritableDatabase().execSQL("delete from " + Tables.API_APPS); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index ecb26b56a..d722fa9e7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -39,6 +39,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; @@ -75,6 +76,9 @@ public class KeychainProvider extends ContentProvider { private static final int KEY_RINGS_FIND_BY_EMAIL = 400; private static final int KEY_RINGS_FIND_BY_SUBKEY = 401; + private static final int UPDATED_KEYS = 500; + private static final int UPDATED_KEYS_SPECIFIC = 501; + protected UriMatcher mUriMatcher; /** @@ -192,6 +196,12 @@ public class KeychainProvider extends ContentProvider { matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/" + KeychainContract.PATH_ALLOWED_KEYS, API_ALLOWED_KEYS); + /** + * to access table containing last updated dates of keys + */ + matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS, UPDATED_KEYS); + matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS + "/*", UPDATED_KEYS_SPECIFIC); + return matcher; } @@ -231,6 +241,11 @@ public class KeychainProvider extends ContentProvider { case KEY_RING_SECRET: return KeyRings.CONTENT_ITEM_TYPE; + case UPDATED_KEYS: + return UpdatedKeys.CONTENT_TYPE; + case UPDATED_KEYS_SPECIFIC: + return UpdatedKeys.CONTENT_ITEM_TYPE; + case API_APPS: return ApiApps.CONTENT_TYPE; @@ -536,7 +551,6 @@ public class KeychainProvider extends ContentProvider { } break; - } case KEY_RINGS_PUBLIC: @@ -631,23 +645,42 @@ public class KeychainProvider extends ContentProvider { break; } - case API_APPS: + case UPDATED_KEYS: + case UPDATED_KEYS_SPECIFIC: { + HashMap<String, String> projectionMap = new HashMap<>(); + qb.setTables(Tables.UPDATED_KEYS); + projectionMap.put(UpdatedKeys.MASTER_KEY_ID, Tables.UPDATED_KEYS + "." + + UpdatedKeys.MASTER_KEY_ID); + projectionMap.put(UpdatedKeys.LAST_UPDATED, Tables.UPDATED_KEYS + "." + + UpdatedKeys.LAST_UPDATED); + qb.setProjectionMap(projectionMap); + if (match == UPDATED_KEYS_SPECIFIC) { + qb.appendWhere(UpdatedKeys.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + } + break; + } + + case API_APPS: { qb.setTables(Tables.API_APPS); break; - case API_APPS_BY_PACKAGE_NAME: + } + case API_APPS_BY_PACKAGE_NAME: { qb.setTables(Tables.API_APPS); qb.appendWhere(ApiApps.PACKAGE_NAME + " = "); qb.appendWhereEscapeString(uri.getLastPathSegment()); break; - case API_ACCOUNTS: + } + case API_ACCOUNTS: { qb.setTables(Tables.API_ACCOUNTS); qb.appendWhere(Tables.API_ACCOUNTS + "." + ApiAccounts.PACKAGE_NAME + " = "); qb.appendWhereEscapeString(uri.getPathSegments().get(1)); break; - case API_ACCOUNTS_BY_ACCOUNT_NAME: + } + case API_ACCOUNTS_BY_ACCOUNT_NAME: { qb.setTables(Tables.API_ACCOUNTS); qb.appendWhere(Tables.API_ACCOUNTS + "." + ApiAccounts.PACKAGE_NAME + " = "); qb.appendWhereEscapeString(uri.getPathSegments().get(1)); @@ -656,14 +689,17 @@ public class KeychainProvider extends ContentProvider { qb.appendWhereEscapeString(uri.getLastPathSegment()); break; - case API_ALLOWED_KEYS: + } + case API_ALLOWED_KEYS: { qb.setTables(Tables.API_ALLOWED_KEYS); qb.appendWhere(Tables.API_ALLOWED_KEYS + "." + ApiAccounts.PACKAGE_NAME + " = "); qb.appendWhereEscapeString(uri.getPathSegments().get(1)); break; - default: + } + default: { throw new IllegalArgumentException("Unknown URI " + uri + " (" + match + ")"); + } } @@ -708,47 +744,53 @@ public class KeychainProvider extends ContentProvider { final int match = mUriMatcher.match(uri); switch (match) { - case KEY_RING_PUBLIC: + case KEY_RING_PUBLIC: { db.insertOrThrow(Tables.KEY_RINGS_PUBLIC, null, values); keyId = values.getAsLong(KeyRings.MASTER_KEY_ID); break; - - case KEY_RING_SECRET: + } + case KEY_RING_SECRET: { db.insertOrThrow(Tables.KEY_RINGS_SECRET, null, values); keyId = values.getAsLong(KeyRings.MASTER_KEY_ID); break; - - case KEY_RING_KEYS: + } + case KEY_RING_KEYS: { db.insertOrThrow(Tables.KEYS, null, values); keyId = values.getAsLong(Keys.MASTER_KEY_ID); break; - - case KEY_RING_USER_IDS: + } + case KEY_RING_USER_IDS: { // iff TYPE is null, user_id MUST be null as well - if ( ! (values.get(UserPacketsColumns.TYPE) == null + if (!(values.get(UserPacketsColumns.TYPE) == null ? (values.get(UserPacketsColumns.USER_ID) != null && values.get(UserPacketsColumns.ATTRIBUTE_DATA) == null) : (values.get(UserPacketsColumns.ATTRIBUTE_DATA) != null && values.get(UserPacketsColumns.USER_ID) == null) - )) { + )) { throw new AssertionError("Incorrect type for user packet! This is a bug!"); } - if (values.get(UserPacketsColumns.RANK) == 0 && values.get(UserPacketsColumns.USER_ID) == null) { + if (((Number) values.get(UserPacketsColumns.RANK)).intValue() == 0 && values.get(UserPacketsColumns.USER_ID) == null) { throw new AssertionError("Rank 0 user packet must be a user id!"); } db.insertOrThrow(Tables.USER_PACKETS, null, values); keyId = values.getAsLong(UserPackets.MASTER_KEY_ID); break; - - case KEY_RING_CERTS: + } + case KEY_RING_CERTS: { // we replace here, keeping only the latest signature // TODO this would be better handled in savePublicKeyRing directly! db.replaceOrThrow(Tables.CERTS, null, values); keyId = values.getAsLong(Certs.MASTER_KEY_ID); break; - - case API_APPS: + } + case UPDATED_KEYS: { + long updatedKeyId = db.replace(Tables.UPDATED_KEYS, null, values); + rowUri = UpdatedKeys.CONTENT_URI.buildUpon().appendPath("" + updatedKeyId) + .build(); + break; + } + case API_APPS: { db.insertOrThrow(Tables.API_APPS, null, values); break; - + } case API_ACCOUNTS: { // set foreign key automatically based on given uri // e.g., api_apps/com.example.app/accounts/ @@ -767,8 +809,9 @@ public class KeychainProvider extends ContentProvider { db.insertOrThrow(Tables.API_ALLOWED_KEYS, null, values); break; } - default: + default: { throw new UnsupportedOperationException("Unknown uri: " + uri); + } } if (keyId != null) { @@ -826,20 +869,24 @@ public class KeychainProvider extends ContentProvider { break; } - case API_APPS_BY_PACKAGE_NAME: + case API_APPS_BY_PACKAGE_NAME: { count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, additionalSelection), selectionArgs); break; - case API_ACCOUNTS_BY_ACCOUNT_NAME: + } + case API_ACCOUNTS_BY_ACCOUNT_NAME: { count = db.delete(Tables.API_ACCOUNTS, buildDefaultApiAccountsSelection(uri, additionalSelection), selectionArgs); break; - case API_ALLOWED_KEYS: + } + case API_ALLOWED_KEYS: { count = db.delete(Tables.API_ALLOWED_KEYS, buildDefaultApiAllowedKeysSelection(uri, additionalSelection), selectionArgs); break; - default: + } + default: { throw new UnsupportedOperationException("Unknown uri: " + uri); + } } // notify of changes in db @@ -875,16 +922,19 @@ public class KeychainProvider extends ContentProvider { count = db.update(Tables.KEYS, values, actualSelection, selectionArgs); break; } - case API_APPS_BY_PACKAGE_NAME: + case API_APPS_BY_PACKAGE_NAME: { count = db.update(Tables.API_APPS, values, buildDefaultApiAppsSelection(uri, selection), selectionArgs); break; - case API_ACCOUNTS_BY_ACCOUNT_NAME: + } + case API_ACCOUNTS_BY_ACCOUNT_NAME: { count = db.update(Tables.API_ACCOUNTS, values, buildDefaultApiAccountsSelection(uri, selection), selectionArgs); break; - default: + } + default: { throw new UnsupportedOperationException("Unknown uri: " + uri); + } } // notify of changes in db diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java index bf56417e9..d9ef4f3c8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -26,6 +26,7 @@ import android.content.OperationApplicationException; import android.database.Cursor; import android.net.Uri; import android.os.RemoteException; +import android.support.annotation.NonNull; import android.support.v4.util.LongSparseArray; import org.spongycastle.bcpg.CompressionAlgorithmTags; @@ -38,7 +39,7 @@ import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.operations.ImportExportOperation; +import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; @@ -49,7 +50,7 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.PgpConstants; +import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; @@ -61,6 +62,8 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; import org.sufficientlysecure.keychain.remote.AccountSettings; import org.sufficientlysecure.keychain.remote.AppSettings; import org.sufficientlysecure.keychain.util.IterableIterator; @@ -81,6 +84,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * This class contains high level methods for database access. Despite its @@ -648,10 +652,6 @@ public class ProviderHelper { UserPacketItem item = uids.get(userIdRank); operations.add(buildUserIdOperations(masterKeyId, item, userIdRank)); - if (item.selfCert == null) { - throw new AssertionError("User ids MUST be self-certified at this point!!"); - } - if (item.selfRevocation != null) { operations.add(buildCertOperations(masterKeyId, userIdRank, item.selfRevocation, Certs.VERIFIED_SELF)); @@ -659,6 +659,10 @@ public class ProviderHelper { continue; } + if (item.selfCert == null) { + throw new AssertionError("User ids MUST be self-certified at this point!!"); + } + operations.add(buildCertOperations(masterKeyId, userIdRank, item.selfCert, selfCertsAreTrusted ? Certs.VERIFIED_SECRET : Certs.VERIFIED_SELF)); @@ -684,6 +688,36 @@ public class ProviderHelper { mIndent -= 1; } + // before deleting key, retrieve it's last updated time + final int INDEX_MASTER_KEY_ID = 0; + final int INDEX_LAST_UPDATED = 1; + Cursor lastUpdatedCursor = mContentResolver.query( + UpdatedKeys.CONTENT_URI, + new String[]{ + UpdatedKeys.MASTER_KEY_ID, + UpdatedKeys.LAST_UPDATED + }, + UpdatedKeys.MASTER_KEY_ID + " = ?", + new String[]{"" + masterKeyId}, + null + ); + if (lastUpdatedCursor.moveToNext()) { + // there was an entry to re-insert + // this operation must happen after the new key is inserted + ContentValues lastUpdatedEntry = new ContentValues(2); + lastUpdatedEntry.put(UpdatedKeys.MASTER_KEY_ID, + lastUpdatedCursor.getLong(INDEX_MASTER_KEY_ID)); + lastUpdatedEntry.put(UpdatedKeys.LAST_UPDATED, + lastUpdatedCursor.getLong(INDEX_LAST_UPDATED)); + operations.add( + ContentProviderOperation + .newInsert(UpdatedKeys.CONTENT_URI) + .withValues(lastUpdatedEntry) + .build() + ); + } + lastUpdatedCursor.close(); + try { // delete old version of this keyRing, which also deletes all keys and userIds on cascade int deleted = mContentResolver.delete( @@ -725,7 +759,7 @@ public class ProviderHelper { LongSparseArray<WrappedSignature> trustedCerts = new LongSparseArray<>(); @Override - public int compareTo(UserPacketItem o) { + public int compareTo(@NonNull UserPacketItem o) { // revoked keys always come last! //noinspection DoubleNegation if ((selfRevocation != null) != (o.selfRevocation != null)) { @@ -782,7 +816,7 @@ public class ProviderHelper { // first, mark all keys as not available ContentValues values = new ContentValues(); - values.put(Keys.HAS_SECRET, SecretKeyType.UNAVAILABLE.getNum()); + values.put(Keys.HAS_SECRET, SecretKeyType.GNU_DUMMY.getNum()); mContentResolver.update(uri, values, null, null); // then, mark exactly the keys we have available @@ -831,7 +865,7 @@ public class ProviderHelper { mIndent -= 1; // this implicitly leaves all keys which were not in the secret key ring - // with has_secret = 0 + // with has_secret = 1 } log(LogType.MSG_IS_SUCCESS); @@ -906,7 +940,8 @@ public class ProviderHelper { // If there is a secret key, merge new data (if any) and save the key for later CanonicalizedSecretKeyRing canSecretRing; try { - UncachedKeyRing secretRing = getCanonicalizedSecretKeyRing(publicRing.getMasterKeyId()).getUncachedKeyRing(); + UncachedKeyRing secretRing = getCanonicalizedSecretKeyRing(publicRing.getMasterKeyId()) + .getUncachedKeyRing(); // Merge data from new public ring into secret one log(LogType.MSG_IP_MERGE_SECRET); @@ -1031,7 +1066,8 @@ public class ProviderHelper { publicRing = secretRing.extractPublicKeyRing(); } - CanonicalizedPublicKeyRing canPublicRing = (CanonicalizedPublicKeyRing) publicRing.canonicalize(mLog, mIndent); + CanonicalizedPublicKeyRing canPublicRing = (CanonicalizedPublicKeyRing) publicRing.canonicalize(mLog, + mIndent); if (canPublicRing == null) { return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } @@ -1057,6 +1093,7 @@ public class ProviderHelper { } + @NonNull public ConsolidateResult consolidateDatabaseStep1(Progressable progress) { OperationLog log = new OperationLog(); @@ -1082,7 +1119,7 @@ public class ProviderHelper { indent += 1; final Cursor cursor = mContentResolver.query(KeyRingData.buildSecretKeyRingUri(), - new String[]{ KeyRingData.KEY_RING_DATA }, null, null, null); + new String[]{KeyRingData.KEY_RING_DATA}, null, null, null); if (cursor == null) { log.add(LogType.MSG_CON_ERROR_DB, indent); @@ -1124,6 +1161,7 @@ public class ProviderHelper { } }); + cursor.close(); } catch (IOException e) { Log.e(Constants.TAG, "error saving secret", e); @@ -1143,7 +1181,7 @@ public class ProviderHelper { final Cursor cursor = mContentResolver.query( KeyRingData.buildPublicKeyRingUri(), - new String[]{ KeyRingData.KEY_RING_DATA }, null, null, null); + new String[]{KeyRingData.KEY_RING_DATA}, null, null, null); if (cursor == null) { log.add(LogType.MSG_CON_ERROR_DB, indent); @@ -1185,6 +1223,7 @@ public class ProviderHelper { } }); + cursor.close(); } catch (IOException e) { Log.e(Constants.TAG, "error saving public", e); @@ -1200,12 +1239,14 @@ public class ProviderHelper { return consolidateDatabaseStep2(log, indent, progress, false); } + @NonNull public ConsolidateResult consolidateDatabaseStep2(Progressable progress) { return consolidateDatabaseStep2(new OperationLog(), 0, progress, true); } private static boolean mConsolidateCritical = false; + @NonNull private ConsolidateResult consolidateDatabaseStep2( OperationLog log, int indent, Progressable progress, boolean recovery) { @@ -1231,6 +1272,28 @@ public class ProviderHelper { } // 2. wipe database (IT'S DANGEROUS) + + // first, backup our list of updated key times + ArrayList<ContentValues> updatedKeysValues = new ArrayList<>(); + final int INDEX_MASTER_KEY_ID = 0; + final int INDEX_LAST_UPDATED = 1; + Cursor lastUpdatedCursor = mContentResolver.query( + UpdatedKeys.CONTENT_URI, + new String[]{ + UpdatedKeys.MASTER_KEY_ID, + UpdatedKeys.LAST_UPDATED + }, + null, null, null); + while (lastUpdatedCursor.moveToNext()) { + ContentValues values = new ContentValues(); + values.put(UpdatedKeys.MASTER_KEY_ID, + lastUpdatedCursor.getLong(INDEX_MASTER_KEY_ID)); + values.put(UpdatedKeys.LAST_UPDATED, + lastUpdatedCursor.getLong(INDEX_LAST_UPDATED)); + updatedKeysValues.add(values); + } + lastUpdatedCursor.close(); + log.add(LogType.MSG_CON_DB_CLEAR, indent); mContentResolver.delete(KeyRings.buildUnifiedKeyRingsUri(), null, null); @@ -1248,9 +1311,9 @@ public class ProviderHelper { // 3. Re-Import secret keyrings from cache if (numSecrets > 0) { - ImportKeyResult result = new ImportExportOperation(mContext, this, + ImportKeyResult result = new ImportOperation(mContext, this, new ProgressFixedScaler(progress, 10, 25, 100, R.string.progress_con_reimport)) - .importKeyRings(itSecrets, numSecrets, null); + .serialKeyRingImport(itSecrets, numSecrets, null, null); log.add(result, indent); } else { log.add(LogType.MSG_CON_REIMPORT_SECRET_SKIP, indent); @@ -1276,10 +1339,14 @@ public class ProviderHelper { // 4. Re-Import public keyrings from cache if (numPublics > 0) { - ImportKeyResult result = new ImportExportOperation(mContext, this, + ImportKeyResult result = new ImportOperation(mContext, this, new ProgressFixedScaler(progress, 25, 99, 100, R.string.progress_con_reimport)) - .importKeyRings(itPublics, numPublics, null); + .serialKeyRingImport(itPublics, numPublics, null, null); log.add(result, indent); + // re-insert our backed up list of updated key times + // TODO: can this cause issues in case a public key re-import failed? + mContentResolver.bulkInsert(UpdatedKeys.CONTENT_URI, + updatedKeysValues.toArray(new ContentValues[updatedKeysValues.size()])); } else { log.add(LogType.MSG_CON_REIMPORT_PUBLIC_SKIP, indent); } @@ -1389,6 +1456,14 @@ public class ProviderHelper { return getKeyRingAsArmoredString(data); } + public Uri renewKeyLastUpdatedTime(long masterKeyId, long time, TimeUnit timeUnit) { + ContentValues values = new ContentValues(); + values.put(UpdatedKeys.MASTER_KEY_ID, masterKeyId); + values.put(UpdatedKeys.LAST_UPDATED, timeUnit.toSeconds(time)); + + return mContentResolver.insert(UpdatedKeys.CONTENT_URI, values); + } + public ArrayList<String> getRegisteredApiApps() { Cursor cursor = mContentResolver.query(ApiApps.CONTENT_URI, null, null, null, null); @@ -1414,7 +1489,7 @@ public class ProviderHelper { private ContentValues contentValueForApiApps(AppSettings appSettings) { ContentValues values = new ContentValues(); values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName()); - values.put(ApiApps.PACKAGE_CERTIFICATE, appSettings.getPackageSignature()); + values.put(ApiApps.PACKAGE_CERTIFICATE, appSettings.getPackageCertificate()); return values; } @@ -1426,9 +1501,9 @@ public class ProviderHelper { // DEPRECATED and thus hardcoded values.put(KeychainContract.ApiAccounts.COMPRESSION, CompressionAlgorithmTags.ZLIB); values.put(KeychainContract.ApiAccounts.ENCRYPTION_ALGORITHM, - PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED); + PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT); values.put(KeychainContract.ApiAccounts.HASH_ALORITHM, - PgpConstants.OpenKeychainHashAlgorithmTags.USE_PREFERRED); + PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT); return values; } @@ -1460,7 +1535,7 @@ public class ProviderHelper { settings = new AppSettings(); settings.setPackageName(cursor.getString( cursor.getColumnIndex(KeychainContract.ApiApps.PACKAGE_NAME))); - settings.setPackageSignature(cursor.getBlob( + settings.setPackageCertificate(cursor.getBlob( cursor.getColumnIndex(KeychainContract.ApiApps.PACKAGE_CERTIFICATE))); } } finally { @@ -1514,8 +1589,8 @@ public class ProviderHelper { return keyIds; } - public Set<Long> getAllowedKeyIdsForApp(Uri uri) { - Set<Long> keyIds = new HashSet<>(); + public HashSet<Long> getAllowedKeyIdsForApp(Uri uri) { + HashSet<Long> keyIds = new HashSet<>(); Cursor cursor = mContentResolver.query(uri, null, null, null, null); try { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java index 45f806960..7e9b24989 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java @@ -18,6 +18,8 @@ package org.sufficientlysecure.keychain.provider; + +import android.content.ClipDescription; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; @@ -38,6 +40,25 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.UUID; +/** + * TemporaryStorageProvider stores decrypted files inside the app's cache directory previously to + * sharing them with other applications. + * + * Security: + * - It is writable by OpenKeychain only (see Manifest), but exported for reading files + * - It uses UUIDs as identifiers which makes predicting files from outside impossible + * - Querying a number of files is not allowed, only querying single files + * -> You can only open a file if you know the Uri containing the precise UUID, this Uri is only + * revealed when the user shares a decrypted file with another app. + * + * Why is support lib's FileProvider not used? + * Because granting Uri permissions temporarily does not work correctly. See + * - https://code.google.com/p/android/issues/detail?id=76683 + * - https://github.com/nmr8acme/FileProvider-permission-bug + * - http://stackoverflow.com/q/24467696 + * - http://stackoverflow.com/q/18249007 + * - Comments at http://www.blogc.at/2014/03/23/share-private-files-with-other-apps-fileprovider/ + */ public class TemporaryStorageProvider extends ContentProvider { private static final String DB_NAME = "tempstorage.db"; @@ -45,17 +66,37 @@ public class TemporaryStorageProvider extends ContentProvider { private static final String COLUMN_ID = "id"; private static final String COLUMN_NAME = "name"; private static final String COLUMN_TIME = "time"; - private static final Uri BASE_URI = Uri.parse("content://org.sufficientlysecure.keychain.tempstorage/"); - private static final int DB_VERSION = 2; + private static final String COLUMN_TYPE = "mimetype"; + public static final String CONTENT_AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY; + private static final Uri BASE_URI = Uri.parse("content://" + CONTENT_AUTHORITY); + private static final int DB_VERSION = 3; private static File cacheDir; + public static Uri createFile(Context context, String targetName, String mimeType) { + ContentValues contentValues = new ContentValues(); + contentValues.put(COLUMN_NAME, targetName); + contentValues.put(COLUMN_TYPE, mimeType); + return context.getContentResolver().insert(BASE_URI, contentValues); + } + public static Uri createFile(Context context, String targetName) { ContentValues contentValues = new ContentValues(); contentValues.put(COLUMN_NAME, targetName); return context.getContentResolver().insert(BASE_URI, contentValues); } + public static Uri createFile(Context context) { + ContentValues contentValues = new ContentValues(); + return context.getContentResolver().insert(BASE_URI, contentValues); + } + + public static int setMimeType(Context context, Uri uri, String mimetype) { + ContentValues values = new ContentValues(); + values.put(COLUMN_TYPE, mimetype); + return context.getContentResolver().update(uri, values, null, null); + } + public static int cleanUp(Context context) { return context.getContentResolver().delete(BASE_URI, COLUMN_TIME + "< ?", new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)}); @@ -72,6 +113,7 @@ public class TemporaryStorageProvider extends ContentProvider { db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" + COLUMN_ID + " TEXT PRIMARY KEY, " + COLUMN_NAME + " TEXT, " + + COLUMN_TYPE + " TEXT, " + COLUMN_TIME + " INTEGER" + ");"); } @@ -88,6 +130,8 @@ public class TemporaryStorageProvider extends ContentProvider { COLUMN_NAME + " TEXT, " + COLUMN_TIME + " INTEGER" + ");"); + case 2: + db.execSQL("ALTER TABLE files ADD COLUMN " + COLUMN_TYPE + " TEXT"); } } } @@ -115,6 +159,10 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if (uri.getLastPathSegment() == null) { + throw new SecurityException("Listing temporary files is not allowed, only querying single files."); + } + File file; try { file = getFile(uri); @@ -125,9 +173,15 @@ public class TemporaryStorageProvider extends ContentProvider { new String[]{uri.getLastPathSegment()}, null, null, null); if (fileName != null) { if (fileName.moveToNext()) { - MatrixCursor cursor = - new MatrixCursor(new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, "_data"}); - cursor.newRow().add(fileName.getString(0)).add(file.length()).add(file.getAbsolutePath()); + MatrixCursor cursor = new MatrixCursor(new String[]{ + OpenableColumns.DISPLAY_NAME, + OpenableColumns.SIZE, + "_data" + }); + cursor.newRow() + .add(fileName.getString(0)) + .add(file.length()) + .add(file.getAbsolutePath()); fileName.close(); return cursor; } @@ -138,9 +192,30 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public String getType(Uri uri) { - // Note: If we can find a files mime type, we can decrypt it to temp storage and open it after - // encryption. The mime type is needed, else UI really sucks and some apps break. - return "*/*"; + Cursor cursor = db.getReadableDatabase().query(TABLE_FILES, + new String[]{COLUMN_TYPE}, COLUMN_ID + "=?", + new String[]{uri.getLastPathSegment()}, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + if (!cursor.isNull(0)) { + return cursor.getString(0); + } + } + } finally { + cursor.close(); + } + } + return "application/octet-stream"; + } + + @Override + public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { + String type = getType(uri); + if (ClipDescription.compareMimeTypes(type, mimeTypeFilter)) { + return new String[]{type}; + } + return null; } @Override @@ -151,9 +226,14 @@ public class TemporaryStorageProvider extends ContentProvider { String uuid = UUID.randomUUID().toString(); values.put(COLUMN_ID, uuid); int insert = (int) db.getWritableDatabase().insert(TABLE_FILES, null, values); + if (insert == -1) { + Log.e(Constants.TAG, "Insert failed!"); + return null; + } try { getFile(uuid).createNewFile(); } catch (IOException e) { + Log.e(Constants.TAG, "File creation failed!"); return null; } return Uri.withAppendedPath(BASE_URI, uuid); @@ -161,10 +241,13 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public int delete(Uri uri, String selection, String[] selectionArgs) { - if (uri.getLastPathSegment() != null) { - selection = DatabaseUtil.concatenateWhere(selection, COLUMN_ID + "=?"); - selectionArgs = DatabaseUtil.appendSelectionArgs(selectionArgs, new String[]{uri.getLastPathSegment()}); + if (uri == null || uri.getLastPathSegment() == null) { + return 0; } + + selection = DatabaseUtil.concatenateWhere(selection, COLUMN_ID + "=?"); + selectionArgs = DatabaseUtil.appendSelectionArgs(selectionArgs, new String[]{uri.getLastPathSegment()}); + Cursor files = db.getReadableDatabase().query(TABLE_FILES, new String[]{COLUMN_ID}, selection, selectionArgs, null, null, null); if (files != null) { @@ -179,11 +262,19 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException("Update not supported"); + if (values.size() != 1 || !values.containsKey(COLUMN_TYPE)) { + throw new UnsupportedOperationException("Update supported only for type field!"); + } + if (selection != null || selectionArgs != null) { + throw new UnsupportedOperationException("Update supported only for plain uri!"); + } + return db.getWritableDatabase().update(TABLE_FILES, values, + COLUMN_ID + " = ?", new String[]{uri.getLastPathSegment()}); } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { return openFileHelper(uri, mode); } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java index a3f9f84c9..498601769 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java @@ -19,7 +19,7 @@ package org.sufficientlysecure.keychain.remote; public class AppSettings { private String mPackageName; - private byte[] mPackageSignature; + private byte[] mPackageCertificate; public AppSettings() { @@ -28,7 +28,7 @@ public class AppSettings { public AppSettings(String packageName, byte[] packageSignature) { super(); this.mPackageName = packageName; - this.mPackageSignature = packageSignature; + this.mPackageCertificate = packageSignature; } public String getPackageName() { @@ -39,12 +39,12 @@ public class AppSettings { this.mPackageName = packageName; } - public byte[] getPackageSignature() { - return mPackageSignature; + public byte[] getPackageCertificate() { + return mPackageCertificate; } - public void setPackageSignature(byte[] packageSignature) { - this.mPackageSignature = packageSignature; + public void setPackageCertificate(byte[] packageCertificate) { + this.mPackageCertificate = packageCertificate; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java index 4a8bf9332..49079f585 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2013-2015 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 @@ -24,21 +24,22 @@ import android.database.Cursor; import android.net.Uri; import android.os.IBinder; import android.os.ParcelFileDescriptor; +import android.os.Parcelable; import android.text.TextUtils; import org.openintents.openpgp.IOpenPgpService; +import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.util.OpenPgpApi; -import org.spongycastle.bcpg.CompressionAlgorithmTags; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel; import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; -import org.sufficientlysecure.keychain.pgp.PgpConstants; -import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; +import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; @@ -64,7 +65,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Set; +import java.util.Date; +import java.util.HashSet; public class OpenPgpService extends RemoteService { @@ -88,8 +90,8 @@ public class OpenPgpService extends RemoteService { boolean duplicateUserIdsCheck = false; ArrayList<Long> keyIds = new ArrayList<>(); - ArrayList<String> missingUserIds = new ArrayList<>(); - ArrayList<String> duplicateUserIds = new ArrayList<>(); + ArrayList<String> missingEmails = new ArrayList<>(); + ArrayList<String> duplicateEmails = new ArrayList<>(); if (!noUserIdsCheck) { for (String email : encryptionUserIds) { // try to find the key for this specific email @@ -102,13 +104,13 @@ public class OpenPgpService extends RemoteService { keyIds.add(id); } else { missingUserIdsCheck = true; - missingUserIds.add(email); + missingEmails.add(email); Log.d(Constants.TAG, "user id missing"); } - // another entry for this email -> too keys with the same email inside user id + // another entry for this email -> two keys with the same email inside user id if (cursor != null && cursor.moveToNext()) { duplicateUserIdsCheck = true; - duplicateUserIds.add(email); + duplicateEmails.add(email); // also pre-select long id = cursor.getLong(cursor.getColumnIndex(KeyRings.MASTER_KEY_ID)); @@ -136,8 +138,8 @@ public class OpenPgpService extends RemoteService { intent.setAction(RemoteServiceActivity.ACTION_SELECT_PUB_KEYS); intent.putExtra(RemoteServiceActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray); intent.putExtra(RemoteServiceActivity.EXTRA_NO_USER_IDS_CHECK, noUserIdsCheck); - intent.putExtra(RemoteServiceActivity.EXTRA_MISSING_USER_IDS, missingUserIds); - intent.putExtra(RemoteServiceActivity.EXTRA_DUPLICATE_USER_IDS, duplicateUserIds); + intent.putExtra(RemoteServiceActivity.EXTRA_MISSING_EMAILS, missingEmails); + intent.putExtra(RemoteServiceActivity.EXTRA_DUPLICATE_EMAILS, duplicateEmails); intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, @@ -164,9 +166,12 @@ public class OpenPgpService extends RemoteService { } private static PendingIntent getRequiredInputPendingIntent(Context context, - Intent data, RequiredInputParcel requiredInput) { + Intent data, + RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInput) { switch (requiredInput.mType) { + case NFC_MOVE_KEY_TO_CARD: case NFC_DECRYPT: case NFC_SIGN: { // build PendingIntent for YubiKey NFC operations @@ -174,6 +179,7 @@ public class OpenPgpService extends RemoteService { // pass params through to activity that it can be returned again later to repeat pgp operation intent.putExtra(NfcOperationActivity.EXTRA_SERVICE_INTENT, data); intent.putExtra(NfcOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput); + intent.putExtra(NfcOperationActivity.EXTRA_CRYPTO_INPUT, cryptoInput); return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); } @@ -184,6 +190,7 @@ public class OpenPgpService extends RemoteService { // pass params through to activity that it can be returned again later to repeat pgp operation intent.putExtra(PassphraseDialogActivity.EXTRA_SERVICE_INTENT, data); intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT, requiredInput); + intent.putExtra(PassphraseDialogActivity.EXTRA_CRYPTO_INPUT, cryptoInput); return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); } @@ -241,7 +248,7 @@ public class OpenPgpService extends RemoteService { .setCleartextSignature(cleartextSign) .setDetachedSignature(!cleartextSign) .setVersionHeader(null) - .setSignatureHashAlgorithm(PgpConstants.OpenKeychainHashAlgorithmTags.USE_PREFERRED); + .setSignatureHashAlgorithm(PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT); Intent signKeyIdIntent = getSignKeyMasterId(data); // NOTE: Fallback to return account settings (Old API) @@ -278,12 +285,12 @@ public class OpenPgpService extends RemoteService { CryptoInputParcel inputParcel = CryptoInputParcelCacheService.getCryptoInputParcel(this, data); if (inputParcel == null) { - inputParcel = new CryptoInputParcel(); + inputParcel = new CryptoInputParcel(new Date()); } // override passphrase in input parcel if given by API call if (data.hasExtra(OpenPgpApi.EXTRA_PASSPHRASE)) { - inputParcel = new CryptoInputParcel(inputParcel.getSignatureTime(), - new Passphrase(data.getCharArrayExtra(OpenPgpApi.EXTRA_PASSPHRASE))); + inputParcel.mPassphrase = + new Passphrase(data.getCharArrayExtra(OpenPgpApi.EXTRA_PASSPHRASE)); } // execute PGP operation! @@ -293,7 +300,8 @@ public class OpenPgpService extends RemoteService { if (pgpResult.isPending()) { RequiredInputParcel requiredInput = pgpResult.getRequiredInputParcel(); - PendingIntent pIntent = getRequiredInputPendingIntent(getBaseContext(), data, requiredInput); + PendingIntent pIntent = getRequiredInputPendingIntent(getBaseContext(), data, + requiredInput, pgpResult.mCryptoInputParcel); // return PendingIntent to be executed by client Intent result = new Intent(); @@ -351,9 +359,9 @@ public class OpenPgpService extends RemoteService { boolean enableCompression = data.getBooleanExtra(OpenPgpApi.EXTRA_ENABLE_COMPRESSION, true); int compressionId; if (enableCompression) { - compressionId = CompressionAlgorithmTags.ZLIB; + compressionId = PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.USE_DEFAULT; } else { - compressionId = CompressionAlgorithmTags.UNCOMPRESSED; + compressionId = PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.UNCOMPRESSED; } // first try to get key ids from non-ambiguous key id extra @@ -383,8 +391,8 @@ public class OpenPgpService extends RemoteService { PgpSignEncryptInputParcel pseInput = new PgpSignEncryptInputParcel(); pseInput.setEnableAsciiArmorOutput(asciiArmor) .setVersionHeader(null) - .setCompressionId(compressionId) - .setSymmetricEncryptionAlgorithm(PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED) + .setCompressionAlgorithm(compressionId) + .setSymmetricEncryptionAlgorithm(PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT) .setEncryptionMasterKeyIds(keyIds) .setFailOnMissingEncryptionKeyIds(true); @@ -413,7 +421,7 @@ public class OpenPgpService extends RemoteService { } // sign and encrypt - pseInput.setSignatureHashAlgorithm(PgpConstants.OpenKeychainHashAlgorithmTags.USE_PREFERRED) + pseInput.setSignatureHashAlgorithm(PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT) .setAdditionalEncryptId(signKeyId); // add sign key for encryption } @@ -433,12 +441,12 @@ public class OpenPgpService extends RemoteService { CryptoInputParcel inputParcel = CryptoInputParcelCacheService.getCryptoInputParcel(this, data); if (inputParcel == null) { - inputParcel = new CryptoInputParcel(); + inputParcel = new CryptoInputParcel(new Date()); } // override passphrase in input parcel if given by API call if (data.hasExtra(OpenPgpApi.EXTRA_PASSPHRASE)) { - inputParcel = new CryptoInputParcel(inputParcel.getSignatureTime(), - new Passphrase(data.getCharArrayExtra(OpenPgpApi.EXTRA_PASSPHRASE))); + inputParcel.mPassphrase = + new Passphrase(data.getCharArrayExtra(OpenPgpApi.EXTRA_PASSPHRASE)); } PgpSignEncryptOperation op = new PgpSignEncryptOperation(this, new ProviderHelper(getContext()), null); @@ -448,7 +456,8 @@ public class OpenPgpService extends RemoteService { if (pgpResult.isPending()) { RequiredInputParcel requiredInput = pgpResult.getRequiredInputParcel(); - PendingIntent pIntent = getRequiredInputPendingIntent(getBaseContext(), data, requiredInput); + PendingIntent pIntent = getRequiredInputPendingIntent(getBaseContext(), data, + requiredInput, pgpResult.mCryptoInputParcel); // return PendingIntent to be executed by client Intent result = new Intent(); @@ -488,23 +497,23 @@ public class OpenPgpService extends RemoteService { } } - private Intent decryptAndVerifyImpl(Intent data, ParcelFileDescriptor input, + private Intent decryptAndVerifyImpl(Intent data, ParcelFileDescriptor inputDescriptor, ParcelFileDescriptor output, boolean decryptMetadataOnly) { - InputStream is = null; - OutputStream os = null; + InputStream inputStream = null; + OutputStream outputStream = null; try { // Get Input- and OutputStream from ParcelFileDescriptor - is = new ParcelFileDescriptor.AutoCloseInputStream(input); + inputStream = new ParcelFileDescriptor.AutoCloseInputStream(inputDescriptor); // output is optional, e.g., for verifying detached signatures if (decryptMetadataOnly || output == null) { - os = null; + outputStream = null; } else { - os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(output); } String currentPkg = getCurrentCallingPackage(); - Set<Long> allowedKeyIds = mProviderHelper.getAllowedKeyIdsForApp( + HashSet<Long> allowedKeyIds = mProviderHelper.getAllowedKeyIdsForApp( KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg)); if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 7) { @@ -512,38 +521,38 @@ public class OpenPgpService extends RemoteService { ApiAccounts.buildBaseUri(currentPkg))); } - long inputLength = is.available(); - InputData inputData = new InputData(is, inputLength); - - PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder( - this, new ProviderHelper(getContext()), null, inputData, os - ); - - CryptoInputParcel inputParcel = CryptoInputParcelCacheService.getCryptoInputParcel(this, data); - if (inputParcel == null) { - inputParcel = new CryptoInputParcel(); + CryptoInputParcel cryptoInput = CryptoInputParcelCacheService.getCryptoInputParcel(this, data); + if (cryptoInput == null) { + cryptoInput = new CryptoInputParcel(); } // override passphrase in input parcel if given by API call if (data.hasExtra(OpenPgpApi.EXTRA_PASSPHRASE)) { - inputParcel = new CryptoInputParcel(inputParcel.getSignatureTime(), - new Passphrase(data.getCharArrayExtra(OpenPgpApi.EXTRA_PASSPHRASE))); + cryptoInput.mPassphrase = + new Passphrase(data.getCharArrayExtra(OpenPgpApi.EXTRA_PASSPHRASE)); } byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE); + PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(this, mProviderHelper, null); + + long inputLength = inputStream.available(); + InputData inputData = new InputData(inputStream, inputLength); + // allow only private keys associated with accounts of this app // no support for symmetric encryption - builder.setAllowSymmetricDecryption(false) + PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel() + .setAllowSymmetricDecryption(false) .setAllowedKeyIds(allowedKeyIds) .setDecryptMetadataOnly(decryptMetadataOnly) .setDetachedSignature(detachedSignature); - DecryptVerifyResult pgpResult = builder.build().execute(inputParcel); + DecryptVerifyResult pgpResult = op.execute(input, cryptoInput, inputData, outputStream); if (pgpResult.isPending()) { // prepare and return PendingIntent to be executed by client RequiredInputParcel requiredInput = pgpResult.getRequiredInputParcel(); - PendingIntent pIntent = getRequiredInputPendingIntent(getBaseContext(), data, requiredInput); + PendingIntent pIntent = getRequiredInputPendingIntent(getBaseContext(), data, + requiredInput, pgpResult.mCryptoInputParcel); Intent result = new Intent(); result.putExtra(OpenPgpApi.RESULT_INTENT, pIntent); @@ -554,40 +563,71 @@ public class OpenPgpService extends RemoteService { Intent result = new Intent(); OpenPgpSignatureResult signatureResult = pgpResult.getSignatureResult(); - // TODO: currently RESULT_TYPE_UNENCRYPTED_UNSIGNED is never returned - // instead an error is returned when no pgp data has been found - int resultType = OpenPgpApi.RESULT_TYPE_UNENCRYPTED_UNSIGNED; - if (signatureResult != null) { - resultType |= OpenPgpApi.RESULT_TYPE_SIGNED; - if (!signatureResult.isSignatureOnly()) { - resultType |= OpenPgpApi.RESULT_TYPE_ENCRYPTED; + + result.putExtra(OpenPgpApi.RESULT_SIGNATURE, signatureResult); + + if (signatureResult.getResult() == OpenPgpSignatureResult.RESULT_KEY_MISSING) { + // If signature is unknown we return an _additional_ PendingIntent + // to retrieve the missing key + result.putExtra(OpenPgpApi.RESULT_INTENT, getKeyserverPendingIntent(data, signatureResult.getKeyId())); + } else { + // If signature key is known, return PendingIntent to show key + result.putExtra(OpenPgpApi.RESULT_INTENT, getShowKeyPendingIntent(signatureResult.getKeyId())); + } + + if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 5) { + // RESULT_INVALID_KEY_REVOKED and RESULT_INVALID_KEY_EXPIRED have been added in version 5 + if (signatureResult.getResult() == OpenPgpSignatureResult.RESULT_INVALID_KEY_REVOKED + || signatureResult.getResult() == OpenPgpSignatureResult.RESULT_INVALID_KEY_EXPIRED) { + signatureResult.setResult(OpenPgpSignatureResult.RESULT_INVALID_SIGNATURE); + } + } + + if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 8) { + // RESULT_INVALID_INSECURE has been added in version 8, fallback to RESULT_INVALID_SIGNATURE + if (signatureResult.getResult() == OpenPgpSignatureResult.RESULT_INVALID_INSECURE) { + signatureResult.setResult(OpenPgpSignatureResult.RESULT_INVALID_SIGNATURE); + } + + // RESULT_NO_SIGNATURE has been added in version 8, before the signatureResult was null + if (signatureResult.getResult() == OpenPgpSignatureResult.RESULT_NO_SIGNATURE) { + result.putExtra(OpenPgpApi.RESULT_SIGNATURE, (Parcelable[]) null); } - result.putExtra(OpenPgpApi.RESULT_SIGNATURE, signatureResult); + // OpenPgpDecryptionResult does not exist in API < 8 + { + OpenPgpDecryptionResult decryptionResult = pgpResult.getDecryptionResult(); - if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 5) { - // SIGNATURE_KEY_REVOKED and SIGNATURE_KEY_EXPIRED have been added in version 5 - if (signatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_KEY_REVOKED - || signatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_KEY_EXPIRED) { - signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_ERROR); + // case RESULT_NOT_ENCRYPTED, but a signature, fallback to deprecated signatureOnly variable + if (decryptionResult.getResult() == OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED + && signatureResult.getResult() != OpenPgpSignatureResult.RESULT_NO_SIGNATURE) { + signatureResult.setSignatureOnly(true); } + + // case RESULT_INSECURE, fallback to an error + if (decryptionResult.getResult() == OpenPgpDecryptionResult.RESULT_INSECURE) { + Intent resultError = new Intent(); + resultError.putExtra(OpenPgpApi.RESULT_ERROR, new OpenPgpError(OpenPgpError.GENERIC_ERROR, + "Insecure encryption: An outdated algorithm has been used!")); + resultError.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return resultError; + } + + // case RESULT_ENCRYPTED + // nothing to do! } + } - if (signatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_KEY_MISSING) { - // If signature is unknown we return an _additional_ PendingIntent - // to retrieve the missing key - result.putExtra(OpenPgpApi.RESULT_INTENT, getKeyserverPendingIntent(data, signatureResult.getKeyId())); - } else { - // If signature key is known, return PendingIntent to show key - result.putExtra(OpenPgpApi.RESULT_INTENT, getShowKeyPendingIntent(signatureResult.getKeyId())); + if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) >= 8) { + OpenPgpDecryptionResult decryptionResult = pgpResult.getDecryptionResult(); + if (decryptionResult != null) { + result.putExtra(OpenPgpApi.RESULT_DECRYPTION, decryptionResult); } - } else { - resultType |= OpenPgpApi.RESULT_TYPE_ENCRYPTED; } - result.putExtra(OpenPgpApi.RESULT_TYPE, resultType); + if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) >= 4) { - OpenPgpMetadata metadata = pgpResult.getDecryptMetadata(); + OpenPgpMetadata metadata = pgpResult.getDecryptionMetadata(); if (metadata != null) { result.putExtra(OpenPgpApi.RESULT_METADATA, metadata); } @@ -601,9 +641,8 @@ public class OpenPgpService extends RemoteService { result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); return result; } else { - LogEntryParcel errorMsg = pgpResult.getLog().getLast(); - - if (errorMsg.mType == OperationResult.LogType.MSG_DC_ERROR_NO_KEY) { + // + if (pgpResult.isKeysDisallowed()) { // allow user to select allowed keys Intent result = new Intent(); result.putExtra(OpenPgpApi.RESULT_INTENT, getSelectAllowedKeysIntent(data)); @@ -611,32 +650,36 @@ public class OpenPgpService extends RemoteService { return result; } - throw new Exception(getString(errorMsg.mType.getMsgId())); + String errorMsg = getString(pgpResult.getLog().getLast().mType.getMsgId()); + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, new OpenPgpError(OpenPgpError.GENERIC_ERROR, errorMsg)); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; } - } catch (Exception e) { - Log.d(Constants.TAG, "decryptAndVerifyImpl", e); + } catch (IOException e) { + Log.e(Constants.TAG, "decryptAndVerifyImpl", e); Intent result = new Intent(); - result.putExtra(OpenPgpApi.RESULT_ERROR, - new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + result.putExtra(OpenPgpApi.RESULT_ERROR, new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); return result; } finally { - if (is != null) { + if (inputStream != null) { try { - is.close(); + inputStream.close(); } catch (IOException e) { Log.e(Constants.TAG, "IOException when closing InputStream", e); } } - if (os != null) { + if (outputStream != null) { try { - os.close(); + outputStream.close(); } catch (IOException e) { Log.e(Constants.TAG, "IOException when closing OutputStream", e); } } } + } private Intent getKeyImpl(Intent data) { @@ -672,28 +715,40 @@ public class OpenPgpService extends RemoteService { } private Intent getSignKeyIdImpl(Intent data) { - String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID); + // if data already contains EXTRA_SIGN_KEY_ID, it has been executed again + // after user interaction. Then, we just need to return the long again! + if (data.hasExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID)) { + long signKeyId = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, + Constants.key.none); - Intent intent = new Intent(getBaseContext(), SelectSignKeyIdActivity.class); - String currentPkg = getCurrentCallingPackage(); - intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(currentPkg)); - intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId); - intent.putExtra(SelectSignKeyIdActivity.EXTRA_DATA, data); + Intent result = new Intent(); + result.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, signKeyId); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } else { + String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID); - PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, - intent, - PendingIntent.FLAG_CANCEL_CURRENT); + Intent intent = new Intent(getBaseContext(), SelectSignKeyIdActivity.class); + String currentPkg = getCurrentCallingPackage(); + intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(currentPkg)); + intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId); + intent.putExtra(SelectSignKeyIdActivity.EXTRA_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); - // return PendingIntent to be executed by client - Intent result = new Intent(); - result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); - result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); - return result; + return result; + } } private Intent getKeyIdsImpl(Intent data) { - // if data already contains key ids extra GET_KEY_IDS has been executed again + // if data already contains EXTRA_KEY_IDS, it has been executed again // after user interaction. Then, we just need to return the array again! if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) { long[] keyIdsArray = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS); @@ -763,12 +818,13 @@ public class OpenPgpService extends RemoteService { && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 4 && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 5 && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 6 - && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 7) { + && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 7 + && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 8) { Intent result = new Intent(); OpenPgpError error = new OpenPgpError (OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n" + "used API version: " + data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) + "\n" - + "supported API versions: 3-7"); + + "supported API versions: 3-8"); result.putExtra(OpenPgpApi.RESULT_ERROR, error); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); return result; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java index e4d4ac49a..792a4d253 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.remote; +import android.annotation.SuppressLint; import android.app.PendingIntent; import android.app.Service; import android.content.Context; @@ -65,12 +66,11 @@ public abstract class RemoteService extends Service { /** * Checks if caller is allowed to access the API * - * @param data * @return null if caller is allowed, or a Bundle with a PendingIntent */ protected Intent isAllowed(Intent data) { try { - if (isCallerAllowed(false)) { + if (isCallerAllowed()) { return null; } else { String packageName = getCurrentCallingPackage(); @@ -130,8 +130,8 @@ public abstract class RemoteService extends Service { } private byte[] getPackageCertificate(String packageName) throws NameNotFoundException { - PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName, - PackageManager.GET_SIGNATURES); + @SuppressLint("PackageManagerGetSignatures") // we do check the byte array of *all* signatures + PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES); // NOTE: Silly Android API naming: Signatures are actually certificates Signature[] certificates = pkgInfo.signatures; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @@ -211,22 +211,15 @@ public abstract class RemoteService extends Service { * Checks if process that binds to this service (i.e. the package name corresponding to the * process) is in the list of allowed package names. * - * @param allowOnlySelf allow only Keychain app itself * @return true if process is allowed to use this service * @throws WrongPackageCertificateException */ - private boolean isCallerAllowed(boolean allowOnlySelf) throws WrongPackageCertificateException { - return isUidAllowed(Binder.getCallingUid(), allowOnlySelf); + private boolean isCallerAllowed() throws WrongPackageCertificateException { + return isUidAllowed(Binder.getCallingUid()); } - private boolean isUidAllowed(int uid, boolean allowOnlySelf) + private boolean isUidAllowed(int uid) throws WrongPackageCertificateException { - if (android.os.Process.myUid() == uid) { - return true; - } - if (allowOnlySelf) { // barrier - return false; - } String[] callingPackages = getPackageManager().getPackagesForUid(uid); @@ -237,7 +230,7 @@ public abstract class RemoteService extends Service { } } - Log.d(Constants.TAG, "Uid is NOT allowed!"); + Log.e(Constants.TAG, "Uid is NOT allowed!"); return false; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java index 81181d61d..18afd2f23 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java @@ -58,7 +58,7 @@ public class AccountSettingsFragment extends Fragment { this.mAccSettings = accountSettings; mAccNameView.setText(accountSettings.getAccountName()); - mSelectKeySpinner.setSelectedKeyId(accountSettings.getKeyId()); + mSelectKeySpinner.setPreSelectedKeyId(accountSettings.getKeyId()); } /** @@ -107,7 +107,7 @@ public class AccountSettingsFragment extends Fragment { if (resultCode == Activity.RESULT_OK) { if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { EditKeyResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); - mSelectKeySpinner.setSelectedKeyId(result.mMasterKeyId); + mSelectKeySpinner.setPreSelectedKeyId(result.mMasterKeyId); } else { Log.e(Constants.TAG, "missing result!"); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java index 2b71d6dc1..4eee73e01 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java @@ -75,7 +75,7 @@ public class AppSettingsActivity extends BaseActivity { mAppNameView = (TextView) findViewById(R.id.api_app_settings_app_name); mAppIconView = (ImageView) findViewById(R.id.api_app_settings_app_icon); mPackageName = (TextView) findViewById(R.id.api_app_settings_package_name); - mPackageSignature = (TextView) findViewById(R.id.api_app_settings_package_signature); + mPackageSignature = (TextView) findViewById(R.id.api_app_settings_package_certificate); mStartFab = (FloatingActionButton) findViewById(R.id.fab); mStartFab.setOnClickListener(new View.OnClickListener() { @@ -148,19 +148,19 @@ public class AppSettingsActivity extends BaseActivity { } private void showAdvancedInfo() { - String signature = null; - // advanced info: package signature SHA-256 + String certificate = null; + // advanced info: package certificate SHA-256 try { MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(mAppSettings.getPackageSignature()); + md.update(mAppSettings.getPackageCertificate()); byte[] digest = md.digest(); - signature = new String(Hex.encode(digest)); + certificate = new String(Hex.encode(digest)); } catch (NoSuchAlgorithmException e) { Log.e(Constants.TAG, "Should not happen!", e); } AdvancedAppSettingsDialogFragment dialogFragment = - AdvancedAppSettingsDialogFragment.newInstance(mAppSettings.getPackageName(), signature); + AdvancedAppSettingsDialogFragment.newInstance(mAppSettings.getPackageName(), certificate); dialogFragment.show(getSupportFragmentManager(), "advancedDialog"); } @@ -217,13 +217,15 @@ public class AppSettingsActivity extends BaseActivity { // show accounts only if available (deprecated API) Cursor cursor = getContentResolver().query(accountsUri, null, null, null, null); - if (cursor.moveToFirst()) { + if (cursor != null && cursor.moveToFirst()) try { mAccountsLabel.setVisibility(View.VISIBLE); mAccountsListFragment = AccountsListFragment.newInstance(accountsUri); // Create an instance of the fragments getSupportFragmentManager().beginTransaction() .replace(R.id.api_accounts_list_fragment, mAccountsListFragment) .commitAllowingStateLoss(); + } finally { + cursor.close(); } // Create an instance of the fragments diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java index b880525ca..caa173f03 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java @@ -17,10 +17,11 @@ package org.sufficientlysecure.keychain.remote.ui; -import android.content.Context; + +import java.util.Set; + import android.content.OperationApplicationException; import android.database.Cursor; -import android.database.DatabaseUtils; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; @@ -35,23 +36,17 @@ import android.widget.ListView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter; +import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; +import org.sufficientlysecure.keychain.ui.adapter.KeySelectableAdapter; import org.sufficientlysecure.keychain.ui.widget.FixedListView; import org.sufficientlysecure.keychain.util.Log; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.Vector; - public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround implements LoaderManager.LoaderCallbacks<Cursor> { private static final String ARG_DATA_URI = "uri"; - private SelectKeyCursorAdapter mAdapter; - private Set<Long> mSelectedMasterKeyIds; + private KeySelectableAdapter mAdapter; private ProviderHelper mProviderHelper; private Uri mDataUri; @@ -80,8 +75,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View layout = super.onCreateView(inflater, container, - savedInstanceState); + View layout = super.onCreateView(inflater, container, savedInstanceState); ListView lv = (ListView) layout.findViewById(android.R.id.list); ViewGroup parent = (ViewGroup) lv.getParent(); @@ -109,67 +103,29 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i mDataUri = getArguments().getParcelable(ARG_DATA_URI); - getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - // Give some text to display if there is no data. In a real // application this would come from a resource. setEmptyText(getString(R.string.list_empty)); - mAdapter = new SecretKeyCursorAdapter(getActivity(), null, 0, getListView()); - + Set<Long> checked = mProviderHelper.getAllKeyIdsForApp(mDataUri); + mAdapter = new KeySelectableAdapter(getActivity(), null, 0, checked); setListAdapter(mAdapter); + getListView().setOnItemClickListener(mAdapter); // Start out with a progress indicator. setListShown(false); - mSelectedMasterKeyIds = mProviderHelper.getAllKeyIdsForApp(mDataUri); - Log.d(Constants.TAG, "allowed: " + mSelectedMasterKeyIds.toString()); - // Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this); - } - /** - * Selects items based on master key ids in list view - * - * @param masterKeyIds - */ - private void preselectMasterKeyIds(Set<Long> masterKeyIds) { - for (int i = 0; i < getListView().getCount(); ++i) { - long listKeyId = mAdapter.getMasterKeyId(i); - for (long keyId : masterKeyIds) { - if (listKeyId == keyId) { - getListView().setItemChecked(i, true); - break; - } - } - } } - - /** - * Returns all selected master key ids - * - * @return - */ + /** Returns all selected master key ids. */ public Set<Long> getSelectedMasterKeyIds() { - // mListView.getCheckedItemIds() would give the row ids of the KeyRings not the master key - // ids! - Set<Long> keyIds = new HashSet<>(); - for (int i = 0; i < getListView().getCount(); ++i) { - if (getListView().isItemChecked(i)) { - keyIds.add(mAdapter.getMasterKeyId(i)); - } - } - - return keyIds; + return mAdapter.getSelectedMasterKeyIds(); } - /** - * Returns all selected user ids - * - * @return - */ + /** Returns all selected user ids. public String[] getSelectedUserIds() { Vector<String> userIds = new Vector<>(); for (int i = 0; i < getListView().getCount(); ++i) { @@ -181,7 +137,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i // make empty array to not return null String userIdArray[] = new String[0]; return userIds.toArray(userIdArray); - } + } */ public void saveAllowedKeys() { try { @@ -192,46 +148,11 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i } @Override - public Loader<Cursor> onCreateLoader(int id, Bundle args) { - Uri baseUri = KeyRings.buildUnifiedKeyRingsUri(); - - // These are the rows that we will retrieve. - String[] projection = new String[]{ - KeyRings._ID, - KeyRings.MASTER_KEY_ID, - KeyRings.USER_ID, - KeyRings.IS_EXPIRED, - KeyRings.IS_REVOKED, - KeyRings.HAS_ENCRYPT, - KeyRings.VERIFIED, - KeyRings.HAS_ANY_SECRET, - KeyRings.HAS_DUPLICATE_USER_ID, - KeyRings.CREATION, - }; - - String inMasterKeyList = null; - if (mSelectedMasterKeyIds != null && mSelectedMasterKeyIds.size() > 0) { - inMasterKeyList = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + " IN ("; - Iterator iter = mSelectedMasterKeyIds.iterator(); - while (iter.hasNext()) { - inMasterKeyList += DatabaseUtils.sqlEscapeString("" + iter.next()); - if (iter.hasNext()) { - inMasterKeyList += ", "; - } - } - inMasterKeyList += ")"; - } - - String selection = KeyRings.HAS_ANY_SECRET + " != 0"; + public Loader<Cursor> onCreateLoader(int loaderId, Bundle data) { + Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri(); + String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1"; - String orderBy = KeyRings.USER_ID + " ASC"; - if (inMasterKeyList != null) { - // sort by selected master keys - orderBy = inMasterKeyList + " DESC, " + orderBy; - } - // Now create and return a CursorLoader that will take care of - // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), baseUri, projection, selection, null, orderBy); + return new CursorLoader(getActivity(), baseUri, KeyAdapter.PROJECTION, where, null, null); } @Override @@ -246,9 +167,6 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i } else { setListShownNoAnimation(true); } - - // preselect given master keys - preselectMasterKeyIds(mSelectedMasterKeyIds); } @Override @@ -259,36 +177,4 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i mAdapter.swapCursor(null); } - private class SecretKeyCursorAdapter extends SelectKeyCursorAdapter { - - public SecretKeyCursorAdapter(Context context, Cursor c, int flags, ListView listView) { - super(context, c, flags, listView); - } - - @Override - protected void initIndex(Cursor cursor) { - super.initIndex(cursor); - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - super.bindView(view, context, cursor); - ViewHolderItem h = (ViewHolderItem) view.getTag(); - - // We care about the checkbox - h.selected.setVisibility(View.VISIBLE); - // the getListView works because this is not a static subclass! - h.selected.setChecked(getListView().isItemChecked(cursor.getPosition())); - - boolean enabled = false; - if ((Boolean) h.statusIcon.getTag()) { - h.statusIcon.setVisibility(View.GONE); - enabled = true; - } - - h.setEnabled(enabled); - } - - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsHeaderFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsHeaderFragment.java index 7beac8973..9160987ab 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsHeaderFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsHeaderFragment.java @@ -47,7 +47,7 @@ public class AppSettingsHeaderFragment extends Fragment { private TextView mAppNameView; private ImageView mAppIconView; private TextView mPackageName; - private TextView mPackageSignature; + private TextView mPackageCertificate; public AppSettings getAppSettings() { return mAppSettings; @@ -67,7 +67,7 @@ public class AppSettingsHeaderFragment extends Fragment { mAppNameView = (TextView) view.findViewById(R.id.api_app_settings_app_name); mAppIconView = (ImageView) view.findViewById(R.id.api_app_settings_app_icon); mPackageName = (TextView) view.findViewById(R.id.api_app_settings_package_name); - mPackageSignature = (TextView) view.findViewById(R.id.api_app_settings_package_signature); + mPackageCertificate = (TextView) view.findViewById(R.id.api_app_settings_package_certificate); return view; } @@ -94,11 +94,11 @@ public class AppSettingsHeaderFragment extends Fragment { // advanced info: package signature SHA-256 try { MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(appSettings.getPackageSignature()); + md.update(appSettings.getPackageCertificate()); byte[] digest = md.digest(); String signature = new String(Hex.encode(digest)); - mPackageSignature.setText(signature); + mPackageCertificate.setText(signature); } catch (NoSuchAlgorithmException e) { Log.e(Constants.TAG, "Should not happen!", e); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java index 5facde64f..a2e781e8a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java @@ -69,8 +69,8 @@ public class RemoteServiceActivity extends BaseActivity { public static final String EXTRA_ACC_NAME = "acc_name"; // select pub keys action public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids"; - public static final String EXTRA_MISSING_USER_IDS = "missing_user_ids"; - public static final String EXTRA_DUPLICATE_USER_IDS = "dublicate_user_ids"; + public static final String EXTRA_MISSING_EMAILS = "missing_emails"; + public static final String EXTRA_DUPLICATE_EMAILS = "dublicate_emails"; public static final String EXTRA_NO_USER_IDS_CHECK = "no_user_ids"; // error message public static final String EXTRA_ERROR_MESSAGE = "error_message"; @@ -222,10 +222,10 @@ public class RemoteServiceActivity extends BaseActivity { case ACTION_SELECT_PUB_KEYS: { long[] selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS); boolean noUserIdsCheck = intent.getBooleanExtra(EXTRA_NO_USER_IDS_CHECK, true); - ArrayList<String> missingUserIds = intent - .getStringArrayListExtra(EXTRA_MISSING_USER_IDS); - ArrayList<String> dublicateUserIds = intent - .getStringArrayListExtra(EXTRA_DUPLICATE_USER_IDS); + ArrayList<String> missingEmails = intent + .getStringArrayListExtra(EXTRA_MISSING_EMAILS); + ArrayList<String> duplicateEmails = intent + .getStringArrayListExtra(EXTRA_DUPLICATE_EMAILS); SpannableStringBuilder ssb = new SpannableStringBuilder(); final SpannableString textIntro = new SpannableString( @@ -235,23 +235,23 @@ public class RemoteServiceActivity extends BaseActivity { textIntro.setSpan(new StyleSpan(Typeface.BOLD), 0, textIntro.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append(textIntro); - if (missingUserIds != null && missingUserIds.size() > 0) { + if (missingEmails != null && missingEmails.size() > 0) { ssb.append("\n\n"); ssb.append(getString(R.string.api_select_pub_keys_missing_text)); ssb.append("\n"); - for (String userId : missingUserIds) { - SpannableString ss = new SpannableString(userId + "\n"); + for (String emails : missingEmails) { + SpannableString ss = new SpannableString(emails + "\n"); ss.setSpan(new BulletSpan(15, Color.BLACK), 0, ss.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append(ss); } } - if (dublicateUserIds != null && dublicateUserIds.size() > 0) { + if (duplicateEmails != null && duplicateEmails.size() > 0) { ssb.append("\n\n"); ssb.append(getString(R.string.api_select_pub_keys_dublicates_text)); ssb.append("\n"); - for (String userId : dublicateUserIds) { - SpannableString ss = new SpannableString(userId + "\n"); + for (String email : duplicateEmails) { + SpannableString ss = new SpannableString(email + "\n"); ss.setSpan(new BulletSpan(15, Color.BLACK), 0, ss.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append(ss); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java index cb9f46f7f..bed49a6f6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java @@ -40,14 +40,9 @@ public class SelectSignKeyIdActivity extends BaseActivity { private static final int REQUEST_CODE_CREATE_KEY = 0x00008884; - private Uri mAppUri; private String mPreferredUserId; private Intent mData; - private SelectSignKeyIdListFragment mListFragment; - private TextView mActionCreateKey; - private TextView mNone; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -62,15 +57,15 @@ public class SelectSignKeyIdActivity extends BaseActivity { } }); - mActionCreateKey = (TextView) findViewById(R.id.api_select_sign_key_create_key); - mActionCreateKey.setOnClickListener(new View.OnClickListener() { + TextView createKeyButton = (TextView) findViewById(R.id.api_select_sign_key_create_key); + createKeyButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createKey(mPreferredUserId); } }); - mNone = (TextView) findViewById(R.id.api_select_sign_key_none); - mNone.setOnClickListener(new View.OnClickListener() { + TextView noneButton = (TextView) findViewById(R.id.api_select_sign_key_none); + noneButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 0 is "none" @@ -82,16 +77,16 @@ public class SelectSignKeyIdActivity extends BaseActivity { }); Intent intent = getIntent(); - mAppUri = intent.getData(); + Uri appUri = intent.getData(); mPreferredUserId = intent.getStringExtra(EXTRA_USER_ID); mData = intent.getParcelableExtra(EXTRA_DATA); - if (mAppUri == null) { + if (appUri == null) { Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!"); finish(); return; } else { - Log.d(Constants.TAG, "uri: " + mAppUri); - startListFragments(savedInstanceState, mAppUri, mData); + Log.d(Constants.TAG, "uri: " + appUri); + startListFragments(savedInstanceState, appUri, mData); } } @@ -113,11 +108,11 @@ public class SelectSignKeyIdActivity extends BaseActivity { } // Create an instance of the fragments - mListFragment = SelectSignKeyIdListFragment.newInstance(dataUri, data); + SelectSignKeyIdListFragment listFragment = SelectSignKeyIdListFragment.newInstance(dataUri, data); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! getSupportFragmentManager().beginTransaction() - .replace(R.id.api_select_sign_key_list_fragment, mListFragment) + .replace(R.id.api_select_sign_key_list_fragment, listFragment) .commitAllowingStateLoss(); // do it immediately! getSupportFragmentManager().executePendingTransactions(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java index a7571a7ac..3cdbca633 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java @@ -22,14 +22,13 @@ import android.os.Parcel; import android.os.Parcelable; import java.io.Serializable; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Date; import java.util.Map; import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.util.ParcelableProxy; /** @@ -44,6 +43,8 @@ public class CertifyActionsParcel implements Parcelable { public ArrayList<CertifyAction> mCertifyActions = new ArrayList<>(); + public String keyServerUri; + public CertifyActionsParcel(long masterKeyId) { mMasterKeyId = masterKeyId; mLevel = CertifyLevel.DEFAULT; @@ -53,6 +54,7 @@ public class CertifyActionsParcel implements Parcelable { mMasterKeyId = source.readLong(); // just like parcelables, this is meant for ad-hoc IPC only and is NOT portable! mLevel = CertifyLevel.values()[source.readInt()]; + keyServerUri = source.readString(); mCertifyActions = (ArrayList<CertifyAction>) source.readSerializable(); } @@ -65,6 +67,7 @@ public class CertifyActionsParcel implements Parcelable { public void writeToParcel(Parcel destination, int flags) { destination.writeLong(mMasterKeyId); destination.writeInt(mLevel.ordinal()); + destination.writeString(keyServerUri); destination.writeSerializable(mCertifyActions); } @@ -86,8 +89,7 @@ public class CertifyActionsParcel implements Parcelable { final public ArrayList<String> mUserIds; final public ArrayList<WrappedUserAttribute> mUserAttributes; - public CertifyAction(long masterKeyId, List<String> userIds, - List<WrappedUserAttribute> attributes) { + public CertifyAction(long masterKeyId, List<String> userIds, List<WrappedUserAttribute> attributes) { mMasterKeyId = masterKeyId; mUserIds = userIds == null ? null : new ArrayList<>(userIds); mUserAttributes = attributes == null ? null : new ArrayList<>(attributes); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CloudImportService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CloudImportService.java deleted file mode 100644 index 249586f6d..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CloudImportService.java +++ /dev/null @@ -1,384 +0,0 @@ -/* - * 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.service; - -import android.app.Service; -import android.content.Intent; -import android.os.Bundle; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.operations.ImportExportOperation; -import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.ParcelableFileCache; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * When this service is started it will initiate a multi-threaded key import and when done it will - * shut itself down. - */ -public class CloudImportService extends Service implements Progressable { - - // required as extras from intent - public static final String EXTRA_MESSENGER = "messenger"; - public static final String EXTRA_DATA = "data"; - - // required by data bundle - public static final String IMPORT_KEY_LIST = "import_key_list"; - public static final String IMPORT_KEY_SERVER = "import_key_server"; - - // indicates a request to cancel the import - public static final String ACTION_CANCEL = Constants.INTENT_PREFIX + "CANCEL"; - - // tells the spawned threads whether the user has requested a cancel - private static AtomicBoolean mActionCancelled = new AtomicBoolean(false); - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - /** - * Used to accumulate the results of individual key imports - */ - private class KeyImportAccumulator { - private OperationResult.OperationLog mImportLog = new OperationResult.OperationLog(); - private int mTotalKeys; - private int mImportedKeys = 0; - private Progressable mImportProgressable; - ArrayList<Long> mImportedMasterKeyIds = new ArrayList<Long>(); - private int mBadKeys = 0; - private int mNewKeys = 0; - private int mUpdatedKeys = 0; - private int mSecret = 0; - private int mResultType = 0; - - public KeyImportAccumulator(int totalKeys) { - mTotalKeys = totalKeys; - // ignore updates from ImportExportOperation for now - mImportProgressable = new Progressable() { - @Override - public void setProgress(String message, int current, int total) { - - } - - @Override - public void setProgress(int resourceId, int current, int total) { - - } - - @Override - public void setProgress(int current, int total) { - - } - - @Override - public void setPreventCancel() { - - } - }; - } - - public Progressable getImportProgressable() { - return mImportProgressable; - } - - public int getTotalKeys() { - return mTotalKeys; - } - - public int getImportedKeys() { - return mImportedKeys; - } - - public synchronized void accumulateKeyImport(ImportKeyResult result) { - mImportedKeys++; - mImportLog.addAll(result.getLog().toList());//accumulates log - mBadKeys += result.mBadKeys; - mNewKeys += result.mNewKeys; - mUpdatedKeys += result.mUpdatedKeys; - mSecret += result.mSecret; - - long[] masterKeyIds = result.getImportedMasterKeyIds(); - for (long masterKeyId : masterKeyIds) { - mImportedMasterKeyIds.add(masterKeyId); - } - - // 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 getConsolidatedImportKeyResult() { - - // adding required information to mResultType - // special case,no keys requested for import - if (mBadKeys == 0 && mNewKeys == 0 && mUpdatedKeys == 0) { - 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); - } - - return new ImportKeyResult(mResultType, mImportLog, mNewKeys, mUpdatedKeys, mBadKeys, - mSecret, masterKeyIds); - } - - public boolean isImportFinished() { - return mTotalKeys == mImportedKeys; - } - } - - private KeyImportAccumulator mKeyImportAccumulator; - - Messenger mMessenger; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - - if (ACTION_CANCEL.equals(intent.getAction())) { - mActionCancelled.set(true); - return Service.START_NOT_STICKY; - } - - mActionCancelled.set(false);//we haven't been cancelled, yet - - Bundle extras = intent.getExtras(); - - mMessenger = (Messenger) extras.get(EXTRA_MESSENGER); - - Bundle data = extras.getBundle(EXTRA_DATA); - - final String keyServer = data.getString(IMPORT_KEY_SERVER); - // keyList being null (in case key list to be reaad from cache) is checked by importKeys - final ArrayList<ParcelableKeyRing> keyList = data.getParcelableArrayList(IMPORT_KEY_LIST); - - // Adding keys to the ThreadPoolExecutor takes time, we don't want to block the main thread - Thread baseImportThread = new Thread(new Runnable() { - - @Override - public void run() { - importKeys(keyList, keyServer); - } - }); - baseImportThread.start(); - return Service.START_NOT_STICKY; - } - - public void importKeys(ArrayList<ParcelableKeyRing> keyList, final String keyServer) { - ParcelableFileCache<ParcelableKeyRing> cache = - new ParcelableFileCache<>(this, "key_import.pcl"); - int totKeys = 0; - Iterator<ParcelableKeyRing> keyListIterator = null; - // either keyList or cache must be null, no guarantees otherwise - if (keyList == null) {//export from cache, copied from ImportExportOperation.importKeyRings - - try { - ParcelableFileCache.IteratorWithSize<ParcelableKeyRing> it = cache.readCache(); - keyListIterator = it; - totKeys = it.getSize(); - } catch (IOException e) { - - // Special treatment here, we need a lot - OperationResult.OperationLog log = new OperationResult.OperationLog(); - log.add(OperationResult.LogType.MSG_IMPORT, 0, 0); - log.add(OperationResult.LogType.MSG_IMPORT_ERROR_IO, 0, 0); - - keyImportFailed(new ImportKeyResult(ImportKeyResult.RESULT_ERROR, log)); - } - } else { - keyListIterator = keyList.iterator(); - totKeys = keyList.size(); - } - - - if (keyListIterator != null) { - mKeyImportAccumulator = new KeyImportAccumulator(totKeys); - setProgress(0, totKeys); - - final int maxThreads = 200; - ExecutorService importExecutor = new ThreadPoolExecutor(0, maxThreads, - 30L, TimeUnit.SECONDS, - new SynchronousQueue<Runnable>()); - - while (keyListIterator.hasNext()) { - - final ParcelableKeyRing pkRing = keyListIterator.next(); - - Runnable importOperationRunnable = new Runnable() { - - @Override - public void run() { - ImportKeyResult result = null; - try { - ImportExportOperation importExportOperation = new ImportExportOperation( - CloudImportService.this, - new ProviderHelper(CloudImportService.this), - mKeyImportAccumulator.getImportProgressable(), - mActionCancelled); - - ArrayList<ParcelableKeyRing> list = new ArrayList<>(); - list.add(pkRing); - result = importExportOperation.importKeyRings(list, - keyServer); - } finally { - // in the off-chance that importKeyRings does something to crash the - // thread before it can call singleKeyRingImportCompleted, our imported - // key count will go wrong. This will cause the service to never die, - // and the progress dialog to stay displayed. The finally block was - // originally meant to ensure singleKeyRingImportCompleted was called, - // and checks for null were to be introduced, but in such a scenario, - // knowing an uncaught error exists in importKeyRings is more important. - - // if a null gets passed, something wrong is happening. We want a crash. - - singleKeyRingImportCompleted(result); - } - } - }; - - importExecutor.execute(importOperationRunnable); - } - } - } - - private synchronized void singleKeyRingImportCompleted(ImportKeyResult result) { - // increase imported key count and accumulate log and bad, new etc. key counts from result - mKeyImportAccumulator.accumulateKeyImport(result); - - setProgress(mKeyImportAccumulator.getImportedKeys(), mKeyImportAccumulator.getTotalKeys()); - - if (mKeyImportAccumulator.isImportFinished()) { - ContactSyncAdapterService.requestSync(); - - sendMessageToHandler(ServiceProgressHandler.MessageStatus.OKAY, - mKeyImportAccumulator.getConsolidatedImportKeyResult()); - - stopSelf();//we're done here - } - } - - private void keyImportFailed(ImportKeyResult result) { - sendMessageToHandler(ServiceProgressHandler.MessageStatus.OKAY, result); - } - - private void sendMessageToHandler(ServiceProgressHandler.MessageStatus status, Integer arg2, Bundle data) { - - Message msg = Message.obtain(); - assert msg != null; - msg.arg1 = status.ordinal(); - if (arg2 != null) { - msg.arg2 = arg2; - } - if (data != null) { - msg.setData(data); - } - - try { - mMessenger.send(msg); - } catch (RemoteException e) { - Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); - } catch (NullPointerException e) { - Log.w(Constants.TAG, "Messenger is null!", e); - } - } - - private void sendMessageToHandler(ServiceProgressHandler.MessageStatus status, OperationResult data) { - Bundle bundle = new Bundle(); - bundle.putParcelable(OperationResult.EXTRA_RESULT, data); - sendMessageToHandler(status, null, bundle); - } - - private void sendMessageToHandler(ServiceProgressHandler.MessageStatus status, Bundle data) { - sendMessageToHandler(status, null, data); - } - - private void sendMessageToHandler(ServiceProgressHandler.MessageStatus status) { - sendMessageToHandler(status, null, null); - } - - /** - * Set progress of ProgressDialog by sending message to handler on UI thread - */ - @Override - public synchronized void setProgress(String message, int progress, int max) { - Log.d(Constants.TAG, "Send message by setProgress with progress=" + progress + ", max=" - + max); - - Bundle data = new Bundle(); - if (message != null) { - data.putString(ServiceProgressHandler.DATA_MESSAGE, message); - } - data.putInt(ServiceProgressHandler.DATA_PROGRESS, progress); - data.putInt(ServiceProgressHandler.DATA_PROGRESS_MAX, max); - - sendMessageToHandler(ServiceProgressHandler.MessageStatus.UPDATE_PROGRESS, null, data); - } - - @Override - public synchronized void setProgress(int resourceId, int progress, int max) { - setProgress(getString(resourceId), progress, max); - } - - @Override - public synchronized void setProgress(int progress, int max) { - setProgress(null, progress, max); - } - - @Override - public synchronized void setPreventCancel() { - sendMessageToHandler(ServiceProgressHandler.MessageStatus.PREVENT_CANCEL); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ConsolidateInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ConsolidateInputParcel.java new file mode 100644 index 000000000..15d109814 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ConsolidateInputParcel.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.service; + +import android.os.Parcel; +import android.os.Parcelable; + +public class ConsolidateInputParcel implements Parcelable { + + public boolean mConsolidateRecovery; + + public ConsolidateInputParcel(boolean consolidateRecovery) { + mConsolidateRecovery = consolidateRecovery; + } + + protected ConsolidateInputParcel(Parcel in) { + mConsolidateRecovery = in.readByte() != 0x00; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte((byte) (mConsolidateRecovery ? 0x01 : 0x00)); + } + + public static final Parcelable.Creator<ConsolidateInputParcel> CREATOR = new Parcelable.Creator<ConsolidateInputParcel>() { + @Override + public ConsolidateInputParcel createFromParcel(Parcel in) { + return new ConsolidateInputParcel(in); + } + + @Override + public ConsolidateInputParcel[] newArray(int size) { + return new ConsolidateInputParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java index 7688b9252..b36d23775 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java @@ -45,7 +45,7 @@ public class ContactSyncAdapterService extends Service { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, final SyncResult syncResult) { - Log.d(Constants.TAG, "Performing a sync!"); + Log.d(Constants.TAG, "Performing a contact sync!"); // TODO: Import is currently disabled for 2.8, until we implement proper origin management // importDone.set(false); // KeychainApplication.setupAccountAsNeeded(ContactSyncAdapterService.this); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DeleteKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DeleteKeyringParcel.java new file mode 100644 index 000000000..b412a6e2b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DeleteKeyringParcel.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.service; + +import android.os.Parcel; +import android.os.Parcelable; + +public class DeleteKeyringParcel implements Parcelable { + + public long[] mMasterKeyIds; + public boolean mIsSecret; + + public DeleteKeyringParcel(long[] masterKeyIds, boolean isSecret) { + mMasterKeyIds = masterKeyIds; + mIsSecret = isSecret; + } + + protected DeleteKeyringParcel(Parcel in) { + mIsSecret = in.readByte() != 0x00; + mMasterKeyIds = in.createLongArray(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte((byte) (mIsSecret ? 0x01 : 0x00)); + dest.writeLongArray(mMasterKeyIds); + } + + public static final Parcelable.Creator<DeleteKeyringParcel> CREATOR = new Parcelable.Creator<DeleteKeyringParcel>() { + @Override + public DeleteKeyringParcel createFromParcel(Parcel in) { + return new DeleteKeyringParcel(in); + } + + @Override + public DeleteKeyringParcel[] newArray(int size) { + return new DeleteKeyringParcel[size]; + } + }; +} + diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DummyAccountService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DummyAccountService.java index 41ff6d02b..18bc3cc9d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DummyAccountService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/DummyAccountService.java @@ -83,7 +83,7 @@ public class DummyAccountService extends Service { public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { response.onResult(new Bundle()); - toaster.toast(R.string.info_no_manual_account_creation); + toaster.toast(R.string.account_no_manual_account_creation); Log.d(Constants.TAG, "DummyAccountService.addAccount"); return null; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java new file mode 100644 index 000000000..24c002bbd --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.service; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; + +public class ExportKeyringParcel implements Parcelable { + public String mKeyserver; + public Uri mCanonicalizedPublicKeyringUri; + public UncachedKeyRing mUncachedKeyRing; + + public boolean mExportSecret; + public long mMasterKeyIds[]; + public String mOutputFile; + public Uri mOutputUri; + public ExportType mExportType; + + public enum ExportType { + UPLOAD_KEYSERVER, + EXPORT_FILE, + EXPORT_URI + } + + public ExportKeyringParcel(String keyserver, Uri keyringUri) { + mExportType = ExportType.UPLOAD_KEYSERVER; + mKeyserver = keyserver; + mCanonicalizedPublicKeyringUri = keyringUri; + } + + public ExportKeyringParcel(String keyserver, UncachedKeyRing uncachedKeyRing) { + mExportType = ExportType.UPLOAD_KEYSERVER; + mKeyserver = keyserver; + mUncachedKeyRing = uncachedKeyRing; + } + + public ExportKeyringParcel(long[] masterKeyIds, boolean exportSecret, String outputFile) { + mExportType = ExportType.EXPORT_FILE; + mMasterKeyIds = masterKeyIds; + mExportSecret = exportSecret; + mOutputFile = outputFile; + } + + @SuppressWarnings("unused") // TODO: is it used? + public ExportKeyringParcel(long[] masterKeyIds, boolean exportSecret, Uri outputUri) { + mExportType = ExportType.EXPORT_URI; + mMasterKeyIds = masterKeyIds; + mExportSecret = exportSecret; + mOutputUri = outputUri; + } + + protected ExportKeyringParcel(Parcel in) { + mKeyserver = in.readString(); + mCanonicalizedPublicKeyringUri = (Uri) in.readValue(Uri.class.getClassLoader()); + mUncachedKeyRing = (UncachedKeyRing) in.readValue(UncachedKeyRing.class.getClassLoader()); + mExportSecret = in.readByte() != 0x00; + mOutputFile = in.readString(); + mOutputUri = (Uri) in.readValue(Uri.class.getClassLoader()); + mExportType = (ExportType) in.readValue(ExportType.class.getClassLoader()); + mMasterKeyIds = in.createLongArray(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mKeyserver); + dest.writeValue(mCanonicalizedPublicKeyringUri); + dest.writeValue(mUncachedKeyRing); + dest.writeByte((byte) (mExportSecret ? 0x01 : 0x00)); + dest.writeString(mOutputFile); + dest.writeValue(mOutputUri); + dest.writeValue(mExportType); + dest.writeLongArray(mMasterKeyIds); + } + + public static final Parcelable.Creator<ExportKeyringParcel> CREATOR = new Parcelable.Creator<ExportKeyringParcel>() { + @Override + public ExportKeyringParcel createFromParcel(Parcel in) { + return new ExportKeyringParcel(in); + } + + @Override + public ExportKeyringParcel[] newArray(int size) { + return new ExportKeyringParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java new file mode 100644 index 000000000..a41dd71cb --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ImportKeyringParcel.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.service; + +import android.os.Parcel; +import android.os.Parcelable; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; + +import java.util.ArrayList; + +public class ImportKeyringParcel implements Parcelable { + // if null, keys are expected to be read from a cache file in ImportExportOperations + public ArrayList<ParcelableKeyRing> mKeyList; + public String mKeyserver; // must be set if keys are to be imported from a keyserver + + public ImportKeyringParcel (ArrayList<ParcelableKeyRing> keyList, String keyserver) { + mKeyList = keyList; + mKeyserver = keyserver; + } + + protected ImportKeyringParcel(Parcel in) { + if (in.readByte() == 0x01) { + mKeyList = new ArrayList<>(); + in.readList(mKeyList, ParcelableKeyRing.class.getClassLoader()); + } else { + mKeyList = null; + } + mKeyserver = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mKeyList == null) { + dest.writeByte((byte) (0x00)); + } else { + dest.writeByte((byte) (0x01)); + dest.writeList(mKeyList); + } + dest.writeString(mKeyserver); + } + + public static final Parcelable.Creator<ImportKeyringParcel> CREATOR = new Parcelable.Creator<ImportKeyringParcel>() { + @Override + public ImportKeyringParcel createFromParcel(Parcel in) { + return new ImportKeyringParcel(in); + } + + @Override + public ImportKeyringParcel[] newArray(int size) { + return new ImportKeyringParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeybaseVerificationParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeybaseVerificationParcel.java new file mode 100644 index 000000000..1872191af --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeybaseVerificationParcel.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.service; + +import android.os.Parcel; +import android.os.Parcelable; + +public class KeybaseVerificationParcel implements Parcelable { + + public String mKeybaseProof; + public String mRequiredFingerprint; + + public KeybaseVerificationParcel(String keybaseProof, String requiredFingerprint) { + mKeybaseProof = keybaseProof; + mRequiredFingerprint = requiredFingerprint; + } + + protected KeybaseVerificationParcel(Parcel in) { + mKeybaseProof = in.readString(); + mRequiredFingerprint = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mKeybaseProof); + dest.writeString(mRequiredFingerprint); + } + + public static final Parcelable.Creator<KeybaseVerificationParcel> CREATOR = new Parcelable.Creator<KeybaseVerificationParcel>() { + @Override + public KeybaseVerificationParcel createFromParcel(Parcel in) { + return new KeybaseVerificationParcel(in); + } + + @Override + public KeybaseVerificationParcel[] newArray(int size) { + return new KeybaseVerificationParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java deleted file mode 100644 index 63ea6285c..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ /dev/null @@ -1,752 +0,0 @@ -/* - * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> - * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> - * - * 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.service; - -import android.app.IntentService; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; - -import com.textuality.keybase.lib.Proof; -import com.textuality.keybase.lib.prover.Prover; - -import org.json.JSONObject; -import org.spongycastle.openpgp.PGPUtil; -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; -import org.sufficientlysecure.keychain.keyimport.Keyserver; -import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.operations.CertifyOperation; -import org.sufficientlysecure.keychain.operations.DeleteOperation; -import org.sufficientlysecure.keychain.operations.EditKeyOperation; -import org.sufficientlysecure.keychain.operations.ImportExportOperation; -import org.sufficientlysecure.keychain.operations.PromoteKeyOperation; -import org.sufficientlysecure.keychain.operations.SignEncryptOperation; -import org.sufficientlysecure.keychain.operations.results.CertifyResult; -import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; -import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; -import org.sufficientlysecure.keychain.operations.results.DeleteResult; -import org.sufficientlysecure.keychain.operations.results.ExportResult; -import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.operations.results.CertifyResult; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; -import org.sufficientlysecure.keychain.util.Preferences; -import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; -import org.sufficientlysecure.keychain.keyimport.Keyserver; -import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; -import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult; -import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; -import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; -import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; -import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralMsgIdException; -import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler.MessageStatus; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.InputData; -import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.ParcelableFileCache; -import org.sufficientlysecure.keychain.util.Passphrase; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import de.measite.minidns.Client; -import de.measite.minidns.DNSMessage; -import de.measite.minidns.Question; -import de.measite.minidns.Record; -import de.measite.minidns.record.Data; -import de.measite.minidns.record.TXT; - -/** - * This Service contains all important long lasting operations for OpenKeychain. It receives Intents with - * data from the activities or other apps, queues these intents, executes them, and stops itself - * after doing them. - */ -public class KeychainIntentService extends IntentService implements Progressable { - - /* extras that can be given by intent */ - public static final String EXTRA_MESSENGER = "messenger"; - public static final String EXTRA_DATA = "data"; - - /* possible actions */ - public static final String ACTION_SIGN_ENCRYPT = Constants.INTENT_PREFIX + "SIGN_ENCRYPT"; - - public static final String ACTION_DECRYPT_VERIFY = Constants.INTENT_PREFIX + "DECRYPT_VERIFY"; - - public static final String ACTION_VERIFY_KEYBASE_PROOF = Constants.INTENT_PREFIX + "VERIFY_KEYBASE_PROOF"; - - public static final String ACTION_DECRYPT_METADATA = Constants.INTENT_PREFIX + "DECRYPT_METADATA"; - - public static final String ACTION_EDIT_KEYRING = Constants.INTENT_PREFIX + "EDIT_KEYRING"; - - public static final String ACTION_PROMOTE_KEYRING = Constants.INTENT_PREFIX + "PROMOTE_KEYRING"; - - public static final String ACTION_IMPORT_KEYRING = Constants.INTENT_PREFIX + "IMPORT_KEYRING"; - public static final String ACTION_EXPORT_KEYRING = Constants.INTENT_PREFIX + "EXPORT_KEYRING"; - - public static final String ACTION_UPLOAD_KEYRING = Constants.INTENT_PREFIX + "UPLOAD_KEYRING"; - - public static final String ACTION_CERTIFY_KEYRING = Constants.INTENT_PREFIX + "SIGN_KEYRING"; - - public static final String ACTION_DELETE = Constants.INTENT_PREFIX + "DELETE"; - - public static final String ACTION_CONSOLIDATE = Constants.INTENT_PREFIX + "CONSOLIDATE"; - - public static final String ACTION_CANCEL = Constants.INTENT_PREFIX + "CANCEL"; - - /* keys for data bundle */ - - // encrypt, decrypt, import export - public static final String TARGET = "target"; - public static final String SOURCE = "source"; - - // possible targets: - public static enum IOType { - UNKNOWN, - BYTES, - URI; - - private static final IOType[] values = values(); - - public static IOType fromInt(int n) { - if (n < 0 || n >= values.length) { - return UNKNOWN; - } else { - return values[n]; - } - } - } - - // encrypt - public static final String ENCRYPT_DECRYPT_INPUT_URI = "input_uri"; - public static final String ENCRYPT_DECRYPT_OUTPUT_URI = "output_uri"; - public static final String SIGN_ENCRYPT_PARCEL = "sign_encrypt_parcel"; - - // decrypt/verify - public static final String DECRYPT_CIPHERTEXT_BYTES = "ciphertext_bytes"; - - // keybase proof - public static final String KEYBASE_REQUIRED_FINGERPRINT = "keybase_required_fingerprint"; - public static final String KEYBASE_PROOF = "keybase_proof"; - - // save keyring - public static final String EDIT_KEYRING_PARCEL = "save_parcel"; - public static final String EDIT_KEYRING_PASSPHRASE = "passphrase"; - public static final String EXTRA_CRYPTO_INPUT = "crypto_input"; - - // delete keyring(s) - public static final String DELETE_KEY_LIST = "delete_list"; - public static final String DELETE_IS_SECRET = "delete_is_secret"; - - // import key - public static final String IMPORT_KEY_LIST = "import_key_list"; - public static final String IMPORT_KEY_SERVER = "import_key_server"; - - // export key - public static final String EXPORT_FILENAME = "export_filename"; - public static final String EXPORT_URI = "export_uri"; - public static final String EXPORT_SECRET = "export_secret"; - public static final String EXPORT_ALL = "export_all"; - public static final String EXPORT_KEY_RING_MASTER_KEY_ID = "export_key_ring_id"; - - // upload key - public static final String UPLOAD_KEY_SERVER = "upload_key_server"; - - // certify key - public static final String CERTIFY_PARCEL = "certify_parcel"; - - // promote key - public static final String PROMOTE_MASTER_KEY_ID = "promote_master_key_id"; - public static final String PROMOTE_CARD_AID = "promote_card_aid"; - - // consolidate - public static final String CONSOLIDATE_RECOVERY = "consolidate_recovery"; - - - /* - * possible data keys as result send over messenger - */ - - // decrypt/verify - public static final String RESULT_DECRYPTED_BYTES = "decrypted_data"; - - Messenger mMessenger; - - // this attribute can possibly merged with the one above? not sure... - private AtomicBoolean mActionCanceled = new AtomicBoolean(false); - - public KeychainIntentService() { - super("KeychainIntentService"); - } - - /** - * The IntentService calls this method from the default worker thread with the intent that - * started the service. When this method returns, IntentService stops the service, as - * appropriate. - */ - @Override - protected void onHandleIntent(Intent intent) { - - // We have not been cancelled! (yet) - mActionCanceled.set(false); - - Bundle extras = intent.getExtras(); - if (extras == null) { - Log.e(Constants.TAG, "Extras bundle is null!"); - return; - } - - if (!(extras.containsKey(EXTRA_MESSENGER) || extras.containsKey(EXTRA_DATA) || (intent - .getAction() == null))) { - Log.e(Constants.TAG, - "Extra bundle must contain a messenger, a data bundle, and an action!"); - return; - } - - Uri dataUri = intent.getData(); - - mMessenger = (Messenger) extras.get(EXTRA_MESSENGER); - Bundle data = extras.getBundle(EXTRA_DATA); - if (data == null) { - Log.e(Constants.TAG, "data extra is null!"); - return; - } - - Log.logDebugBundle(data, "EXTRA_DATA"); - - ProviderHelper providerHelper = new ProviderHelper(this); - - String action = intent.getAction(); - - // executeServiceMethod action from extra bundle - switch (action) { - case ACTION_CERTIFY_KEYRING: { - - // Input - CertifyActionsParcel parcel = data.getParcelable(CERTIFY_PARCEL); - CryptoInputParcel cryptoInput = data.getParcelable(EXTRA_CRYPTO_INPUT); - String keyServerUri = data.getString(UPLOAD_KEY_SERVER); - - // Operation - CertifyOperation op = new CertifyOperation(this, providerHelper, this, mActionCanceled); - CertifyResult result = op.certify(parcel, cryptoInput, keyServerUri); - - // Result - sendMessageToHandler(MessageStatus.OKAY, result); - - break; - } - case ACTION_CONSOLIDATE: { - - // Operation - ConsolidateResult result; - if (data.containsKey(CONSOLIDATE_RECOVERY) && data.getBoolean(CONSOLIDATE_RECOVERY)) { - result = new ProviderHelper(this).consolidateDatabaseStep2(this); - } else { - result = new ProviderHelper(this).consolidateDatabaseStep1(this); - } - - // Result - sendMessageToHandler(MessageStatus.OKAY, result); - - break; - } - case ACTION_DECRYPT_METADATA: { - - try { - /* Input */ - CryptoInputParcel cryptoInput = data.getParcelable(EXTRA_CRYPTO_INPUT); - - InputData inputData = createDecryptInputData(data); - - // verifyText and decrypt returning additional resultData values for the - // verification of signatures - PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder( - this, new ProviderHelper(this), this, inputData, null - ); - builder.setAllowSymmetricDecryption(true) - .setDecryptMetadataOnly(true); - - DecryptVerifyResult decryptVerifyResult = builder.build().execute(cryptoInput); - - sendMessageToHandler(MessageStatus.OKAY, decryptVerifyResult); - } catch (Exception e) { - sendErrorToHandler(e); - } - - break; - } - case ACTION_VERIFY_KEYBASE_PROOF: { - - try { - Proof proof = new Proof(new JSONObject(data.getString(KEYBASE_PROOF))); - setProgress(R.string.keybase_message_fetching_data, 0, 100); - - Prover prover = Prover.findProverFor(proof); - - if (prover == null) { - sendProofError(getString(R.string.keybase_no_prover_found) + ": " + proof.getPrettyName()); - return; - } - - if (!prover.fetchProofData()) { - sendProofError(prover.getLog(), getString(R.string.keybase_problem_fetching_evidence)); - return; - } - String requiredFingerprint = data.getString(KEYBASE_REQUIRED_FINGERPRINT); - if (!prover.checkFingerprint(requiredFingerprint)) { - sendProofError(getString(R.string.keybase_key_mismatch)); - return; - } - - String domain = prover.dnsTxtCheckRequired(); - if (domain != null) { - DNSMessage dnsQuery = new Client().query(new Question(domain, Record.TYPE.TXT)); - if (dnsQuery == null) { - sendProofError(prover.getLog(), getString(R.string.keybase_dns_query_failure)); - return; - } - Record[] records = dnsQuery.getAnswers(); - List<List<byte[]>> extents = new ArrayList<List<byte[]>>(); - for (Record r : records) { - Data d = r.getPayload(); - if (d instanceof TXT) { - extents.add(((TXT) d).getExtents()); - } - } - if (!prover.checkDnsTxt(extents)) { - sendProofError(prover.getLog(), null); - return; - } - } - - byte[] messageBytes = prover.getPgpMessage().getBytes(); - if (prover.rawMessageCheckRequired()) { - InputStream messageByteStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(messageBytes)); - if (!prover.checkRawMessageBytes(messageByteStream)) { - sendProofError(prover.getLog(), null); - return; - } - } - - // kind of awkward, but this whole class wants to pull bytes out of “data” - data.putInt(KeychainIntentService.TARGET, IOType.BYTES.ordinal()); - data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, messageBytes); - - InputData inputData = createDecryptInputData(data); - OutputStream outStream = createCryptOutputStream(data); - - PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder( - this, new ProviderHelper(this), this, - inputData, outStream - ); - builder.setSignedLiteralData(true).setRequiredSignerFingerprint(requiredFingerprint); - - DecryptVerifyResult decryptVerifyResult = builder.build().execute( - new CryptoInputParcel()); - outStream.close(); - - if (!decryptVerifyResult.success()) { - OperationLog log = decryptVerifyResult.getLog(); - OperationResult.LogEntryParcel lastEntry = null; - for (OperationResult.LogEntryParcel entry : log) { - lastEntry = entry; - } - sendProofError(getString(lastEntry.mType.getMsgId())); - return; - } - - if (!prover.validate(outStream.toString())) { - sendProofError(getString(R.string.keybase_message_payload_mismatch)); - return; - } - - Bundle resultData = new Bundle(); - resultData.putString(ServiceProgressHandler.DATA_MESSAGE, "OK"); - - // these help the handler construct a useful human-readable message - resultData.putString(ServiceProgressHandler.KEYBASE_PROOF_URL, prover.getProofUrl()); - resultData.putString(ServiceProgressHandler.KEYBASE_PRESENCE_URL, prover.getPresenceUrl()); - resultData.putString(ServiceProgressHandler.KEYBASE_PRESENCE_LABEL, prover.getPresenceLabel()); - sendMessageToHandler(MessageStatus.OKAY, resultData); - } catch (Exception e) { - sendErrorToHandler(e); - } - - break; - } - case ACTION_DECRYPT_VERIFY: { - - try { - /* Input */ - CryptoInputParcel cryptoInput = data.getParcelable(EXTRA_CRYPTO_INPUT); - - InputData inputData = createDecryptInputData(data); - OutputStream outStream = createCryptOutputStream(data); - - /* Operation */ - Bundle resultData = new Bundle(); - - // verifyText and decrypt returning additional resultData values for the - // verification of signatures - PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder( - this, new ProviderHelper(this), this, - inputData, outStream - ); - builder.setAllowSymmetricDecryption(true); - - DecryptVerifyResult decryptVerifyResult = builder.build().execute(cryptoInput); - - outStream.close(); - - resultData.putParcelable(DecryptVerifyResult.EXTRA_RESULT, decryptVerifyResult); - - /* Output */ - finalizeDecryptOutputStream(data, resultData, outStream); - Log.logDebugBundle(resultData, "resultData"); - - sendMessageToHandler(MessageStatus.OKAY, resultData); - - } catch (IOException | PgpGeneralException e) { - // TODO get rid of this! - sendErrorToHandler(e); - } - - break; - } - case ACTION_DELETE: { - - // Input - long[] masterKeyIds = data.getLongArray(DELETE_KEY_LIST); - boolean isSecret = data.getBoolean(DELETE_IS_SECRET); - - // Operation - DeleteOperation op = new DeleteOperation(this, new ProviderHelper(this), this); - DeleteResult result = op.execute(masterKeyIds, isSecret); - - // Result - sendMessageToHandler(MessageStatus.OKAY, result); - - break; - } - case ACTION_EDIT_KEYRING: { - - // Input - SaveKeyringParcel saveParcel = data.getParcelable(EDIT_KEYRING_PARCEL); - CryptoInputParcel cryptoInput = data.getParcelable(EXTRA_CRYPTO_INPUT); - - // Operation - EditKeyOperation op = new EditKeyOperation(this, providerHelper, this, mActionCanceled); - OperationResult result = op.execute(saveParcel, cryptoInput); - - // Result - sendMessageToHandler(MessageStatus.OKAY, result); - - break; - } - case ACTION_PROMOTE_KEYRING: { - - // Input - long keyRingId = data.getLong(PROMOTE_MASTER_KEY_ID); - byte[] cardAid = data.getByteArray(PROMOTE_CARD_AID); - - // Operation - PromoteKeyOperation op = new PromoteKeyOperation(this, providerHelper, this, mActionCanceled); - PromoteKeyResult result = op.execute(keyRingId, cardAid); - - // Result - sendMessageToHandler(MessageStatus.OKAY, result); - - break; - } - case ACTION_EXPORT_KEYRING: { - - // Input - boolean exportSecret = data.getBoolean(EXPORT_SECRET, false); - String outputFile = data.getString(EXPORT_FILENAME); - Uri outputUri = data.getParcelable(EXPORT_URI); - - boolean exportAll = data.getBoolean(EXPORT_ALL); - long[] masterKeyIds = exportAll ? null : data.getLongArray(EXPORT_KEY_RING_MASTER_KEY_ID); - - // Operation - ImportExportOperation importExportOperation = new ImportExportOperation(this, new ProviderHelper(this), this); - ExportResult result; - if (outputFile != null) { - result = importExportOperation.exportToFile(masterKeyIds, exportSecret, outputFile); - } else { - result = importExportOperation.exportToUri(masterKeyIds, exportSecret, outputUri); - } - - // Result - sendMessageToHandler(MessageStatus.OKAY, result); - - break; - } - case ACTION_IMPORT_KEYRING: { - - // Input - String keyServer = data.getString(IMPORT_KEY_SERVER); - ArrayList<ParcelableKeyRing> list = data.getParcelableArrayList(IMPORT_KEY_LIST); - ParcelableFileCache<ParcelableKeyRing> cache = - new ParcelableFileCache<>(this, "key_import.pcl"); - - // Operation - ImportExportOperation importExportOperation = new ImportExportOperation( - this, providerHelper, this, mActionCanceled); - // Either list or cache must be null, no guarantees otherwise. - ImportKeyResult result = list != null - ? importExportOperation.importKeyRings(list, keyServer) - : importExportOperation.importKeyRings(cache, keyServer); - - // Result - sendMessageToHandler(MessageStatus.OKAY, result); - - break; - - } - case ACTION_SIGN_ENCRYPT: { - - // Input - SignEncryptParcel inputParcel = data.getParcelable(SIGN_ENCRYPT_PARCEL); - CryptoInputParcel cryptoInput = data.getParcelable(EXTRA_CRYPTO_INPUT); - - // Operation - SignEncryptOperation op = new SignEncryptOperation( - this, new ProviderHelper(this), this, mActionCanceled); - SignEncryptResult result = op.execute(inputParcel, cryptoInput); - - // Result - sendMessageToHandler(MessageStatus.OKAY, result); - - break; - } - case ACTION_UPLOAD_KEYRING: { - try { - - /* Input */ - String keyServer = data.getString(UPLOAD_KEY_SERVER); - // and dataUri! - - /* Operation */ - HkpKeyserver server = new HkpKeyserver(keyServer); - - CanonicalizedPublicKeyRing keyring = providerHelper.getCanonicalizedPublicKeyRing(dataUri); - ImportExportOperation importExportOperation = new ImportExportOperation(this, new ProviderHelper(this), this); - - try { - importExportOperation.uploadKeyRingToServer(server, keyring); - } catch (Keyserver.AddKeyException e) { - throw new PgpGeneralException("Unable to export key to selected server"); - } - - sendMessageToHandler(MessageStatus.OKAY); - } catch (Exception e) { - sendErrorToHandler(e); - } - break; - } - } - } - - private void sendProofError(List<String> log, String label) { - String msg = null; - label = (label == null) ? "" : label + ": "; - for (String m : log) { - Log.e(Constants.TAG, label + m); - msg = m; - } - sendProofError(label + msg); - } - - private void sendProofError(String msg) { - Bundle bundle = new Bundle(); - bundle.putString(ServiceProgressHandler.DATA_ERROR, msg); - sendMessageToHandler(MessageStatus.OKAY, bundle); - } - - private void sendErrorToHandler(Exception e) { - // TODO: Implement a better exception handling here - // contextualize the exception, if necessary - String message; - if (e instanceof PgpGeneralMsgIdException) { - e = ((PgpGeneralMsgIdException) e).getContextualized(this); - message = e.getMessage(); - } else { - message = e.getMessage(); - } - Log.d(Constants.TAG, "KeychainIntentService Exception: ", e); - - Bundle data = new Bundle(); - data.putString(ServiceProgressHandler.DATA_ERROR, message); - sendMessageToHandler(MessageStatus.EXCEPTION, null, data); - } - - private void sendMessageToHandler(MessageStatus status, Integer arg2, Bundle data) { - - Message msg = Message.obtain(); - assert msg != null; - msg.arg1 = status.ordinal(); - if (arg2 != null) { - msg.arg2 = arg2; - } - if (data != null) { - msg.setData(data); - } - - try { - mMessenger.send(msg); - } catch (RemoteException e) { - Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); - } catch (NullPointerException e) { - Log.w(Constants.TAG, "Messenger is null!", e); - } - } - - private void sendMessageToHandler(MessageStatus status, OperationResult data) { - Bundle bundle = new Bundle(); - bundle.putParcelable(OperationResult.EXTRA_RESULT, data); - sendMessageToHandler(status, null, bundle); - } - - private void sendMessageToHandler(MessageStatus status, Bundle data) { - sendMessageToHandler(status, null, data); - } - - private void sendMessageToHandler(MessageStatus status) { - sendMessageToHandler(status, null, null); - } - - /** - * Set progress of ProgressDialog by sending message to handler on UI thread - */ - public void setProgress(String message, int progress, int max) { - Log.d(Constants.TAG, "Send message by setProgress with progress=" + progress + ", max=" - + max); - - Bundle data = new Bundle(); - if (message != null) { - data.putString(ServiceProgressHandler.DATA_MESSAGE, message); - } - data.putInt(ServiceProgressHandler.DATA_PROGRESS, progress); - data.putInt(ServiceProgressHandler.DATA_PROGRESS_MAX, max); - - sendMessageToHandler(MessageStatus.UPDATE_PROGRESS, null, data); - } - - public void setProgress(int resourceId, int progress, int max) { - setProgress(getString(resourceId), progress, max); - } - - public void setProgress(int progress, int max) { - setProgress(null, progress, max); - } - - @Override - public void setPreventCancel() { - sendMessageToHandler(MessageStatus.PREVENT_CANCEL); - } - - private InputData createDecryptInputData(Bundle data) throws IOException, PgpGeneralException { - return createCryptInputData(data, DECRYPT_CIPHERTEXT_BYTES); - } - - private InputData createCryptInputData(Bundle data, String bytesName) throws PgpGeneralException, IOException { - int source = data.get(SOURCE) != null ? data.getInt(SOURCE) : data.getInt(TARGET); - IOType type = IOType.fromInt(source); - switch (type) { - case BYTES: /* encrypting bytes directly */ - byte[] bytes = data.getByteArray(bytesName); - return new InputData(new ByteArrayInputStream(bytes), bytes.length); - - case URI: /* encrypting content uri */ - Uri providerUri = data.getParcelable(ENCRYPT_DECRYPT_INPUT_URI); - - // InputStream - return new InputData(getContentResolver().openInputStream(providerUri), FileHelper.getFileSize(this, providerUri, 0)); - - default: - throw new PgpGeneralException("No target chosen!"); - } - } - - private OutputStream createCryptOutputStream(Bundle data) throws PgpGeneralException, FileNotFoundException { - int target = data.getInt(TARGET); - IOType type = IOType.fromInt(target); - switch (type) { - case BYTES: - return new ByteArrayOutputStream(); - - case URI: - Uri providerUri = data.getParcelable(ENCRYPT_DECRYPT_OUTPUT_URI); - - return getContentResolver().openOutputStream(providerUri); - - default: - throw new PgpGeneralException("No target chosen!"); - } - } - - private void finalizeDecryptOutputStream(Bundle data, Bundle resultData, OutputStream outStream) { - finalizeCryptOutputStream(data, resultData, outStream, RESULT_DECRYPTED_BYTES); - } - - private void finalizeCryptOutputStream(Bundle data, Bundle resultData, OutputStream outStream, String bytesName) { - int target = data.getInt(TARGET); - IOType type = IOType.fromInt(target); - switch (type) { - case BYTES: - byte output[] = ((ByteArrayOutputStream) outStream).toByteArray(); - resultData.putByteArray(bytesName, output); - break; - case URI: - // nothing, output was written, just send okay and verification bundle - - break; - } - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (ACTION_CANCEL.equals(intent.getAction())) { - mActionCanceled.set(true); - return START_NOT_STICKY; - } - return super.onStartCommand(intent, flags, startId); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java new file mode 100644 index 000000000..eff27f112 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * 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.service; + +import android.app.Service; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcelable; +import android.os.RemoteException; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.operations.BaseOperation; +import org.sufficientlysecure.keychain.operations.CertifyOperation; +import org.sufficientlysecure.keychain.operations.ConsolidateOperation; +import org.sufficientlysecure.keychain.operations.DeleteOperation; +import org.sufficientlysecure.keychain.operations.EditKeyOperation; +import org.sufficientlysecure.keychain.operations.ExportOperation; +import org.sufficientlysecure.keychain.operations.ImportOperation; +import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation; +import org.sufficientlysecure.keychain.operations.PromoteKeyOperation; +import org.sufficientlysecure.keychain.operations.RevokeOperation; +import org.sufficientlysecure.keychain.operations.SignEncryptOperation; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.ServiceProgressHandler.MessageStatus; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This Service contains all important long lasting operations for OpenKeychain. It receives Intents with + * data from the activities or other apps, executes them, and stops itself after doing them. + */ +public class KeychainService extends Service implements Progressable { + + // messenger for communication (hack) + public static final String EXTRA_MESSENGER = "messenger"; + + // extras for operation + public static final String EXTRA_OPERATION_INPUT = "op_input"; + public static final String EXTRA_CRYPTO_INPUT = "crypto_input"; + + public static final String ACTION_CANCEL = "action_cancel"; + + // this attribute can possibly merged with the one above? not sure... + private AtomicBoolean mActionCanceled = new AtomicBoolean(false); + + ThreadLocal<Messenger> mMessenger = new ThreadLocal<>(); + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * This is run on the main thread, we need to spawn a runnable which runs on another thread for the actual operation + */ + @Override + public int onStartCommand(final Intent intent, int flags, int startId) { + + if (intent.getAction() != null && intent.getAction().equals(ACTION_CANCEL)) { + mActionCanceled.set(true); + return START_NOT_STICKY; + } + + Runnable actionRunnable = new Runnable() { + @Override + public void run() { + // We have not been cancelled! (yet) + mActionCanceled.set(false); + + Bundle extras = intent.getExtras(); + + // Set messenger for communication (for this particular thread) + mMessenger.set(extras.<Messenger>getParcelable(EXTRA_MESSENGER)); + + // Input + Parcelable inputParcel = extras.getParcelable(EXTRA_OPERATION_INPUT); + CryptoInputParcel cryptoInput = extras.getParcelable(EXTRA_CRYPTO_INPUT); + + // Operation + BaseOperation op; + + // just for brevity + KeychainService outerThis = KeychainService.this; + if (inputParcel instanceof SignEncryptParcel) { + op = new SignEncryptOperation(outerThis, new ProviderHelper(outerThis), + outerThis, mActionCanceled); + } else if (inputParcel instanceof PgpDecryptVerifyInputParcel) { + op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis); + } else if (inputParcel instanceof SaveKeyringParcel) { + op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, + mActionCanceled); + } else if (inputParcel instanceof RevokeKeyringParcel) { + op = new RevokeOperation(outerThis, new ProviderHelper(outerThis), outerThis); + } else if (inputParcel instanceof CertifyActionsParcel) { + op = new CertifyOperation(outerThis, new ProviderHelper(outerThis), outerThis, + mActionCanceled); + } else if (inputParcel instanceof DeleteKeyringParcel) { + op = new DeleteOperation(outerThis, new ProviderHelper(outerThis), outerThis); + } else if (inputParcel instanceof PromoteKeyringParcel) { + op = new PromoteKeyOperation(outerThis, new ProviderHelper(outerThis), + outerThis, mActionCanceled); + } else if (inputParcel instanceof ImportKeyringParcel) { + op = new ImportOperation(outerThis, new ProviderHelper(outerThis), outerThis, + mActionCanceled); + } else if (inputParcel instanceof ExportKeyringParcel) { + op = new ExportOperation(outerThis, new ProviderHelper(outerThis), outerThis, + mActionCanceled); + } else if (inputParcel instanceof ConsolidateInputParcel) { + op = new ConsolidateOperation(outerThis, new ProviderHelper(outerThis), + outerThis); + } else if (inputParcel instanceof KeybaseVerificationParcel) { + op = new KeybaseVerificationOperation(outerThis, new ProviderHelper(outerThis), + outerThis); + } else { + throw new AssertionError("Unrecognized input parcel in KeychainService!"); + } + + @SuppressWarnings("unchecked") // this is unchecked, we make sure it's the correct op above! + OperationResult result = op.execute(inputParcel, cryptoInput); + sendMessageToHandler(MessageStatus.OKAY, result); + + } + }; + + Thread actionThread = new Thread(actionRunnable); + actionThread.start(); + + return START_NOT_STICKY; + } + + private void sendMessageToHandler(MessageStatus status, Integer arg2, Bundle data) { + + Message msg = Message.obtain(); + assert msg != null; + msg.arg1 = status.ordinal(); + if (arg2 != null) { + msg.arg2 = arg2; + } + if (data != null) { + msg.setData(data); + } + + try { + mMessenger.get().send(msg); + } catch (RemoteException e) { + Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); + } catch (NullPointerException e) { + Log.w(Constants.TAG, "Messenger is null!", e); + } + } + + private void sendMessageToHandler(MessageStatus status, OperationResult data) { + Bundle bundle = new Bundle(); + bundle.putParcelable(OperationResult.EXTRA_RESULT, data); + sendMessageToHandler(status, null, bundle); + } + + private void sendMessageToHandler(MessageStatus status) { + sendMessageToHandler(status, null, null); + } + + /** + * Set progress of ProgressDialog by sending message to handler on UI thread + */ + @Override + public void setProgress(String message, int progress, int max) { + Log.d(Constants.TAG, "Send message by setProgress with progress=" + progress + ", max=" + + max); + + Bundle data = new Bundle(); + if (message != null) { + data.putString(ServiceProgressHandler.DATA_MESSAGE, message); + } + data.putInt(ServiceProgressHandler.DATA_PROGRESS, progress); + data.putInt(ServiceProgressHandler.DATA_PROGRESS_MAX, max); + + sendMessageToHandler(MessageStatus.UPDATE_PROGRESS, null, data); + } + + @Override + public void setProgress(int resourceId, int progress, int max) { + setProgress(getString(resourceId), progress, max); + } + + @Override + public void setProgress(int progress, int max) { + setProgress(null, progress, max); + } + + @Override + public void setPreventCancel() { + sendMessageToHandler(MessageStatus.PREVENT_CANCEL); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java new file mode 100644 index 000000000..3243df1a8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java @@ -0,0 +1,516 @@ +package org.sufficientlysecure.keychain.service; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SyncResult; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.PowerManager; +import android.os.SystemClock; +import android.support.v4.app.NotificationCompat; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.ImportOperation; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ParcelableProxy; +import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; + +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class KeyserverSyncAdapterService extends Service { + + // how often a sync should be initiated, in s + public static final long SYNC_INTERVAL = + Constants.DEBUG_KEYSERVER_SYNC + ? TimeUnit.MINUTES.toSeconds(2) : TimeUnit.DAYS.toSeconds(3); + // time since last update after which a key should be updated again, in s + public static final long KEY_UPDATE_LIMIT = + Constants.DEBUG_KEYSERVER_SYNC ? 1 : TimeUnit.DAYS.toSeconds(7); + // time by which a sync is postponed in case of a + public static final long SYNC_POSTPONE_TIME = + Constants.DEBUG_KEYSERVER_SYNC ? 30 * 1000 : TimeUnit.MINUTES.toMillis(5); + // Time taken by Orbot before a new circuit is created + public static final int ORBOT_CIRCUIT_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(10); + + + private static final String ACTION_IGNORE_TOR = "ignore_tor"; + private static final String ACTION_UPDATE_ALL = "update_all"; + private static final String ACTION_SYNC_NOW = "sync_now"; + private static final String ACTION_DISMISS_NOTIFICATION = "cancel_sync"; + private static final String ACTION_START_ORBOT = "start_orbot"; + private static final String ACTION_CANCEL = "cancel"; + + private AtomicBoolean mCancelled = new AtomicBoolean(false); + + @Override + public int onStartCommand(final Intent intent, int flags, final int startId) { + switch (intent.getAction()) { + case ACTION_CANCEL: { + mCancelled.set(true); + break; + } + // the reason for the separation betweyeen SYNC_NOW and UPDATE_ALL is so that starting + // the sync directly from the notification is possible while the screen is on with + // UPDATE_ALL, but a postponed sync is only started if screen is off + case ACTION_SYNC_NOW: { + // this checks for screen on/off before sync, and postpones the sync if on + ContentResolver.requestSync( + new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE), + Constants.PROVIDER_AUTHORITY, + new Bundle() + ); + break; + } + case ACTION_UPDATE_ALL: { + // does not check for screen on/off + asyncKeyUpdate(this, new CryptoInputParcel()); + break; + } + case ACTION_IGNORE_TOR: { + NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT); + asyncKeyUpdate(this, new CryptoInputParcel(ParcelableProxy.getForNoProxy())); + break; + } + case ACTION_START_ORBOT: { + NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT); + Intent startOrbot = new Intent(this, OrbotRequiredDialogActivity.class); + startOrbot.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_START_ORBOT, true); + Messenger messenger = new Messenger( + new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case OrbotRequiredDialogActivity.MESSAGE_ORBOT_STARTED: { + asyncKeyUpdate(KeyserverSyncAdapterService.this, + new CryptoInputParcel()); + break; + } + case OrbotRequiredDialogActivity.MESSAGE_ORBOT_IGNORE: { + asyncKeyUpdate(KeyserverSyncAdapterService.this, + new CryptoInputParcel( + ParcelableProxy.getForNoProxy())); + break; + } + case OrbotRequiredDialogActivity.MESSAGE_DIALOG_CANCEL: { + // just stop service + stopSelf(); + break; + } + } + } + } + ); + startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_MESSENGER, messenger); + startActivity(startOrbot); + break; + } + case ACTION_DISMISS_NOTIFICATION: { + NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT); + stopSelf(startId); + break; + } + } + return START_NOT_STICKY; + } + + private class KeyserverSyncAdapter extends AbstractThreadedSyncAdapter { + + public KeyserverSyncAdapter() { + super(KeyserverSyncAdapterService.this, true); + } + + @Override + public void onPerformSync(Account account, Bundle extras, String authority, + ContentProviderClient provider, SyncResult syncResult) { + Log.d(Constants.TAG, "Performing a keyserver sync!"); + + PowerManager pm = (PowerManager) KeyserverSyncAdapterService.this + .getSystemService(Context.POWER_SERVICE); + @SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20 + boolean isScreenOn = pm.isScreenOn(); + + if (!isScreenOn) { + Intent serviceIntent = new Intent(KeyserverSyncAdapterService.this, + KeyserverSyncAdapterService.class); + serviceIntent.setAction(ACTION_UPDATE_ALL); + startService(serviceIntent); + } else { + postponeSync(); + } + } + + @Override + public void onSyncCanceled() { + super.onSyncCanceled(); + cancelUpdates(KeyserverSyncAdapterService.this); + } + } + + @Override + public IBinder onBind(Intent intent) { + return new KeyserverSyncAdapter().getSyncAdapterBinder(); + } + + private void handleUpdateResult(ImportKeyResult result) { + if (result.isPending()) { + // result is pending due to Orbot not being started + // try to start it silently, if disabled show notifications + new OrbotHelper.SilentStartManager() { + @Override + protected void onOrbotStarted() { + // retry the update + asyncKeyUpdate(KeyserverSyncAdapterService.this, + new CryptoInputParcel()); + } + + @Override + protected void onSilentStartDisabled() { + // show notification + NotificationManager manager = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + manager.notify(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT, + getOrbotNoification(KeyserverSyncAdapterService.this)); + } + }.startOrbotAndListen(this, false); + } else if (isUpdateCancelled()) { + Log.d(Constants.TAG, "Keyserver sync cancelled, postponing by" + SYNC_POSTPONE_TIME + + "ms"); + postponeSync(); + } else { + Log.d(Constants.TAG, "Keyserver sync completed: Updated: " + result.mUpdatedKeys + + " Failed: " + result.mBadKeys); + stopSelf(); + } + } + + private void postponeSync() { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent serviceIntent = new Intent(this, KeyserverSyncAdapterService.class); + serviceIntent.setAction(ACTION_SYNC_NOW); + PendingIntent pi = PendingIntent.getService(this, 0, serviceIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.set( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + SYNC_POSTPONE_TIME, + pi + ); + } + + private void asyncKeyUpdate(final Context context, + final CryptoInputParcel cryptoInputParcel) { + new Thread(new Runnable() { + @Override + public void run() { + ImportKeyResult result = updateKeysFromKeyserver(context, cryptoInputParcel); + handleUpdateResult(result); + } + }).start(); + } + + private synchronized ImportKeyResult updateKeysFromKeyserver(final Context context, + final CryptoInputParcel cryptoInputParcel) { + mCancelled.set(false); + + ArrayList<ParcelableKeyRing> keyList = getKeysToUpdate(context); + + if (isUpdateCancelled()) { // if we've already been cancelled + return new ImportKeyResult(OperationResult.RESULT_CANCELLED, + new OperationResult.OperationLog()); + } + + if (cryptoInputParcel.getParcelableProxy() == null) { + // no explicit proxy, retrieve from preferences. Check if we should do a staggered sync + if (Preferences.getPreferences(context).getProxyPrefs().torEnabled) { + return staggeredUpdate(context, keyList, cryptoInputParcel); + } else { + return directUpdate(context, keyList, cryptoInputParcel); + } + } else { + return directUpdate(context, keyList, cryptoInputParcel); + } + } + + private ImportKeyResult directUpdate(Context context, ArrayList<ParcelableKeyRing> keyList, + CryptoInputParcel cryptoInputParcel) { + Log.d(Constants.TAG, "Starting normal update"); + ImportOperation importOp = new ImportOperation(context, new ProviderHelper(context), null); + return importOp.execute( + new ImportKeyringParcel(keyList, + Preferences.getPreferences(context).getPreferredKeyserver()), + cryptoInputParcel + ); + } + + + /** + * will perform a staggered update of user's keys using delays to ensure new Tor circuits, as + * performed by parcimonie. Relevant issue and method at: + * https://github.com/open-keychain/open-keychain/issues/1337 + * + * @return result of the sync + */ + private ImportKeyResult staggeredUpdate(Context context, ArrayList<ParcelableKeyRing> keyList, + CryptoInputParcel cryptoInputParcel) { + Log.d(Constants.TAG, "Starting staggered update"); + // final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7); + final int WEEK_IN_SECONDS = 0; + ImportOperation.KeyImportAccumulator accumulator + = new ImportOperation.KeyImportAccumulator(keyList.size(), null); + for (ParcelableKeyRing keyRing : keyList) { + int waitTime; + int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size())); + if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT) { + waitTime = staggeredTime; + } else { + waitTime = ORBOT_CIRCUIT_TIMEOUT + new Random().nextInt(ORBOT_CIRCUIT_TIMEOUT); + } + Log.d(Constants.TAG, "Updating key with fingerprint " + keyRing.mExpectedFingerprint + + " with a wait time of " + waitTime + "s"); + try { + Thread.sleep(waitTime * 1000); + } catch (InterruptedException e) { + Log.e(Constants.TAG, "Exception during sleep between key updates", e); + // skip this one + continue; + } + ArrayList<ParcelableKeyRing> keyWrapper = new ArrayList<>(); + keyWrapper.add(keyRing); + if (isUpdateCancelled()) { + return new ImportKeyResult(ImportKeyResult.RESULT_CANCELLED, + new OperationResult.OperationLog()); + } + ImportKeyResult result = + new ImportOperation(context, new ProviderHelper(context), null, mCancelled) + .execute( + new ImportKeyringParcel( + keyWrapper, + Preferences.getPreferences(context) + .getPreferredKeyserver() + ), + cryptoInputParcel + ); + if (result.isPending()) { + return result; + } + accumulator.accumulateKeyImport(result); + } + return accumulator.getConsolidatedResult(); + } + + /** + * 1. Get keys which have been updated recently and therefore do not need to + * be updated now + * 2. Get list of all keys and filter out ones that don't need to be updated + * 3. Return keys to be updated + * + * @return list of keys that require update + */ + private ArrayList<ParcelableKeyRing> getKeysToUpdate(Context context) { + + // 1. Get keys which have been updated recently and don't need to updated now + final int INDEX_UPDATED_KEYS_MASTER_KEY_ID = 0; + final int INDEX_LAST_UPDATED = 1; + + // all time in seconds not milliseconds + final long CURRENT_TIME = GregorianCalendar.getInstance().getTimeInMillis() / 1000; + Cursor updatedKeysCursor = context.getContentResolver().query( + KeychainContract.UpdatedKeys.CONTENT_URI, + new String[]{ + KeychainContract.UpdatedKeys.MASTER_KEY_ID, + KeychainContract.UpdatedKeys.LAST_UPDATED + }, + "? - " + KeychainContract.UpdatedKeys.LAST_UPDATED + " < " + KEY_UPDATE_LIMIT, + new String[]{"" + CURRENT_TIME}, + null + ); + + ArrayList<Long> ignoreMasterKeyIds = new ArrayList<>(); + while (updatedKeysCursor.moveToNext()) { + long masterKeyId = updatedKeysCursor.getLong(INDEX_UPDATED_KEYS_MASTER_KEY_ID); + Log.d(Constants.TAG, "Keyserver sync: Ignoring {" + masterKeyId + "} last updated at {" + + updatedKeysCursor.getLong(INDEX_LAST_UPDATED) + "}s"); + ignoreMasterKeyIds.add(masterKeyId); + } + updatedKeysCursor.close(); + + // 2. Make a list of public keys which should be updated + final int INDEX_MASTER_KEY_ID = 0; + final int INDEX_FINGERPRINT = 1; + Cursor keyCursor = context.getContentResolver().query( + KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), + new String[]{ + KeychainContract.KeyRings.MASTER_KEY_ID, + KeychainContract.KeyRings.FINGERPRINT + }, + null, + null, + null + ); + + if (keyCursor == null) { + return new ArrayList<>(); + } + + ArrayList<ParcelableKeyRing> keyList = new ArrayList<>(); + while (keyCursor.moveToNext()) { + long keyId = keyCursor.getLong(INDEX_MASTER_KEY_ID); + if (ignoreMasterKeyIds.contains(keyId)) { + continue; + } + Log.d(Constants.TAG, "Keyserver sync: Updating {" + keyId + "}"); + String fingerprint = KeyFormattingUtils + .convertFingerprintToHex(keyCursor.getBlob(INDEX_FINGERPRINT)); + String hexKeyId = KeyFormattingUtils + .convertKeyIdToHex(keyId); + // we aren't updating from keybase as of now + keyList.add(new ParcelableKeyRing(fingerprint, hexKeyId, null)); + } + keyCursor.close(); + + return keyList; + } + + private boolean isUpdateCancelled() { + return mCancelled.get(); + } + + /** + * will cancel an update already in progress. We send an Intent to cancel it instead of simply + * modifying a static variable sync the service is running in a process that is different from + * the default application process where the UI code runs. + * + * @param context used to send an Intent to the service requesting cancellation. + */ + public static void cancelUpdates(Context context) { + Intent intent = new Intent(context, KeyserverSyncAdapterService.class); + intent.setAction(ACTION_CANCEL); + context.startService(intent); + } + + private Notification getOrbotNoification(Context context) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(context); + builder.setSmallIcon(R.drawable.ic_stat_notify_24dp) + .setLargeIcon(getBitmap(R.drawable.ic_launcher, context)) + .setContentTitle(context.getString(R.string.keyserver_sync_orbot_notif_title)) + .setContentText(context.getString(R.string.keyserver_sync_orbot_notif_msg)) + .setAutoCancel(true); + + // In case the user decides to not use tor + Intent ignoreTorIntent = new Intent(context, KeyserverSyncAdapterService.class); + ignoreTorIntent.setAction(ACTION_IGNORE_TOR); + PendingIntent ignoreTorPi = PendingIntent.getService( + context, + 0, // security not issue since we're giving this pending intent to Notification Manager + ignoreTorIntent, + PendingIntent.FLAG_CANCEL_CURRENT + ); + + builder.addAction(R.drawable.ic_stat_tor_off, + context.getString(R.string.keyserver_sync_orbot_notif_ignore), + ignoreTorPi); + + Intent startOrbotIntent = new Intent(context, KeyserverSyncAdapterService.class); + startOrbotIntent.setAction(ACTION_START_ORBOT); + PendingIntent startOrbotPi = PendingIntent.getService( + context, + 0, // security not issue since we're giving this pending intent to Notification Manager + startOrbotIntent, + PendingIntent.FLAG_CANCEL_CURRENT + ); + + builder.addAction(R.drawable.ic_stat_tor, + context.getString(R.string.keyserver_sync_orbot_notif_start), + startOrbotPi + ); + builder.setContentIntent(startOrbotPi); + + return builder.build(); + } + + public static void enableKeyserverSync(Context context) { + try { + AccountManager manager = AccountManager.get(context); + Account[] accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE); + + Account account = new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE); + if (accounts.length == 0) { + if (!manager.addAccountExplicitly(account, null, null)) { + Log.e(Constants.TAG, "Adding account failed!"); + } + } + // for keyserver sync + ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1); + ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY, + true); + ContentResolver.addPeriodicSync( + account, + Constants.PROVIDER_AUTHORITY, + new Bundle(), + SYNC_INTERVAL + ); + } catch (SecurityException e) { + Log.e(Constants.TAG, "SecurityException when adding the account", e); + Toast.makeText(context, R.string.reinstall_openkeychain, Toast.LENGTH_LONG).show(); + } + } + + // from de.azapps.mirakel.helper.Helpers from https://github.com/MirakelX/mirakel-android + private Bitmap getBitmap(int resId, Context context) { + int mLargeIconWidth = (int) context.getResources().getDimension( + android.R.dimen.notification_large_icon_width); + int mLargeIconHeight = (int) context.getResources().getDimension( + android.R.dimen.notification_large_icon_height); + Drawable d; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // noinspection deprecation (can't help it at this api level) + d = context.getResources().getDrawable(resId); + } else { + d = context.getDrawable(resId); + } + if (d == null) { + return null; + } + Bitmap b = Bitmap.createBitmap(mLargeIconWidth, mLargeIconHeight, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + d.setBounds(0, 0, mLargeIconWidth, mLargeIconHeight); + d.draw(c); + return b; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java index 78137170d..be269c66d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java @@ -17,6 +17,9 @@ package org.sufficientlysecure.keychain.service; + +import java.util.Date; + import android.app.AlarmManager; import android.app.Notification; import android.app.PendingIntent; @@ -25,8 +28,13 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; import android.os.Binder; import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; @@ -46,8 +54,6 @@ import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; -import java.util.Date; - /** * This service runs in its own process, but is available to all other processes as the main * passphrase cache. Use the static methods addCachedPassphrase and getCachedPassphrase for @@ -95,8 +101,6 @@ public class PassphraseCacheService extends Service { private static final long DEFAULT_TTL = 15; - private static final int NOTIFICATION_ID = 1; - private static final int MSG_PASSPHRASE_CACHE_GET_OKAY = 1; private static final int MSG_PASSPHRASE_CACHE_GET_KEY_NOT_FOUND = 2; @@ -149,6 +153,14 @@ public class PassphraseCacheService extends Service { context.startService(intent); } + public static void clearCachedPassphrases(Context context) { + Log.d(Constants.TAG, "PassphraseCacheService.clearCachedPassphrase()"); + + Intent intent = new Intent(context, PassphraseCacheService.class); + intent.setAction(ACTION_PASSPHRASE_CACHE_CLEAR); + + context.startService(intent); + } /** * Gets a cached passphrase from memory by sending an intent to the service. This method is @@ -218,21 +230,21 @@ public class PassphraseCacheService extends Service { * Internal implementation to get cached passphrase. */ private Passphrase getCachedPassphraseImpl(long masterKeyId, long subKeyId) throws ProviderHelper.NotFoundException { + // on "none" key, just do nothing + if (masterKeyId == Constants.key.none) { + return null; + } + // passphrase for symmetric encryption? if (masterKeyId == Constants.key.symmetric) { Log.d(Constants.TAG, "PassphraseCacheService.getCachedPassphraseImpl() for symmetric encryption"); - Passphrase cachedPassphrase = mPassphraseCache.get(Constants.key.symmetric).getPassphrase(); + CachedPassphrase cachedPassphrase = mPassphraseCache.get(Constants.key.symmetric); if (cachedPassphrase == null) { return null; } addCachedPassphrase(this, Constants.key.symmetric, Constants.key.symmetric, - cachedPassphrase, getString(R.string.passp_cache_notif_pwd)); - return cachedPassphrase; - } - - // on "none" key, just do nothing - if (masterKeyId == Constants.key.none) { - return null; + cachedPassphrase.getPassphrase(), getString(R.string.passp_cache_notif_pwd)); + return cachedPassphrase.getPassphrase(); } // try to get master key id which is used as an identifier for cached passphrases @@ -309,7 +321,7 @@ public class PassphraseCacheService extends Service { if (action.equals(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE)) { long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1); - timeout(context, keyId); + timeout(keyId); } } }; @@ -411,9 +423,9 @@ public class PassphraseCacheService extends Service { long referenceKeyId; if (Preferences.getPreferences(mContext).getPassphraseCacheSubs()) { - referenceKeyId = intent.getLongExtra(EXTRA_KEY_ID, 0L); - } else { referenceKeyId = intent.getLongExtra(EXTRA_SUBKEY_ID, 0L); + } else { + referenceKeyId = intent.getLongExtra(EXTRA_KEY_ID, 0L); } // Stop specific ttl alarm and am.cancel(buildIntent(this, referenceKeyId)); @@ -444,12 +456,17 @@ public class PassphraseCacheService extends Service { /** * Called when one specific passphrase for keyId timed out */ - private void timeout(Context context, long keyId) { + private void timeout(long keyId) { + CachedPassphrase cPass = mPassphraseCache.get(keyId); - // clean internal char[] from memory! - cPass.getPassphrase().removeFromMemory(); - // remove passphrase object - mPassphraseCache.remove(keyId); + if (cPass != null) { + if (cPass.getPassphrase() != null) { + // clean internal char[] from memory! + cPass.getPassphrase().removeFromMemory(); + } + // remove passphrase object + mPassphraseCache.remove(keyId); + } Log.d(Constants.TAG, "PassphraseCacheService Timeout of keyId " + keyId + ", removed from memory!"); @@ -458,7 +475,7 @@ public class PassphraseCacheService extends Service { private void updateService() { if (mPassphraseCache.size() > 0) { - startForeground(NOTIFICATION_ID, getNotification()); + startForeground(Constants.Notification.PASSPHRASE_CACHE, getNotification()); } else { // stop whole service if no cached passphrases remaining Log.d(Constants.TAG, "PassphraseCacheService: No passphrases remaining in memory, stopping service!"); @@ -466,59 +483,67 @@ public class PassphraseCacheService extends Service { } } + // from de.azapps.mirakel.helper.Helpers from https://github.com/MirakelX/mirakel-android + private static Bitmap getBitmap(int resId, Context context) { + int mLargeIconWidth = (int) context.getResources().getDimension( + android.R.dimen.notification_large_icon_width); + int mLargeIconHeight = (int) context.getResources().getDimension( + android.R.dimen.notification_large_icon_height); + Drawable d; + if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) { + // noinspection deprecation (can't help it at this api level) + d = context.getResources().getDrawable(resId); + } else { + d = context.getDrawable(resId); + } + if (d == null) { + return null; + } + Bitmap b = Bitmap.createBitmap(mLargeIconWidth, mLargeIconHeight, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + d.setBounds(0, 0, mLargeIconWidth, mLargeIconHeight); + d.draw(c); + return b; + } + private Notification getNotification() { NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setSmallIcon(R.drawable.ic_stat_notify_24dp) + .setLargeIcon(getBitmap(R.drawable.ic_launcher, getBaseContext())) + .setContentTitle(getResources().getQuantityString(R.plurals.passp_cache_notif_n_keys, + mPassphraseCache.size(), mPassphraseCache.size())) + .setContentText(getString(R.string.passp_cache_notif_click_to_clear)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - builder.setSmallIcon(R.drawable.ic_launcher) - .setContentTitle(getString(R.string.app_name)) - .setContentText(String.format(getString(R.string.passp_cache_notif_n_keys), - mPassphraseCache.size())); + NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); + inboxStyle.setBigContentTitle(getString(R.string.passp_cache_notif_keys)); - inboxStyle.setBigContentTitle(getString(R.string.passp_cache_notif_keys)); + // Moves events into the big view + for (int i = 0; i < mPassphraseCache.size(); i++) { + inboxStyle.addLine(mPassphraseCache.valueAt(i).getPrimaryUserID()); + } - // Moves events into the big view - for (int i = 0; i < mPassphraseCache.size(); i++) { - inboxStyle.addLine(mPassphraseCache.valueAt(i).getPrimaryUserID()); - } + // Moves the big view style object into the notification object. + builder.setStyle(inboxStyle); - // Moves the big view style object into the notification object. - builder.setStyle(inboxStyle); - - // Add purging action - Intent intent = new Intent(getApplicationContext(), PassphraseCacheService.class); - intent.setAction(ACTION_PASSPHRASE_CACHE_CLEAR); - builder.addAction( - R.drawable.abc_ic_clear_mtrl_alpha, - getString(R.string.passp_cache_notif_clear), - PendingIntent.getService( - getApplicationContext(), - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT - ) - ); - } else { - // Fallback, since expandable notifications weren't available back then - builder.setSmallIcon(R.drawable.ic_launcher) - .setContentTitle(String.format(getString(R.string.passp_cache_notif_n_keys), - mPassphraseCache.size())) - .setContentText(getString(R.string.passp_cache_notif_click_to_clear)); - - Intent intent = new Intent(getApplicationContext(), PassphraseCacheService.class); - intent.setAction(ACTION_PASSPHRASE_CACHE_CLEAR); - - builder.setContentIntent( - PendingIntent.getService( - getApplicationContext(), - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT - ) - ); - } + Intent intent = new Intent(getApplicationContext(), PassphraseCacheService.class); + intent.setAction(ACTION_PASSPHRASE_CACHE_CLEAR); + PendingIntent clearCachePi = PendingIntent.getService( + getApplicationContext(), + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT + ); + + // Add cache clear PI to normal touch + builder.setContentIntent(clearCachePi); + + // Add clear PI action below text + builder.addAction( + R.drawable.abc_ic_clear_mtrl_alpha, + getString(R.string.passp_cache_notif_clear), + clearCachePi + ); return builder.build(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PromoteKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PromoteKeyringParcel.java new file mode 100644 index 000000000..d268c3694 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PromoteKeyringParcel.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.service; + +import android.os.Parcel; +import android.os.Parcelable; + +public class PromoteKeyringParcel implements Parcelable { + + public long mKeyRingId; + public byte[] mCardAid; + public long[] mSubKeyIds; + + public PromoteKeyringParcel(long keyRingId, byte[] cardAid, long[] subKeyIds) { + mKeyRingId = keyRingId; + mCardAid = cardAid; + mSubKeyIds = subKeyIds; + } + + protected PromoteKeyringParcel(Parcel in) { + mKeyRingId = in.readLong(); + mCardAid = in.createByteArray(); + mSubKeyIds = in.createLongArray(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mKeyRingId); + dest.writeByteArray(mCardAid); + dest.writeLongArray(mSubKeyIds); + } + + public static final Parcelable.Creator<PromoteKeyringParcel> CREATOR = new Parcelable.Creator<PromoteKeyringParcel>() { + @Override + public PromoteKeyringParcel createFromParcel(Parcel in) { + return new PromoteKeyringParcel(in); + } + + @Override + public PromoteKeyringParcel[] newArray(int size) { + return new PromoteKeyringParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/RevokeKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/RevokeKeyringParcel.java new file mode 100644 index 000000000..02e8fda46 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/RevokeKeyringParcel.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.service; + +import android.os.Parcel; +import android.os.Parcelable; + +public class RevokeKeyringParcel implements Parcelable { + + final public long mMasterKeyId; + final public boolean mUpload; + final public String mKeyserver; + + public RevokeKeyringParcel(long masterKeyId, boolean upload, String keyserver) { + mMasterKeyId = masterKeyId; + mUpload = upload; + mKeyserver = keyserver; + } + + protected RevokeKeyringParcel(Parcel in) { + mMasterKeyId = in.readLong(); + mUpload = in.readByte() != 0x00; + mKeyserver = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mMasterKeyId); + dest.writeByte((byte) (mUpload ? 0x01 : 0x00)); + dest.writeString(mKeyserver); + } + + public static final Parcelable.Creator<RevokeKeyringParcel> CREATOR = new Parcelable.Creator<RevokeKeyringParcel>() { + @Override + public RevokeKeyringParcel createFromParcel(Parcel in) { + return new RevokeKeyringParcel(in); + } + + @Override + public RevokeKeyringParcel[] newArray(int size) { + return new RevokeKeyringParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java index 2e0524141..6959fca56 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java @@ -61,6 +61,15 @@ public class SaveKeyringParcel implements Parcelable { public ArrayList<String> mRevokeUserIds; public ArrayList<Long> mRevokeSubKeys; + // if these are non-null, PINs will be changed on the card + public Passphrase mCardPin; + public Passphrase mCardAdminPin; + + // private because they have to be set together with setUpdateOptions + private boolean mUpload; + private boolean mUploadAtomic; + private String mKeyserver; + public SaveKeyringParcel() { reset(); } @@ -80,6 +89,29 @@ public class SaveKeyringParcel implements Parcelable { mChangeSubKeys = new ArrayList<>(); mRevokeUserIds = new ArrayList<>(); mRevokeSubKeys = new ArrayList<>(); + mCardPin = null; + mCardAdminPin = null; + mUpload = false; + mUploadAtomic = false; + mKeyserver = null; + } + + public void setUpdateOptions(boolean upload, boolean uploadAtomic, String keysever) { + mUpload = upload; + mUploadAtomic = uploadAtomic; + mKeyserver = keysever; + } + + public boolean isUpload() { + return mUpload; + } + + public boolean isUploadAtomic() { + return mUploadAtomic; + } + + public String getUploadKeyserver() { + return mKeyserver; } public boolean isEmpty() { @@ -95,7 +127,8 @@ public class SaveKeyringParcel implements Parcelable { } for (SubkeyChange change : mChangeSubKeys) { - if (change.mRecertify || change.mFlags != null || change.mExpiry != null) { + if (change.mRecertify || change.mFlags != null || change.mExpiry != null + || change.mMoveKeyToCard) { return false; } } @@ -142,6 +175,8 @@ public class SaveKeyringParcel implements Parcelable { public boolean mRecertify; // if this flag is true, the subkey should be changed to a stripped key public boolean mDummyStrip; + // if this flag is true, the subkey should be moved to a card + public boolean mMoveKeyToCard; // if this is non-null, the subkey will be changed to a divert-to-card // key for the given serial number public byte[] mDummyDivert; @@ -161,16 +196,16 @@ public class SaveKeyringParcel implements Parcelable { mExpiry = expiry; } - public SubkeyChange(long keyId, boolean dummyStrip, byte[] dummyDivert) { + public SubkeyChange(long keyId, boolean dummyStrip, boolean moveKeyToCard) { this(keyId, null, null); // these flags are mutually exclusive! - if (dummyStrip && dummyDivert != null) { + if (dummyStrip && moveKeyToCard) { throw new AssertionError( - "cannot set strip and divert flags at the same time - this is a bug!"); + "cannot set strip and keytocard flags at the same time - this is a bug!"); } mDummyStrip = dummyStrip; - mDummyDivert = dummyDivert; + mMoveKeyToCard = moveKeyToCard; } @Override @@ -179,6 +214,7 @@ public class SaveKeyringParcel implements Parcelable { out += "mFlags: " + mFlags + ", "; out += "mExpiry: " + mExpiry + ", "; out += "mDummyStrip: " + mDummyStrip + ", "; + out += "mMoveKeyToCard: " + mMoveKeyToCard + ", "; out += "mDummyDivert: [" + (mDummyDivert == null ? 0 : mDummyDivert.length) + " bytes]"; return out; @@ -206,6 +242,7 @@ public class SaveKeyringParcel implements Parcelable { } } + @SuppressWarnings("unchecked") // we verify the reads against writes in writeToParcel public SaveKeyringParcel(Parcel source) { mMasterKeyId = source.readInt() != 0 ? source.readLong() : null; mFingerprint = source.createByteArray(); @@ -221,6 +258,13 @@ public class SaveKeyringParcel implements Parcelable { mRevokeUserIds = source.createStringArrayList(); mRevokeSubKeys = (ArrayList<Long>) source.readSerializable(); + + mCardPin = source.readParcelable(Passphrase.class.getClassLoader()); + mCardAdminPin = source.readParcelable(Passphrase.class.getClassLoader()); + + mUpload = source.readByte() != 0; + mUploadAtomic = source.readByte() != 0; + mKeyserver = source.readString(); } @Override @@ -232,7 +276,7 @@ public class SaveKeyringParcel implements Parcelable { destination.writeByteArray(mFingerprint); // yes, null values are ok for parcelables - destination.writeParcelable(mNewUnlock, 0); + destination.writeParcelable(mNewUnlock, flags); destination.writeStringList(mAddUserIds); destination.writeSerializable(mAddUserAttribute); @@ -243,6 +287,13 @@ public class SaveKeyringParcel implements Parcelable { destination.writeStringList(mRevokeUserIds); destination.writeSerializable(mRevokeSubKeys); + + destination.writeParcelable(mCardPin, flags); + destination.writeParcelable(mCardAdminPin, flags); + + destination.writeByte((byte) (mUpload ? 1 : 0)); + destination.writeByte((byte) (mUploadAtomic ? 1 : 0)); + destination.writeString(mKeyserver); } public static final Creator<SaveKeyringParcel> CREATOR = new Creator<SaveKeyringParcel>() { @@ -270,7 +321,9 @@ public class SaveKeyringParcel implements Parcelable { out += "mChangeSubKeys: " + mChangeSubKeys + "\n"; out += "mChangePrimaryUserId: " + mChangePrimaryUserId + "\n"; out += "mRevokeUserIds: " + mRevokeUserIds + "\n"; - out += "mRevokeSubKeys: " + mRevokeSubKeys; + out += "mRevokeSubKeys: " + mRevokeSubKeys + "\n"; + out += "mCardPin: " + mCardPin + "\n"; + out += "mCardAdminPin: " + mCardAdminPin; return out; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java index 430d8a49b..d294e5057 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ServiceProgressHandler.java @@ -17,8 +17,8 @@ package org.sufficientlysecure.keychain.service; -import android.app.Activity; -import android.content.Intent; + +import android.app.ProgressDialog; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -27,7 +27,6 @@ import android.support.v4.app.FragmentManager; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.CertifyResult; import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Log; @@ -35,7 +34,7 @@ import org.sufficientlysecure.keychain.util.Log; public class ServiceProgressHandler extends Handler { // possible messages sent from this service to handler on ui - public static enum MessageStatus{ + public enum MessageStatus { UNKNOWN, OKAY, EXCEPTION, @@ -44,9 +43,8 @@ public class ServiceProgressHandler extends Handler { private static final MessageStatus[] values = values(); - public static MessageStatus fromInt(int n) - { - if(n < 0 || n >= values.length) { + public static MessageStatus fromInt(int n) { + if (n < 0 || n >= values.length) { return UNKNOWN; } else { return values[n]; @@ -66,74 +64,51 @@ public class ServiceProgressHandler extends Handler { public static final String KEYBASE_PRESENCE_URL = "keybase_presence_url"; public static final String KEYBASE_PRESENCE_LABEL = "keybase_presence_label"; - Activity mActivity; - ProgressDialogFragment mProgressDialogFragment; + public static final String TAG_PROGRESS_DIALOG = "progressDialog"; - public ServiceProgressHandler(Activity activity) { - this.mActivity = activity; - } + FragmentActivity mActivity; - public ServiceProgressHandler(Activity activity, - ProgressDialogFragment progressDialogFragment) { - this.mActivity = activity; - this.mProgressDialogFragment = progressDialogFragment; + public ServiceProgressHandler(FragmentActivity activity) { + mActivity = activity; } - public ServiceProgressHandler(Activity activity, - String progressDialogMessage, - int progressDialogStyle, - ProgressDialogFragment.ServiceType serviceType) { - this(activity, progressDialogMessage, progressDialogStyle, false, serviceType); + public void showProgressDialog() { + showProgressDialog("", ProgressDialog.STYLE_SPINNER, false); } - public ServiceProgressHandler(Activity activity, - String progressDialogMessage, - int progressDialogStyle, - boolean cancelable, - ProgressDialogFragment.ServiceType serviceType) { - this.mActivity = activity; - this.mProgressDialogFragment = ProgressDialogFragment.newInstance( + public void showProgressDialog( + String progressDialogMessage, int progressDialogStyle, boolean cancelable) { + + final ProgressDialogFragment frag = ProgressDialogFragment.newInstance( progressDialogMessage, progressDialogStyle, - cancelable, - serviceType); - } - - public void showProgressDialog(FragmentActivity activity) { - if (mProgressDialogFragment == null) { - return; - } + cancelable); // TODO: This is a hack!, see // http://stackoverflow.com/questions/10114324/show-dialogfragment-from-onactivityresult - final FragmentManager manager = activity.getSupportFragmentManager(); + final FragmentManager manager = mActivity.getSupportFragmentManager(); Handler handler = new Handler(); handler.post(new Runnable() { public void run() { - mProgressDialogFragment.show(manager, "progressDialog"); + frag.show(manager, TAG_PROGRESS_DIALOG); } }); + } @Override public void handleMessage(Message message) { Bundle data = message.getData(); - if (mProgressDialogFragment == null) { - // Log.e(Constants.TAG, - // "Progress has not been updated because mProgressDialogFragment was null!"); - return; - } - MessageStatus status = MessageStatus.fromInt(message.arg1); switch (status) { case OKAY: - mProgressDialogFragment.dismissAllowingStateLoss(); + dismissAllowingStateLoss(); break; case EXCEPTION: - mProgressDialogFragment.dismissAllowingStateLoss(); + dismissAllowingStateLoss(); // show error from service if (data.containsKey(DATA_ERROR)) { @@ -147,23 +122,25 @@ public class ServiceProgressHandler extends Handler { case UPDATE_PROGRESS: if (data.containsKey(DATA_PROGRESS) && data.containsKey(DATA_PROGRESS_MAX)) { + String msg = null; + int progress = data.getInt(DATA_PROGRESS); + int max = data.getInt(DATA_PROGRESS_MAX); + // update progress from service if (data.containsKey(DATA_MESSAGE)) { - mProgressDialogFragment.setProgress(data.getString(DATA_MESSAGE), - data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX)); + msg = data.getString(DATA_MESSAGE); } else if (data.containsKey(DATA_MESSAGE_ID)) { - mProgressDialogFragment.setProgress(data.getInt(DATA_MESSAGE_ID), - data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX)); - } else { - mProgressDialogFragment.setProgress(data.getInt(DATA_PROGRESS), - data.getInt(DATA_PROGRESS_MAX)); + msg = mActivity.getString(data.getInt(DATA_MESSAGE_ID)); } + + onSetProgress(msg, progress, max); + } break; case PREVENT_CANCEL: - mProgressDialogFragment.setPreventCancel(true); + setPreventCancel(true); break; default: @@ -171,4 +148,48 @@ public class ServiceProgressHandler extends Handler { break; } } + + private void setPreventCancel(boolean preventCancel) { + ProgressDialogFragment progressDialogFragment = + (ProgressDialogFragment) mActivity.getSupportFragmentManager() + .findFragmentByTag("progressDialog"); + + if (progressDialogFragment == null) { + return; + } + + progressDialogFragment.setPreventCancel(preventCancel); + } + + protected void dismissAllowingStateLoss() { + ProgressDialogFragment progressDialogFragment = + (ProgressDialogFragment) mActivity.getSupportFragmentManager() + .findFragmentByTag("progressDialog"); + + if (progressDialogFragment == null) { + return; + } + + progressDialogFragment.dismissAllowingStateLoss(); + } + + + protected void onSetProgress(String msg, int progress, int max) { + + ProgressDialogFragment progressDialogFragment = + (ProgressDialogFragment) mActivity.getSupportFragmentManager() + .findFragmentByTag("progressDialog"); + + if (progressDialogFragment == null) { + return; + } + + if (msg != null) { + progressDialogFragment.setProgress(msg, progress, max); + } else { + progressDialogFragment.setProgress(progress, max); + } + + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java index 3d1ccaca1..0d8569fe6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java @@ -17,52 +17,87 @@ package org.sufficientlysecure.keychain.service.input; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - import android.os.Parcel; import android.os.Parcelable; +import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Passphrase; +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + /** * This is a base class for the input of crypto operations. */ public class CryptoInputParcel implements Parcelable { - final Date mSignatureTime; - final Passphrase mPassphrase; + private Date mSignatureTime; + private boolean mHasSignature; + + public Passphrase mPassphrase; + // used to supply an explicit proxy to operations that require it + // this is not final so it can be added to an existing CryptoInputParcel + // (e.g) CertifyOperation with upload might require both passphrase and orbot to be enabled + private ParcelableProxy mParcelableProxy; + + // specifies whether passphrases should be cached + public boolean mCachePassphrase = true; // this map contains both decrypted session keys and signed hashes to be // used in the crypto operation described by this parcel. private HashMap<ByteBuffer, byte[]> mCryptoData = new HashMap<>(); public CryptoInputParcel() { - mSignatureTime = new Date(); + mSignatureTime = null; mPassphrase = null; + mCachePassphrase = true; } public CryptoInputParcel(Date signatureTime, Passphrase passphrase) { + mHasSignature = true; mSignatureTime = signatureTime == null ? new Date() : signatureTime; mPassphrase = passphrase; + mCachePassphrase = true; } public CryptoInputParcel(Passphrase passphrase) { - mSignatureTime = new Date(); mPassphrase = passphrase; + mCachePassphrase = true; } public CryptoInputParcel(Date signatureTime) { + mHasSignature = true; mSignatureTime = signatureTime == null ? new Date() : signatureTime; mPassphrase = null; + mCachePassphrase = true; + } + + public CryptoInputParcel(ParcelableProxy parcelableProxy) { + this(); + mParcelableProxy = parcelableProxy; + } + + public CryptoInputParcel(Date signatureTime, boolean cachePassphrase) { + mHasSignature = true; + mSignatureTime = signatureTime == null ? new Date() : signatureTime; + mPassphrase = null; + mCachePassphrase = cachePassphrase; + } + + public CryptoInputParcel(boolean cachePassphrase) { + mCachePassphrase = cachePassphrase; } protected CryptoInputParcel(Parcel source) { - mSignatureTime = new Date(source.readLong()); + mHasSignature = source.readByte() != 0; + if (mHasSignature) { + mSignatureTime = new Date(source.readLong()); + } mPassphrase = source.readParcelable(getClass().getClassLoader()); + mParcelableProxy = source.readParcelable(getClass().getClassLoader()); + mCachePassphrase = source.readByte() != 0; { int count = source.readInt(); @@ -83,8 +118,13 @@ public class CryptoInputParcel implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeLong(mSignatureTime.getTime()); + dest.writeByte((byte) (mHasSignature ? 1 : 0)); + if (mHasSignature) { + dest.writeLong(mSignatureTime.getTime()); + } dest.writeParcelable(mPassphrase, 0); + dest.writeParcelable(mParcelableProxy, 0); + dest.writeByte((byte) (mCachePassphrase ? 1 : 0)); dest.writeInt(mCryptoData.size()); for (HashMap.Entry<ByteBuffer, byte[]> entry : mCryptoData.entrySet()) { @@ -93,12 +133,28 @@ public class CryptoInputParcel implements Parcelable { } } + public void addParcelableProxy(ParcelableProxy parcelableProxy) { + mParcelableProxy = parcelableProxy; + } + + public void addSignatureTime(Date signatureTime) { + mSignatureTime = signatureTime; + } + public void addCryptoData(byte[] hash, byte[] signedHash) { mCryptoData.put(ByteBuffer.wrap(hash), signedHash); } + public void addCryptoData(Map<ByteBuffer, byte[]> cachedSessionKeys) { + mCryptoData.putAll(cachedSessionKeys); + } + + public ParcelableProxy getParcelableProxy() { + return mParcelableProxy; + } + public Map<ByteBuffer, byte[]> getCryptoData() { - return Collections.unmodifiableMap(mCryptoData); + return mCryptoData; } public Date getSignatureTime() { @@ -138,4 +194,5 @@ public class CryptoInputParcel implements Parcelable { b.append("}"); return b.toString(); } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java index 535c1e735..e4dac3227 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java @@ -1,35 +1,37 @@ package org.sufficientlysecure.keychain.service.input; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; - import android.os.Parcel; import android.os.Parcelable; -import org.sufficientlysecure.keychain.Constants.key; +import org.sufficientlysecure.keychain.util.Passphrase; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; public class RequiredInputParcel implements Parcelable { public enum RequiredInputType { - PASSPHRASE, PASSPHRASE_SYMMETRIC, NFC_SIGN, NFC_DECRYPT + PASSPHRASE, PASSPHRASE_SYMMETRIC, NFC_SIGN, NFC_DECRYPT, NFC_MOVE_KEY_TO_CARD, ENABLE_ORBOT, + UPLOAD_FAIL_RETRY } public Date mSignatureTime; public final RequiredInputType mType; - public final byte[][] mInputHashes; + public final byte[][] mInputData; public final int[] mSignAlgos; private Long mMasterKeyId; private Long mSubKeyId; - private RequiredInputParcel(RequiredInputType type, byte[][] inputHashes, + private RequiredInputParcel(RequiredInputType type, byte[][] inputData, int[] signAlgos, Date signatureTime, Long masterKeyId, Long subKeyId) { mType = type; - mInputHashes = inputHashes; + mInputData = inputData; mSignAlgos = signAlgos; mSignatureTime = signatureTime; mMasterKeyId = masterKeyId; @@ -39,25 +41,25 @@ public class RequiredInputParcel implements Parcelable { public RequiredInputParcel(Parcel source) { mType = RequiredInputType.values()[source.readInt()]; - // 0 = none, 1 = both, 2 = only hashes (decrypt) - int hashTypes = source.readInt(); - if (hashTypes != 0) { + // 0 = none, 1 = signAlgos + inputData, 2 = only inputData (decrypt) + int inputDataType = source.readInt(); + if (inputDataType != 0) { int count = source.readInt(); - mInputHashes = new byte[count][]; - if (hashTypes == 1) { + mInputData = new byte[count][]; + if (inputDataType == 1) { mSignAlgos = new int[count]; for (int i = 0; i < count; i++) { - mInputHashes[i] = source.createByteArray(); + mInputData[i] = source.createByteArray(); mSignAlgos[i] = source.readInt(); } } else { mSignAlgos = null; for (int i = 0; i < count; i++) { - mInputHashes[i] = source.createByteArray(); + mInputData[i] = source.createByteArray(); } } } else { - mInputHashes = null; + mInputData = null; mSignAlgos = null; } @@ -75,16 +77,27 @@ public class RequiredInputParcel implements Parcelable { return mSubKeyId; } + public static RequiredInputParcel createRetryUploadOperation() { + return new RequiredInputParcel(RequiredInputType.UPLOAD_FAIL_RETRY, + null, null, null, 0L, 0L); + } + + public static RequiredInputParcel createOrbotRequiredOperation() { + return new RequiredInputParcel(RequiredInputType.ENABLE_ORBOT, null, null, null, 0L, 0L); + } + public static RequiredInputParcel createNfcSignOperation( + long masterKeyId, long subKeyId, byte[] inputHash, int signAlgo, Date signatureTime) { return new RequiredInputParcel(RequiredInputType.NFC_SIGN, new byte[][] { inputHash }, new int[] { signAlgo }, - signatureTime, null, null); + signatureTime, masterKeyId, subKeyId); } - public static RequiredInputParcel createNfcDecryptOperation(byte[] inputHash, long subKeyId) { + public static RequiredInputParcel createNfcDecryptOperation( + long masterKeyId, long subKeyId, byte[] encryptedSessionKey) { return new RequiredInputParcel(RequiredInputType.NFC_DECRYPT, - new byte[][] { inputHash }, null, null, null, subKeyId); + new byte[][] { encryptedSessionKey }, null, null, masterKeyId, subKeyId); } public static RequiredInputParcel createRequiredSignPassphrase( @@ -118,11 +131,11 @@ public class RequiredInputParcel implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mType.ordinal()); - if (mInputHashes != null) { + if (mInputData != null) { dest.writeInt(mSignAlgos != null ? 1 : 2); - dest.writeInt(mInputHashes.length); - for (int i = 0; i < mInputHashes.length; i++) { - dest.writeByteArray(mInputHashes[i]); + dest.writeInt(mInputData.length); + for (int i = 0; i < mInputData.length; i++) { + dest.writeByteArray(mInputData[i]); if (mSignAlgos != null) { dest.writeInt(mSignAlgos[i]); } @@ -165,10 +178,10 @@ public class RequiredInputParcel implements Parcelable { Date mSignatureTime; ArrayList<Integer> mSignAlgos = new ArrayList<>(); ArrayList<byte[]> mInputHashes = new ArrayList<>(); - Long mMasterKeyId; - Long mSubKeyId; + long mMasterKeyId; + long mSubKeyId; - public NfcSignOperationsBuilder(Date signatureTime, Long masterKeyId, Long subKeyId) { + public NfcSignOperationsBuilder(Date signatureTime, long masterKeyId, long subKeyId) { mSignatureTime = signatureTime; mMasterKeyId = masterKeyId; mSubKeyId = subKeyId; @@ -199,7 +212,7 @@ public class RequiredInputParcel implements Parcelable { throw new AssertionError("operation types must match, this is a progrmming error!"); } - Collections.addAll(mInputHashes, input.mInputHashes); + Collections.addAll(mInputHashes, input.mInputData); for (int signAlgo : input.mSignAlgos) { mSignAlgos.add(signAlgo); } @@ -211,4 +224,66 @@ public class RequiredInputParcel implements Parcelable { } + public static class NfcKeyToCardOperationsBuilder { + ArrayList<byte[]> mSubkeysToExport = new ArrayList<>(); + Long mMasterKeyId; + byte[] mPin; + byte[] mAdminPin; + + public NfcKeyToCardOperationsBuilder(Long masterKeyId) { + mMasterKeyId = masterKeyId; + } + + public RequiredInputParcel build() { + byte[][] inputData = new byte[mSubkeysToExport.size() + 2][]; + + // encode all subkeys into inputData + byte[][] subkeyData = new byte[mSubkeysToExport.size()][]; + mSubkeysToExport.toArray(subkeyData); + + // first two are PINs + inputData[0] = mPin; + inputData[1] = mAdminPin; + // then subkeys + System.arraycopy(subkeyData, 0, inputData, 2, subkeyData.length); + + ByteBuffer buf = ByteBuffer.wrap(mSubkeysToExport.get(0)); + + // We need to pass in a subkey here... + return new RequiredInputParcel(RequiredInputType.NFC_MOVE_KEY_TO_CARD, + inputData, null, null, mMasterKeyId, buf.getLong()); + } + + public void addSubkey(long subkeyId) { + byte[] subKeyId = new byte[8]; + ByteBuffer buf = ByteBuffer.wrap(subKeyId); + buf.putLong(subkeyId).rewind(); + mSubkeysToExport.add(subKeyId); + } + + public void setPin(Passphrase pin) { + mPin = pin.toStringUnsafe().getBytes(); + } + + public void setAdminPin(Passphrase adminPin) { + mAdminPin = adminPin.toStringUnsafe().getBytes(); + } + + public void addAll(RequiredInputParcel input) { + if (!mMasterKeyId.equals(input.mMasterKeyId)) { + throw new AssertionError("Master keys must match, this is a programming error!"); + } + if (input.mType != RequiredInputType.NFC_MOVE_KEY_TO_CARD) { + throw new AssertionError("Operation types must match, this is a programming error!"); + } + + Collections.addAll(mSubkeysToExport, input.mInputData); + } + + public boolean isEmpty() { + return mSubkeysToExport.isEmpty(); + } + + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java new file mode 100644 index 000000000..a3ea8ad9a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2015 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.ui; + + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Locale; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Intent; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.util.ExportHelper; + +public class BackupFragment extends Fragment { + + // This ids for multiple key export. + private ArrayList<Long> mIdsForRepeatAskPassphrase; + // This index for remembering the number of master key. + private int mIndex; + + static final int REQUEST_REPEAT_PASSPHRASE = 1; + private ExportHelper mExportHelper; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + // we won't get attached to a non-fragment activity, so the cast should be safe + mExportHelper = new ExportHelper((FragmentActivity) activity); + } + + @Override + public void onDetach() { + super.onDetach(); + mExportHelper = null; + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.backup_fragment, container, false); + + View backupAll = view.findViewById(R.id.backup_all); + View backupPublicKeys = view.findViewById(R.id.backup_public_keys); + + backupAll.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + exportToFile(true); + } + }); + + backupPublicKeys.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + exportToFile(false); + } + }); + + return view; + } + + private void exportToFile(boolean includeSecretKeys) { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + if (!includeSecretKeys) { + startBackup(false); + return; + } + + new AsyncTask<ContentResolver,Void,ArrayList<Long>>() { + @Override + protected ArrayList<Long> doInBackground(ContentResolver... resolver) { + ArrayList<Long> askPassphraseIds = new ArrayList<>(); + Cursor cursor = resolver[0].query( + KeyRings.buildUnifiedKeyRingsUri(), new String[] { + KeyRings.MASTER_KEY_ID, + KeyRings.HAS_SECRET, + }, KeyRings.HAS_SECRET + " != 0", null, null); + try { + if (cursor != null) { + while (cursor.moveToNext()) { + SecretKeyType secretKeyType = SecretKeyType.fromNum(cursor.getInt(1)); + switch (secretKeyType) { + // all of these make no sense to ask + case PASSPHRASE_EMPTY: + case GNU_DUMMY: + case DIVERT_TO_CARD: + case UNAVAILABLE: + continue; + default: { + long keyId = cursor.getLong(0); + askPassphraseIds.add(keyId); + } + } + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return askPassphraseIds; + } + + @Override + protected void onPostExecute(ArrayList<Long> askPassphraseIds) { + super.onPostExecute(askPassphraseIds); + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + mIdsForRepeatAskPassphrase = askPassphraseIds; + mIndex = 0; + + if (mIdsForRepeatAskPassphrase.size() != 0) { + startPassphraseActivity(); + return; + } + + startBackup(true); + } + + }.execute(activity.getContentResolver()); + + } + + private void startPassphraseActivity() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + Intent intent = new Intent(activity, PassphraseDialogActivity.class); + long masterKeyId = mIdsForRepeatAskPassphrase.get(mIndex++); + intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, masterKeyId); + startActivityForResult(intent, REQUEST_REPEAT_PASSPHRASE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_REPEAT_PASSPHRASE) { + if (resultCode != Activity.RESULT_OK) { + return; + } + if (mIndex < mIdsForRepeatAskPassphrase.size()) { + startPassphraseActivity(); + return; + } + + startBackup(true); + } + } + + private void startBackup(boolean exportSecret) { + File filename; + String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + if (exportSecret) { + filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".asc"); + } else { + filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".pub.asc"); + } + mExportHelper.showExportKeysDialog(null, filename, exportSecret); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java index 016ab5f3c..c5528e40b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java @@ -30,6 +30,8 @@ public class CertifyFingerprintActivity extends BaseActivity { protected Uri mDataUri; + public static final String EXTRA_ENABLE_WORD_CONFIRM = "enable_word_confirm"; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -40,6 +42,7 @@ public class CertifyFingerprintActivity extends BaseActivity { finish(); return; } + boolean enableWordConfirm = getIntent().getBooleanExtra(EXTRA_ENABLE_WORD_CONFIRM, false); setFullScreenDialogClose(new View.OnClickListener() { @Override @@ -50,7 +53,7 @@ public class CertifyFingerprintActivity extends BaseActivity { Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); - startFragment(savedInstanceState, mDataUri); + startFragment(savedInstanceState, mDataUri, enableWordConfirm); } @Override @@ -58,7 +61,7 @@ public class CertifyFingerprintActivity extends BaseActivity { setContentView(R.layout.certify_fingerprint_activity); } - private void startFragment(Bundle savedInstanceState, Uri dataUri) { + private void startFragment(Bundle savedInstanceState, Uri dataUri, boolean enableWordConfirm) { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. @@ -67,7 +70,7 @@ public class CertifyFingerprintActivity extends BaseActivity { } // Create an instance of the fragment - CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(dataUri); + CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(dataUri, enableWordConfirm); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java index a6b8a0e85..552fa34c0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui; import android.content.Intent; import android.database.Cursor; +import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.LoaderManager; @@ -34,6 +35,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.util.ExperimentalWordConfirm; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; @@ -44,23 +46,24 @@ public class CertifyFingerprintFragment extends LoaderFragment implements static final int REQUEST_CERTIFY = 1; public static final String ARG_DATA_URI = "uri"; + public static final String ARG_ENABLE_WORD_CONFIRM = "enable_word_confirm"; private TextView mFingerprint; + private TextView mIntro; private static final int LOADER_ID_UNIFIED = 0; private Uri mDataUri; - - private View mActionNo; - private View mActionYes; + private boolean mEnableWordConfirm; /** * Creates new instance of this fragment */ - public static CertifyFingerprintFragment newInstance(Uri dataUri) { + public static CertifyFingerprintFragment newInstance(Uri dataUri, boolean enableWordConfirm) { CertifyFingerprintFragment frag = new CertifyFingerprintFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_DATA_URI, dataUri); + args.putBoolean(ARG_ENABLE_WORD_CONFIRM, enableWordConfirm); frag.setArguments(args); @@ -72,18 +75,19 @@ public class CertifyFingerprintFragment extends LoaderFragment implements View root = super.onCreateView(inflater, superContainer, savedInstanceState); View view = inflater.inflate(R.layout.certify_fingerprint_fragment, getContainer()); - mActionNo = view.findViewById(R.id.certify_fingerprint_button_no); - mActionYes = view.findViewById(R.id.certify_fingerprint_button_yes); + View actionNo = view.findViewById(R.id.certify_fingerprint_button_no); + View actionYes = view.findViewById(R.id.certify_fingerprint_button_yes); mFingerprint = (TextView) view.findViewById(R.id.certify_fingerprint_fingerprint); + mIntro = (TextView) view.findViewById(R.id.certify_fingerprint_intro); - mActionNo.setOnClickListener(new View.OnClickListener() { + actionNo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { getActivity().finish(); } }); - mActionYes.setOnClickListener(new View.OnClickListener() { + actionYes.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { certify(mDataUri); @@ -103,6 +107,11 @@ public class CertifyFingerprintFragment extends LoaderFragment implements getActivity().finish(); return; } + mEnableWordConfirm = getArguments().getBoolean(ARG_ENABLE_WORD_CONFIRM); + + if (mEnableWordConfirm) { + mIntro.setText(R.string.certify_fingerprint_text_words); + } loadData(dataUri); } @@ -149,10 +158,13 @@ public class CertifyFingerprintFragment extends LoaderFragment implements switch (loader.getId()) { case LOADER_ID_UNIFIED: { if (data.moveToFirst()) { - byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); - mFingerprint.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint)); + + if (mEnableWordConfirm) { + displayWordConfirm(fingerprintBlob); + } else { + displayHexConfirm(fingerprintBlob); + } break; } @@ -162,6 +174,19 @@ public class CertifyFingerprintFragment extends LoaderFragment implements setContentShown(true); } + private void displayHexConfirm(byte[] fingerprintBlob) { + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); + mFingerprint.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint)); + } + + private void displayWordConfirm(byte[] fingerprintBlob) { + String fingerprint = ExperimentalWordConfirm.getWords(getActivity(), fingerprintBlob); + + mFingerprint.setTextSize(24); + mFingerprint.setTypeface(Typeface.DEFAULT, Typeface.BOLD); + mFingerprint.setText(fingerprint); + } + /** * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. * We need to make sure we are no longer using it. diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java index 51b6e824d..357b445f0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java @@ -19,15 +19,12 @@ package org.sufficientlysecure.keychain.ui; import android.app.Activity; -import android.app.ProgressDialog; import android.content.Intent; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.PorterDuff; import android.net.Uri; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; import android.os.Parcel; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; @@ -52,24 +49,24 @@ import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.adapter.MultiUserIdsAdapter; -import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.base.CachingCryptoOperationFragment; import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner; -import org.sufficientlysecure.keychain.ui.widget.KeySpinner; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; import java.util.ArrayList; +import java.util.Date; - -public class CertifyKeyFragment extends CryptoOperationFragment +public class CertifyKeyFragment + extends CachingCryptoOperationFragment<CertifyActionsParcel, CertifyResult> implements LoaderManager.LoaderCallbacks<Cursor> { + public static final String ARG_CHECK_STATES = "check_states"; + private CheckBox mUploadKeyCheckbox; ListView mUserIds; @@ -77,8 +74,6 @@ public class CertifyKeyFragment extends CryptoOperationFragment private long[] mPubMasterKeyIds; - private long mSignMasterKeyId = Constants.key.none; - public static final String[] USER_IDS_PROJECTION = new String[]{ UserPackets._ID, UserPackets.MASTER_KEY_ID, @@ -94,7 +89,6 @@ public class CertifyKeyFragment extends CryptoOperationFragment private static final int INDEX_IS_REVOKED = 4; private MultiUserIdsAdapter mUserIdsAdapter; - private Messenger mPassthroughMessenger; @Override public void onActivityCreated(Bundle savedInstanceState) { @@ -107,24 +101,30 @@ public class CertifyKeyFragment extends CryptoOperationFragment return; } - mPassthroughMessenger = getActivity().getIntent().getParcelableExtra( - KeychainIntentService.EXTRA_MESSENGER); - mPassthroughMessenger = null; // TODO remove, development hack - - // preselect certify key id if given - long certifyKeyId = getActivity().getIntent().getLongExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, Constants.key.none); - if (certifyKeyId != Constants.key.none) { - try { - CachedPublicKeyRing key = (new ProviderHelper(getActivity())).getCachedPublicKeyRing(certifyKeyId); - if (key.canCertify()) { - mCertifyKeySpinner.setSelectedKeyId(certifyKeyId); + ArrayList<Boolean> checkedStates; + if (savedInstanceState != null) { + checkedStates = (ArrayList<Boolean>) savedInstanceState.getSerializable(ARG_CHECK_STATES); + // key spinner and the checkbox keep their own state + } else { + checkedStates = null; + + // preselect certify key id if given + long certifyKeyId = getActivity().getIntent() + .getLongExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, Constants.key.none); + if (certifyKeyId != Constants.key.none) { + try { + CachedPublicKeyRing key = (new ProviderHelper(getActivity())).getCachedPublicKeyRing(certifyKeyId); + if (key.canCertify()) { + mCertifyKeySpinner.setPreSelectedKeyId(certifyKeyId); + } + } catch (PgpKeyNotFoundException e) { + Log.e(Constants.TAG, "certify certify check failed", e); } - } catch (PgpKeyNotFoundException e) { - Log.e(Constants.TAG, "certify certify check failed", e); } + } - mUserIdsAdapter = new MultiUserIdsAdapter(getActivity(), null, 0); + mUserIdsAdapter = new MultiUserIdsAdapter(getActivity(), null, 0, checkedStates); mUserIds.setAdapter(mUserIdsAdapter); mUserIds.setDividerHeight(0); @@ -138,6 +138,15 @@ public class CertifyKeyFragment extends CryptoOperationFragment } @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + ArrayList<Boolean> states = mUserIdsAdapter.getCheckStates(); + // no proper parceling method available :( + outState.putSerializable(ARG_CHECK_STATES, states); + } + + @Override public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.certify_key_fragment, null); @@ -148,26 +157,20 @@ public class CertifyKeyFragment extends CryptoOperationFragment // make certify image gray, like action icons ImageView vActionCertifyImage = (ImageView) view.findViewById(R.id.certify_key_action_certify_image); - vActionCertifyImage.setColorFilter(getResources().getColor(R.color.tertiary_text_light), + vActionCertifyImage.setColorFilter(FormattingUtils.getColorFromAttr(getActivity(), R.attr.colorTertiaryText), PorterDuff.Mode.SRC_IN); - mCertifyKeySpinner.setOnKeyChangedListener(new KeySpinner.OnKeyChangedListener() { - @Override - public void onKeyChanged(long masterKeyId) { - mSignMasterKeyId = masterKeyId; - } - }); - View vCertifyButton = view.findViewById(R.id.certify_key_certify_button); vCertifyButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - if (mSignMasterKeyId == Constants.key.none) { + long selectedKeyId = mCertifyKeySpinner.getSelectedKeyId(); + if (selectedKeyId == Constants.key.none) { Notify.create(getActivity(), getString(R.string.select_key_to_certify), Notify.Style.ERROR).show(); } else { - cryptoOperation(new CryptoInputParcel()); + cryptoOperation(new CryptoInputParcel(new Date())); } } }); @@ -298,82 +301,42 @@ public class CertifyKeyFragment extends CryptoOperationFragment } @Override - protected void cryptoOperation(CryptoInputParcel cryptoInput) { + public CertifyActionsParcel createOperationInput() { + // Bail out if there is not at least one user id selected ArrayList<CertifyAction> certifyActions = mUserIdsAdapter.getSelectedCertifyActions(); if (certifyActions.isEmpty()) { Notify.create(getActivity(), "No identities selected!", Notify.Style.ERROR).show(); - return; - } - - Bundle data = new Bundle(); - { - // fill values for this action - CertifyActionsParcel parcel = new CertifyActionsParcel(mSignMasterKeyId); - parcel.mCertifyActions.addAll(certifyActions); - - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); - data.putParcelable(KeychainIntentService.CERTIFY_PARCEL, parcel); - if (mUploadKeyCheckbox.isChecked()) { - String keyserver = Preferences.getPreferences(getActivity()).getPreferredKeyserver(); - data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, keyserver); - } + return null; } - // Send all information needed to service to sign key in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_CERTIFY_KEYRING); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + long selectedKeyId = mCertifyKeySpinner.getSelectedKeyId(); - if (mPassthroughMessenger != null) { - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, mPassthroughMessenger); - } else { + // fill values for this action + CertifyActionsParcel actionsParcel = new CertifyActionsParcel(selectedKeyId); + actionsParcel.mCertifyActions.addAll(certifyActions); - // Message is received after signing is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_certifying), - ProgressDialog.STYLE_SPINNER, - true, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by KeychainIntentCryptoServiceHandler first - super.handleMessage(message); - - // handle pending messages - if (handlePendingMessage(message)) { - return; - } - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - Bundle data = message.getData(); - - CertifyResult result = data.getParcelable(CertifyResult.EXTRA_RESULT); - - Intent intent = new Intent(); - intent.putExtra(CertifyResult.EXTRA_RESULT, result); - getActivity().setResult(Activity.RESULT_OK, intent); - getActivity().finish(); - } - } - }; - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - saveHandler.showProgressDialog(getActivity()); + if (mUploadKeyCheckbox.isChecked()) { + actionsParcel.keyServerUri = Preferences.getPreferences(getActivity()) + .getPreferredKeyserver(); } - // start service with intent - getActivity().startService(intent); + // cached for next cryptoOperation loop + cacheActionsParcel(actionsParcel); - if (mPassthroughMessenger != null) { - getActivity().setResult(Activity.RESULT_OK); - getActivity().finish(); - } + return actionsParcel; + } + + @Override + public void onQueuedOperationSuccess(CertifyResult result) { + // protected by Queueing*Fragment + Activity activity = getActivity(); + + Intent intent = new Intent(); + intent.putExtra(CertifyResult.EXTRA_RESULT, result); + activity.setResult(Activity.RESULT_OK, intent); + activity.finish(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ConsolidateDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ConsolidateDialogActivity.java index 11ba3e287..ff5fb7cca 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ConsolidateDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ConsolidateDialogActivity.java @@ -17,26 +17,27 @@ package org.sufficientlysecure.keychain.ui; -import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; import android.support.v4.app.FragmentActivity; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; +import org.sufficientlysecure.keychain.service.ConsolidateInputParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; /** * We can not directly create a dialog on the application context. * This activity encapsulates a DialogFragment to emulate a dialog. */ -public class ConsolidateDialogActivity extends FragmentActivity { +public class ConsolidateDialogActivity extends FragmentActivity + implements CryptoOperationHelper.Callback<ConsolidateInputParcel, ConsolidateResult> { public static final String EXTRA_CONSOLIDATE_RECOVERY = "consolidate_recovery"; + private CryptoOperationHelper<ConsolidateInputParcel, ConsolidateResult> mConsolidateOpHelper; + private boolean mRecovery; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -49,55 +50,45 @@ public class ConsolidateDialogActivity extends FragmentActivity { } private void consolidateRecovery(boolean recovery) { - // Message is received after importing is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - this, - getString(R.string.progress_importing), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - /* don't care about the results (for now?) - - // get returned data bundle - Bundle returnData = message.getInputData(); - if (returnData == null) { - return; - } - final ConsolidateResult result = - returnData.getParcelable(KeychainIntentService.RESULT_CONSOLIDATE); - if (result == null) { - return; - } - result.createNotify(ConsolidateDialogActivity.this).show(); - */ - - ConsolidateDialogActivity.this.finish(); - } - } - }; - - // Send all information needed to service to import key in other thread - Intent intent = new Intent(this, KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_CONSOLIDATE); - - // fill values for this action - Bundle data = new Bundle(); - data.putBoolean(KeychainIntentService.CONSOLIDATE_RECOVERY, recovery); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - saveHandler.showProgressDialog(this); - - // start service with intent - startService(intent); + + mRecovery = recovery; + + mConsolidateOpHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_importing); + mConsolidateOpHelper.cryptoOperation(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (mConsolidateOpHelper != null) { + mConsolidateOpHelper.handleActivityResult(requestCode, resultCode, data); + } } + @Override + public ConsolidateInputParcel createOperationInput() { + return new ConsolidateInputParcel(mRecovery); + } + + @Override + public void onCryptoOperationSuccess(ConsolidateResult result) { + // don't care about result (for now?) + ConsolidateDialogActivity.this.finish(); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(ConsolidateResult result) { + // don't care about result (for now?) + ConsolidateDialogActivity.this.finish(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java index e0b728bd4..579a001cb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java @@ -18,6 +18,7 @@ package org.sufficientlysecure.keychain.ui; import android.content.Intent; +import android.nfc.NfcAdapter; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; @@ -29,7 +30,9 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Passphrase; +import org.sufficientlysecure.keychain.util.Preferences; import java.io.IOException; import java.util.ArrayList; @@ -41,6 +44,9 @@ public class CreateKeyActivity extends BaseNfcActivity { public static final String EXTRA_FIRST_TIME = "first_time"; public static final String EXTRA_ADDITIONAL_EMAILS = "additional_emails"; public static final String EXTRA_PASSPHRASE = "passphrase"; + public static final String EXTRA_CREATE_YUBI_KEY = "create_yubi_key"; + public static final String EXTRA_YUBI_KEY_PIN = "yubi_key_pin"; + public static final String EXTRA_YUBI_KEY_ADMIN_PIN = "yubi_key_admin_pin"; public static final String EXTRA_NFC_USER_ID = "nfc_user_id"; public static final String EXTRA_NFC_AID = "nfc_aid"; @@ -53,13 +59,33 @@ public class CreateKeyActivity extends BaseNfcActivity { ArrayList<String> mAdditionalEmails; Passphrase mPassphrase; boolean mFirstTime; + boolean mCreateYubiKey; + Passphrase mYubiKeyPin; + Passphrase mYubiKeyAdminPin; Fragment mCurrentFragment; + + byte[] mScannedFingerprints; + byte[] mNfcAid; + String mNfcUserId; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // React on NDEF_DISCOVERED from Manifest + // NOTE: ACTION_NDEF_DISCOVERED and not ACTION_TAG_DISCOVERED like in BaseNfcActivity + if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) { + + handleIntentInBackground(getIntent()); + + setTitle(R.string.title_manage_my_keys); + + // done + return; + } + // Check whether we're recreating a previously destroyed instance if (savedInstanceState != null) { // Restore value of members from saved state @@ -68,6 +94,9 @@ public class CreateKeyActivity extends BaseNfcActivity { mAdditionalEmails = savedInstanceState.getStringArrayList(EXTRA_ADDITIONAL_EMAILS); mPassphrase = savedInstanceState.getParcelable(EXTRA_PASSPHRASE); mFirstTime = savedInstanceState.getBoolean(EXTRA_FIRST_TIME); + mCreateYubiKey = savedInstanceState.getBoolean(EXTRA_CREATE_YUBI_KEY); + mYubiKeyPin = savedInstanceState.getParcelable(EXTRA_YUBI_KEY_PIN); + mYubiKeyAdminPin = savedInstanceState.getParcelable(EXTRA_YUBI_KEY_ADMIN_PIN); mCurrentFragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); } else { @@ -77,23 +106,36 @@ public class CreateKeyActivity extends BaseNfcActivity { mName = intent.getStringExtra(EXTRA_NAME); mEmail = intent.getStringExtra(EXTRA_EMAIL); mFirstTime = intent.getBooleanExtra(EXTRA_FIRST_TIME, false); + mCreateYubiKey = intent.getBooleanExtra(EXTRA_CREATE_YUBI_KEY, false); if (intent.hasExtra(EXTRA_NFC_FINGERPRINTS)) { byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_NFC_FINGERPRINTS); String nfcUserId = intent.getStringExtra(EXTRA_NFC_USER_ID); byte[] nfcAid = intent.getByteArrayExtra(EXTRA_NFC_AID); - Fragment frag2 = CreateKeyYubiKeyImportFragment.createInstance( - nfcFingerprints, nfcAid, nfcUserId); - loadFragment(frag2, FragAction.START); - - setTitle(R.string.title_import_keys); + if (containsKeys(nfcFingerprints)) { + Fragment frag = CreateYubiKeyImportFragment.newInstance( + nfcFingerprints, nfcAid, nfcUserId); + loadFragment(frag, FragAction.START); + + setTitle(R.string.title_import_keys); + } else { +// Fragment frag = CreateYubiKeyBlankFragment.newInstance(); +// loadFragment(frag, FragAction.START); +// setTitle(R.string.title_manage_my_keys); + Notify.create(this, + "YubiKey key creation is currently not supported. Please follow our FAQ.", + Notify.Style.ERROR + ).show(); + } + + // done return; - } else { - CreateKeyStartFragment frag = CreateKeyStartFragment.newInstance(); - loadFragment(frag, FragAction.START); } + // normal key creation + CreateKeyStartFragment frag = CreateKeyStartFragment.newInstance(); + loadFragment(frag, FragAction.START); } if (mFirstTime) { @@ -106,35 +148,63 @@ public class CreateKeyActivity extends BaseNfcActivity { } @Override - protected void onNfcPerform() throws IOException { + protected void doNfcInBackground() throws IOException { if (mCurrentFragment instanceof NfcListenerFragment) { - ((NfcListenerFragment) mCurrentFragment).onNfcPerform(); + ((NfcListenerFragment) mCurrentFragment).doNfcInBackground(); return; } - byte[] scannedFingerprints = nfcGetFingerprints(); - byte[] nfcAid = nfcGetAid(); - String userId = nfcGetUserId(); - - try { - long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(scannedFingerprints); - CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(masterKeyId); - ring.getMasterKeyId(); + mScannedFingerprints = nfcGetFingerprints(); + mNfcAid = nfcGetAid(); + mNfcUserId = nfcGetUserId(); + } - Intent intent = new Intent(this, ViewKeyActivity.class); - intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, nfcAid); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, userId); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, scannedFingerprints); - startActivity(intent); - finish(); + @Override + protected void onNfcPostExecute() throws IOException { + if (mCurrentFragment instanceof NfcListenerFragment) { + ((NfcListenerFragment) mCurrentFragment).onNfcPostExecute(); + return; + } - } catch (PgpKeyNotFoundException e) { - Fragment frag = CreateKeyYubiKeyImportFragment.createInstance( - scannedFingerprints, nfcAid, userId); - loadFragment(frag, FragAction.TO_RIGHT); + if (containsKeys(mScannedFingerprints)) { + try { + long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mScannedFingerprints); + CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(masterKeyId); + ring.getMasterKeyId(); + + Intent intent = new Intent(this, ViewKeyActivity.class); + intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, mNfcAid); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, mNfcUserId); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, mScannedFingerprints); + startActivity(intent); + finish(); + + } catch (PgpKeyNotFoundException e) { + Fragment frag = CreateYubiKeyImportFragment.newInstance( + mScannedFingerprints, mNfcAid, mNfcUserId); + loadFragment(frag, FragAction.TO_RIGHT); + } + } else { +// Fragment frag = CreateYubiKeyBlankFragment.newInstance(); +// loadFragment(frag, FragAction.TO_RIGHT); + Notify.create(this, + "YubiKey key creation is currently not supported. Please follow our FAQ.", + Notify.Style.ERROR + ).show(); } + } + private boolean containsKeys(byte[] scannedFingerprints) { + // If all fingerprint bytes are 0, the card contains no keys. + boolean cardContainsKeys = false; + for (byte b : scannedFingerprints) { + if (b != 0) { + cardContainsKeys = true; + break; + } + } + return cardContainsKeys; } @Override @@ -146,6 +216,9 @@ public class CreateKeyActivity extends BaseNfcActivity { outState.putStringArrayList(EXTRA_ADDITIONAL_EMAILS, mAdditionalEmails); outState.putParcelable(EXTRA_PASSPHRASE, mPassphrase); outState.putBoolean(EXTRA_FIRST_TIME, mFirstTime); + outState.putBoolean(EXTRA_CREATE_YUBI_KEY, mCreateYubiKey); + outState.putParcelable(EXTRA_YUBI_KEY_PIN, mYubiKeyPin); + outState.putParcelable(EXTRA_YUBI_KEY_ADMIN_PIN, mYubiKeyAdminPin); } @Override @@ -153,7 +226,7 @@ public class CreateKeyActivity extends BaseNfcActivity { setContentView(R.layout.create_key_activity); } - public static enum FragAction { + public enum FragAction { START, TO_RIGHT, TO_LEFT @@ -190,7 +263,19 @@ public class CreateKeyActivity extends BaseNfcActivity { } interface NfcListenerFragment { - public void onNfcPerform() throws IOException; + void doNfcInBackground() throws IOException; + void onNfcPostExecute() throws IOException; } + @Override + public void finish() { + if (mFirstTime) { + Preferences prefs = Preferences.getPreferences(this); + prefs.setFirstTime(false); + Intent intent = new Intent(this, MainActivity.class); + startActivity(intent); + } + + super.finish(); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyEmailFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyEmailFragment.java index 597f04d6b..acb768f55 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyEmailFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyEmailFragment.java @@ -18,6 +18,7 @@ package org.sufficientlysecure.keychain.ui; import android.app.Activity; +import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -29,6 +30,7 @@ import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; @@ -37,7 +39,6 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.dialog.AddEmailDialogFragment; -import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.widget.EmailEditText; @@ -201,7 +202,7 @@ public class CreateKeyEmailFragment extends Fragment { Handler returnHandler = new Handler() { @Override public void handleMessage(Message message) { - if (message.what == SetPassphraseDialogFragment.MESSAGE_OKAY) { + if (message.what == AddEmailDialogFragment.MESSAGE_OKAY) { Bundle data = message.getData(); String email = data.getString(AddEmailDialogFragment.MESSAGE_DATA_EMAIL); @@ -232,11 +233,35 @@ public class CreateKeyEmailFragment extends Fragment { mCreateKeyActivity.mEmail = mEmailEdit.getText().toString(); mCreateKeyActivity.mAdditionalEmails = getAdditionalEmails(); - CreateKeyPassphraseFragment frag = CreateKeyPassphraseFragment.newInstance(); - mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); + CreateKeyActivity createKeyActivity = ((CreateKeyActivity) getActivity()); + + if (createKeyActivity.mCreateYubiKey) { + hideKeyboard(); + + CreateYubiKeyPinFragment frag = CreateYubiKeyPinFragment.newInstance(); + mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); + } else { + CreateKeyPassphraseFragment frag = CreateKeyPassphraseFragment.newInstance(); + mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); + } } } + private void hideKeyboard() { + if (getActivity() == null) { + return; + } + InputMethodManager inputManager = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + + // check if no view has focus + View v = getActivity().getCurrentFocus(); + if (v == null) + return; + + inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + private ArrayList<String> getAdditionalEmails() { ArrayList<String> emails = new ArrayList<>(); for (EmailAdapter.ViewModel holder : mAdditionalEmailModels) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java index b0a13c897..739eb3e35 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2014-2015 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 @@ -17,13 +17,15 @@ package org.sufficientlysecure.keychain.ui; + +import java.util.Date; +import java.util.Iterator; + import android.app.Activity; -import android.app.ProgressDialog; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; @@ -35,27 +37,28 @@ import org.spongycastle.bcpg.sig.KeyFlags; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; +import org.sufficientlysecure.keychain.operations.results.ExportResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.KeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; -import java.util.Iterator; - public class CreateKeyFinalFragment extends Fragment { public static final int REQUEST_EDIT_KEY = 0x00008007; - CreateKeyActivity mCreateKeyActivity; - TextView mNameEdit; TextView mEmailEdit; CheckBox mUploadCheckbox; @@ -66,11 +69,18 @@ public class CreateKeyFinalFragment extends Fragment { SaveKeyringParcel mSaveKeyringParcel; - /** - * Creates new instance of this fragment - */ + private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper; + private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mCreateOpHelper; + private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mMoveToCardOpHelper; + + // queued results which may trigger delayed actions + private EditKeyResult mQueuedSaveKeyResult; + private OperationResult mQueuedFinishResult; + private EditKeyResult mQueuedDisplayResult; + public static CreateKeyFinalFragment newInstance() { CreateKeyFinalFragment frag = new CreateKeyFinalFragment(); + frag.setRetainInstance(true); Bundle args = new Bundle(); frag.setArguments(args); @@ -90,11 +100,13 @@ public class CreateKeyFinalFragment extends Fragment { mEditText = (TextView) view.findViewById(R.id.create_key_edit_text); mEditButton = view.findViewById(R.id.create_key_edit_button); + CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity(); + // set values - mNameEdit.setText(mCreateKeyActivity.mName); - if (mCreateKeyActivity.mAdditionalEmails != null && mCreateKeyActivity.mAdditionalEmails.size() > 0) { - String emailText = mCreateKeyActivity.mEmail + ", "; - Iterator<?> it = mCreateKeyActivity.mAdditionalEmails.iterator(); + mNameEdit.setText(createKeyActivity.mName); + if (createKeyActivity.mAdditionalEmails != null && createKeyActivity.mAdditionalEmails.size() > 0) { + String emailText = createKeyActivity.mEmail + ", "; + Iterator<?> it = createKeyActivity.mAdditionalEmails.iterator(); while (it.hasNext()) { Object next = it.next(); emailText += next; @@ -104,7 +116,7 @@ public class CreateKeyFinalFragment extends Fragment { } mEmailEdit.setText(emailText); } else { - mEmailEdit.setText(mCreateKeyActivity.mEmail); + mEmailEdit.setText(createKeyActivity.mEmail); } mCreateButton.setOnClickListener(new View.OnClickListener() { @@ -117,7 +129,10 @@ public class CreateKeyFinalFragment extends Fragment { mBackButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); + CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity(); + if (createKeyActivity != null) { + createKeyActivity.loadFragment(null, FragAction.TO_LEFT); + } } }); @@ -130,17 +145,26 @@ public class CreateKeyFinalFragment extends Fragment { } }); - return view; - } + // If this is a debug build, don't upload by default + if (Constants.DEBUG) { + mUploadCheckbox.setChecked(false); + } - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - mCreateKeyActivity = (CreateKeyActivity) getActivity(); + return view; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mCreateOpHelper != null) { + mCreateOpHelper.handleActivityResult(requestCode, resultCode, data); + } + if (mMoveToCardOpHelper != null) { + mMoveToCardOpHelper.handleActivityResult(requestCode, resultCode, data); + } + if (mUploadOpHelper != null) { + mUploadOpHelper.handleActivityResult(requestCode, resultCode, data); + } + switch (requestCode) { case REQUEST_EDIT_KEY: { if (resultCode == Activity.RESULT_OK) { @@ -159,147 +183,283 @@ public class CreateKeyFinalFragment extends Fragment { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mCreateKeyActivity = (CreateKeyActivity) getActivity(); + CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity(); if (mSaveKeyringParcel == null) { mSaveKeyringParcel = new SaveKeyringParcel(); - mSaveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( - Algorithm.RSA, 4096, null, KeyFlags.CERTIFY_OTHER, 0L)); - mSaveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( - Algorithm.RSA, 4096, null, KeyFlags.SIGN_DATA, 0L)); - mSaveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd( - Algorithm.RSA, 4096, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L)); + + if (createKeyActivity.mCreateYubiKey) { + mSaveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA, + 2048, null, KeyFlags.SIGN_DATA | KeyFlags.CERTIFY_OTHER, 0L)); + mSaveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA, + 2048, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L)); + mSaveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA, + 2048, null, KeyFlags.AUTHENTICATION, 0L)); + mEditText.setText(R.string.create_key_custom); + mEditButton.setEnabled(false); + + // use empty passphrase + mSaveKeyringParcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase(), null); + } else { + mSaveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA, + 4096, null, KeyFlags.CERTIFY_OTHER, 0L)); + mSaveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA, + 4096, null, KeyFlags.SIGN_DATA, 0L)); + mSaveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA, + 4096, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L)); + + mSaveKeyringParcel.mNewUnlock = createKeyActivity.mPassphrase != null + ? new ChangeUnlockParcel(createKeyActivity.mPassphrase, null) + : null; + } String userId = KeyRing.createUserId( - new KeyRing.UserId(mCreateKeyActivity.mName, mCreateKeyActivity.mEmail, null) + new KeyRing.UserId(createKeyActivity.mName, createKeyActivity.mEmail, null) ); mSaveKeyringParcel.mAddUserIds.add(userId); mSaveKeyringParcel.mChangePrimaryUserId = userId; - if (mCreateKeyActivity.mAdditionalEmails != null - && mCreateKeyActivity.mAdditionalEmails.size() > 0) { - for (String email : mCreateKeyActivity.mAdditionalEmails) { + if (createKeyActivity.mAdditionalEmails != null + && createKeyActivity.mAdditionalEmails.size() > 0) { + for (String email : createKeyActivity.mAdditionalEmails) { String thisUserId = KeyRing.createUserId( - new KeyRing.UserId(mCreateKeyActivity.mName, email, null) + new KeyRing.UserId(createKeyActivity.mName, email, null) ); mSaveKeyringParcel.mAddUserIds.add(thisUserId); } } - mSaveKeyringParcel.mNewUnlock = mCreateKeyActivity.mPassphrase != null - ? new ChangeUnlockParcel(mCreateKeyActivity.mPassphrase, null) - : null; } - } + // handle queued actions + + if (mQueuedFinishResult != null) { + finishWithResult(mQueuedFinishResult); + return; + } + + if (mQueuedDisplayResult != null) { + try { + displayResult(mQueuedDisplayResult); + } finally { + // clear after operation, note that this may drop the operation if it didn't + // work when called from here! + mQueuedDisplayResult = null; + } + } + + if (mQueuedSaveKeyResult != null) { + try { + uploadKey(mQueuedSaveKeyResult); + } finally { + // see above + mQueuedSaveKeyResult = null; + } + } + + } private void createKey() { - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_EDIT_KEYRING); - - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_building_key), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - if (returnData == null) { - return; - } - final EditKeyResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null) { - Log.e(Constants.TAG, "result == null"); - return; - } - - if (result.mMasterKeyId != null && mUploadCheckbox.isChecked()) { - // result will be displayed after upload - uploadKey(result); - } else { - Intent data = new Intent(); - data.putExtra(OperationResult.EXTRA_RESULT, result); - getActivity().setResult(Activity.RESULT_OK, data); - getActivity().finish(); - } + CreateKeyActivity activity = (CreateKeyActivity) getActivity(); + if (activity == null) { + // this is a ui-triggered action, nvm if it fails while detached! + return; + } + + final boolean createYubiKey = activity.mCreateYubiKey; + + CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> createKeyCallback + = new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() { + @Override + public SaveKeyringParcel createOperationInput() { + return mSaveKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(EditKeyResult result) { + + if (createYubiKey) { + moveToCard(result); + return; } + + if (result.mMasterKeyId != null && mUploadCheckbox.isChecked()) { + // result will be displayed after upload + uploadKey(result); + return; + } + + finishWithResult(result); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(EditKeyResult result) { + displayResult(result); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; } }; - // fill values for this action - Bundle data = new Bundle(); + mCreateOpHelper = new CryptoOperationHelper<>(1, this, createKeyCallback, R.string.progress_building_key); + mCreateOpHelper.cryptoOperation(); + } - // get selected key entries - data.putParcelable(KeychainIntentService.EDIT_KEYRING_PARCEL, mSaveKeyringParcel); + private void displayResult(EditKeyResult result) { + Activity activity = getActivity(); + if (activity == null) { + mQueuedDisplayResult = result; + return; + } + result.createNotify(activity).show(); + } + + private void moveToCard(final EditKeyResult saveKeyResult) { + CreateKeyActivity activity = (CreateKeyActivity) getActivity(); + + final SaveKeyringParcel changeKeyringParcel; + CachedPublicKeyRing key = (new ProviderHelper(activity)) + .getCachedPublicKeyRing(saveKeyResult.mMasterKeyId); + try { + changeKeyringParcel = new SaveKeyringParcel(key.getMasterKeyId(), key.getFingerprint()); + } catch (PgpKeyNotFoundException e) { + Log.e(Constants.TAG, "Key that should be moved to YubiKey not found in database!"); + return; + } + + // define subkeys that should be moved to the card + Cursor cursor = activity.getContentResolver().query( + KeychainContract.Keys.buildKeysUri(changeKeyringParcel.mMasterKeyId), + new String[] { KeychainContract.Keys.KEY_ID, }, null, null, null + ); + try { + while (cursor != null && cursor.moveToNext()) { + long subkeyId = cursor.getLong(0); + changeKeyringParcel.getOrCreateSubkeyChange(subkeyId).mMoveKeyToCard = true; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + // define new PIN and Admin PIN for the card + changeKeyringParcel.mCardPin = activity.mYubiKeyPin; + changeKeyringParcel.mCardAdminPin = activity.mYubiKeyAdminPin; - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> callback + = new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() { - saveHandler.showProgressDialog(getActivity()); + @Override + public SaveKeyringParcel createOperationInput() { + return changeKeyringParcel; + } - getActivity().startService(intent); + @Override + public void onCryptoOperationSuccess(EditKeyResult result) { + handleResult(result); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(EditKeyResult result) { + handleResult(result); + } + + public void handleResult(EditKeyResult result) { + // merge logs of createKey with moveToCard + saveKeyResult.getLog().add(result, 0); + + if (result.mMasterKeyId != null && mUploadCheckbox.isChecked()) { + // result will be displayed after upload + uploadKey(saveKeyResult); + return; + } + + finishWithResult(saveKeyResult); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + + + mMoveToCardOpHelper = new CryptoOperationHelper<>(2, this, callback, R.string.progress_modify); + mMoveToCardOpHelper.cryptoOperation(new CryptoInputParcel(new Date())); } - // TODO move into EditKeyOperation private void uploadKey(final EditKeyResult saveKeyResult) { - // Send all information needed to service to upload key in other thread - final Intent intent = new Intent(getActivity(), KeychainIntentService.class); - - intent.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING); + Activity activity = getActivity(); + // if the activity is gone at this point, there is nothing we can do! + if (activity == null) { + mQueuedSaveKeyResult = saveKeyResult; + return; + } // set data uri as path to keyring - Uri blobUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri( - saveKeyResult.mMasterKeyId); - intent.setData(blobUri); + final Uri blobUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(saveKeyResult.mMasterKeyId); + // upload to favorite keyserver + final String keyserver = Preferences.getPreferences(activity).getPreferredKeyserver(); - // fill values for this action - Bundle data = new Bundle(); + CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> callback + = new CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult>() { + + @Override + public ExportKeyringParcel createOperationInput() { + return new ExportKeyringParcel(keyserver, blobUri); + } + + @Override + public void onCryptoOperationSuccess(ExportResult result) { + handleResult(result); + } + + @Override + public void onCryptoOperationCancelled() { - // upload to favorite keyserver - String keyserver = Preferences.getPreferences(getActivity()).getPreferredKeyserver(); - data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, keyserver); - - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_uploading), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // TODO: upload operation needs a result! - // TODO: then combine these results - //if (result.getResult() == OperationResultParcel.RESULT_OK) { - //Notify.create(getActivity(), R.string.key_send_success, - //Notify.Style.OK).show(); - - Intent data = new Intent(); - data.putExtra(OperationResult.EXTRA_RESULT, saveKeyResult); - getActivity().setResult(Activity.RESULT_OK, data); - getActivity().finish(); - } + } + + @Override + public void onCryptoOperationError(ExportResult result) { + handleResult(result); + } + + public void handleResult(ExportResult result) { + saveKeyResult.getLog().add(result, 0); + finishWithResult(saveKeyResult); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; } }; - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + mUploadOpHelper = new CryptoOperationHelper<>(3, this, callback, R.string.progress_uploading); + mUploadOpHelper.cryptoOperation(); + } - // show progress dialog - saveHandler.showProgressDialog(getActivity()); + public void finishWithResult(OperationResult result) { + Activity activity = getActivity(); + if (activity == null) { + mQueuedFinishResult = result; + return; + } - // start service with intent - getActivity().startService(intent); + Intent data = new Intent(); + data.putExtra(OperationResult.EXTRA_RESULT, result); + activity.setResult(Activity.RESULT_OK, data); + activity.finish(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyPassphraseFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyPassphraseFragment.java index 3379e0a6d..d858fd6ec 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyPassphraseFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyPassphraseFragment.java @@ -21,6 +21,8 @@ import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; import android.text.method.HideReturnsTransformationMethod; import android.text.method.PasswordTransformationMethod; import android.view.LayoutInflater; @@ -79,19 +81,10 @@ public class CreateKeyPassphraseFragment extends Fragment { return output; } - private static boolean areEditTextsEqual(Context context, EditText editText1, EditText editText2) { + private static boolean areEditTextsEqual(EditText editText1, EditText editText2) { Passphrase p1 = new Passphrase(editText1); Passphrase p2 = new Passphrase(editText2); - boolean output = (p1.equals(p2)); - - if (!output) { - editText2.setError(context.getString(R.string.create_key_passphrases_not_equal)); - editText2.requestFocus(); - } else { - editText2.setError(null); - } - - return output; + return (p1.equals(p2)); } @Override @@ -107,8 +100,8 @@ public class CreateKeyPassphraseFragment extends Fragment { // initial values // TODO: using String here is unsafe... if (mCreateKeyActivity.mPassphrase != null) { - mPassphraseEdit.setText(new String(mCreateKeyActivity.mPassphrase.getCharArray())); - mPassphraseEditAgain.setText(new String(mCreateKeyActivity.mPassphrase.getCharArray())); + mPassphraseEdit.setText(mCreateKeyActivity.mPassphrase.toStringUnsafe()); + mPassphraseEditAgain.setText(mCreateKeyActivity.mPassphrase.toStringUnsafe()); } mPassphraseEdit.requestFocus(); @@ -137,6 +130,35 @@ public class CreateKeyPassphraseFragment extends Fragment { } }); + TextWatcher textWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (!isEditTextNotEmpty(getActivity(), mPassphraseEdit)) { + mPassphraseEditAgain.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + return; + } + + if (areEditTextsEqual(mPassphraseEdit, mPassphraseEditAgain)) { + mPassphraseEditAgain.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_stat_retyped_ok, 0); + } else { + mPassphraseEditAgain.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_stat_retyped_bad, 0); + } + + } + + @Override + public void afterTextChanged(Editable s) { + + } + }; + + mPassphraseEdit.addTextChangedListener(textWatcher); + mPassphraseEditAgain.addTextChangedListener(textWatcher); return view; } @@ -153,9 +175,15 @@ public class CreateKeyPassphraseFragment extends Fragment { } private void nextClicked() { - if (isEditTextNotEmpty(getActivity(), mPassphraseEdit) - && areEditTextsEqual(getActivity(), mPassphraseEdit, mPassphraseEditAgain)) { + if (isEditTextNotEmpty(getActivity(), mPassphraseEdit)) { + + if (!areEditTextsEqual(mPassphraseEdit, mPassphraseEditAgain)) { + mPassphraseEditAgain.setError(getActivity().getApplicationContext().getString(R.string.create_key_passphrases_not_equal)); + mPassphraseEditAgain.requestFocus(); + return; + } + mPassphraseEditAgain.setError(null); // save state mCreateKeyActivity.mPassphrase = new Passphrase(mPassphraseEdit); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java index 1a844e6e4..68ec0e8c8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyStartFragment.java @@ -81,7 +81,7 @@ public class CreateKeyStartFragment extends Fragment { mYubiKey.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - CreateKeyYubiKeyWaitFragment frag = new CreateKeyYubiKeyWaitFragment(); + CreateYubiKeyWaitFragment frag = new CreateYubiKeyWaitFragment(); mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); } }); @@ -98,17 +98,10 @@ public class CreateKeyStartFragment extends Fragment { mSkipOrCancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (mCreateKeyActivity.mFirstTime) { - Preferences prefs = Preferences.getPreferences(mCreateKeyActivity); - prefs.setFirstTime(false); - Intent intent = new Intent(mCreateKeyActivity, MainActivity.class); - startActivity(intent); - mCreateKeyActivity.finish(); - } else { - // just finish activity and return data + if (!mCreateKeyActivity.mFirstTime) { mCreateKeyActivity.setResult(Activity.RESULT_CANCELED); - mCreateKeyActivity.finish(); } + mCreateKeyActivity.finish(); } }); @@ -124,9 +117,6 @@ public class CreateKeyStartFragment extends Fragment { if (mCreateKeyActivity.mFirstTime) { Preferences prefs = Preferences.getPreferences(mCreateKeyActivity); prefs.setFirstTime(false); - Intent intent = new Intent(mCreateKeyActivity, MainActivity.class); - intent.putExtras(data); - startActivity(intent); mCreateKeyActivity.finish(); } else { // just finish activity and return data diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyBlankFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyBlankFragment.java new file mode 100644 index 000000000..5b13dc88e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyBlankFragment.java @@ -0,0 +1,90 @@ +/* + * 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.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; + +public class CreateYubiKeyBlankFragment extends Fragment { + + CreateKeyActivity mCreateKeyActivity; + View mBackButton; + View mNextButton; + + /** + * Creates new instance of this fragment + */ + public static CreateYubiKeyBlankFragment newInstance() { + CreateYubiKeyBlankFragment frag = new CreateYubiKeyBlankFragment(); + + Bundle args = new Bundle(); + + frag.setArguments(args); + + return frag; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.create_yubi_key_blank_fragment, container, false); + + mBackButton = view.findViewById(R.id.create_key_back_button); + mNextButton = view.findViewById(R.id.create_key_next_button); + + mBackButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getFragmentManager().getBackStackEntryCount() == 0) { + getActivity().setResult(Activity.RESULT_CANCELED); + getActivity().finish(); + } else { + mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); + } + } + }); + mNextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + nextClicked(); + } + }); + + return view; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mCreateKeyActivity = (CreateKeyActivity) getActivity(); + } + + private void nextClicked() { + mCreateKeyActivity.mCreateYubiKey = true; + + CreateKeyNameFragment frag = CreateKeyNameFragment.newInstance(); + mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyYubiKeyImportFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyImportFragment.java index f8d79d33b..d88e6b9f9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyYubiKeyImportFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyImportFragment.java @@ -17,15 +17,14 @@ package org.sufficientlysecure.keychain.ui; + import java.io.IOException; +import java.nio.ByteBuffer; import java.util.ArrayList; import android.app.Activity; -import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; @@ -36,19 +35,19 @@ import android.widget.TextView; import org.spongycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.NfcListenerFragment; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Preferences; -public class CreateKeyYubiKeyImportFragment extends Fragment implements NfcListenerFragment { +public class CreateYubiKeyImportFragment + extends QueueingCryptoOperationFragment<ImportKeyringParcel, ImportKeyResult> + implements NfcListenerFragment { private static final String ARG_FINGERPRINT = "fingerprint"; public static final String ARG_AID = "aid"; @@ -57,7 +56,6 @@ public class CreateKeyYubiKeyImportFragment extends Fragment implements NfcListe CreateKeyActivity mCreateKeyActivity; private byte[] mNfcFingerprints; - private long mNfcMasterKeyId; private byte[] mNfcAid; private String mNfcUserId; private String mNfcFingerprint; @@ -65,9 +63,13 @@ public class CreateKeyYubiKeyImportFragment extends Fragment implements NfcListe private TextView vSerNo; private TextView vUserId; - public static Fragment createInstance(byte[] scannedFingerprints, byte[] nfcAid, String userId) { + // for CryptoOperationFragment key import + private String mKeyserver; + private ArrayList<ParcelableKeyRing> mKeyList; + + public static Fragment newInstance(byte[] scannedFingerprints, byte[] nfcAid, String userId) { - CreateKeyYubiKeyImportFragment frag = new CreateKeyYubiKeyImportFragment(); + CreateYubiKeyImportFragment frag = new CreateYubiKeyImportFragment(); Bundle args = new Bundle(); args.putByteArray(ARG_FINGERPRINT, scannedFingerprints); @@ -88,14 +90,15 @@ public class CreateKeyYubiKeyImportFragment extends Fragment implements NfcListe mNfcAid = args.getByteArray(ARG_AID); mNfcUserId = args.getString(ARG_USER_ID); - mNfcMasterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints); - mNfcFingerprint = KeyFormattingUtils.convertFingerprintToHex(mNfcFingerprints); + byte[] fp = new byte[20]; + ByteBuffer.wrap(fp).put(mNfcFingerprints, 0, 20); + mNfcFingerprint = KeyFormattingUtils.convertFingerprintToHex(fp); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.create_yubikey_import_fragment, container, false); + View view = inflater.inflate(R.layout.create_yubi_key_import_fragment, container, false); vSerNo = (TextView) view.findViewById(R.id.yubikey_serno); vUserId = (TextView) view.findViewById(R.id.yubikey_userid); @@ -164,7 +167,7 @@ public class CreateKeyYubiKeyImportFragment extends Fragment implements NfcListe if (!mNfcUserId.isEmpty()) { vUserId.setText(getString(R.string.yubikey_key_holder, mNfcUserId)); } else { - vUserId.setText(getString(R.string.yubikey_key_holder_unset)); + vUserId.setText(getString(R.string.yubikey_key_holder_not_set)); } } @@ -175,94 +178,68 @@ public class CreateKeyYubiKeyImportFragment extends Fragment implements NfcListe public void importKey() { - // Message is received after decrypting is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_importing), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT - ) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - - ImportKeyResult result = - returnData.getParcelable(DecryptVerifyResult.EXTRA_RESULT); - - long[] masterKeyIds = result.getImportedMasterKeyIds(); - - // TODO handle masterKeyIds.length != 1...? sorta outlandish scenario - - if (!result.success() || masterKeyIds.length == 0) { - result.createNotify(getActivity()).show(); - return; - } - - Intent intent = new Intent(getActivity(), ViewKeyActivity.class); - // use the imported masterKeyId, not the one from the yubikey, because - // that one might* just have been a subkey of the imported key - intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyIds[0])); - intent.putExtra(ViewKeyActivity.EXTRA_DISPLAY_RESULT, result); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, mNfcAid); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, mNfcUserId); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, mNfcFingerprints); - startActivity(intent); - getActivity().finish(); - - } - - } - }; - - // Send all information needed to service to decrypt in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - - // fill values for this action - Bundle data = new Bundle(); - - intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); - - String hexFp = KeyFormattingUtils.convertFingerprintToHex(mNfcFingerprints); ArrayList<ParcelableKeyRing> keyList = new ArrayList<>(); - keyList.add(new ParcelableKeyRing(hexFp, null, null)); - data.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, keyList); + keyList.add(new ParcelableKeyRing(mNfcFingerprint, null, null)); + mKeyList = keyList; { Preferences prefs = Preferences.getPreferences(getActivity()); Preferences.CloudSearchPrefs cloudPrefs = new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver()); - data.putString(KeychainIntentService.IMPORT_KEY_SERVER, cloudPrefs.keyserver); + mKeyserver = cloudPrefs.keyserver; } - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - saveHandler.showProgressDialog(getActivity()); + super.setProgressMessageResource(R.string.progress_importing); - // start service with intent - getActivity().startService(intent); + super.cryptoOperation(); } @Override - public void onNfcPerform() throws IOException { + public void doNfcInBackground() throws IOException { mNfcFingerprints = mCreateKeyActivity.nfcGetFingerprints(); mNfcAid = mCreateKeyActivity.nfcGetAid(); mNfcUserId = mCreateKeyActivity.nfcGetUserId(); - mNfcMasterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints); - mNfcFingerprint = KeyFormattingUtils.convertFingerprintToHex(mNfcFingerprints); + byte[] fp = new byte[20]; + ByteBuffer.wrap(fp).put(mNfcFingerprints, 0, 20); + mNfcFingerprint = KeyFormattingUtils.convertFingerprintToHex(fp); + } + + @Override + public void onNfcPostExecute() throws IOException { setData(); + refreshSearch(); + } + + @Override + public ImportKeyringParcel createOperationInput() { + return new ImportKeyringParcel(mKeyList, mKeyserver); + } + + @Override + public void onQueuedOperationSuccess(ImportKeyResult result) { + long[] masterKeyIds = result.getImportedMasterKeyIds(); + if (masterKeyIds.length == 0) { + super.onCryptoOperationError(result); + return; + } + // null-protected from Queueing*Fragment + Activity activity = getActivity(); + + Intent intent = new Intent(activity, ViewKeyActivity.class); + // use the imported masterKeyId, not the one from the yubikey, because + // that one might* just have been a subkey of the imported key + intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyIds[0])); + intent.putExtra(ViewKeyActivity.EXTRA_DISPLAY_RESULT, result); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, mNfcAid); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, mNfcUserId); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, mNfcFingerprints); + startActivity(intent); + activity.finish(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinFragment.java new file mode 100644 index 000000000..a793b31f2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinFragment.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2015 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.ui; + +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; +import org.sufficientlysecure.keychain.util.Passphrase; + +import java.security.SecureRandom; + +public class CreateYubiKeyPinFragment extends Fragment { + + // view + CreateKeyActivity mCreateKeyActivity; + TextView mPin; + TextView mAdminPin; + View mBackButton; + View mNextButton; + + /** + * Creates new instance of this fragment + */ + public static CreateYubiKeyPinFragment newInstance() { + CreateYubiKeyPinFragment frag = new CreateYubiKeyPinFragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.create_yubi_key_pin_fragment, container, false); + + mPin = (TextView) view.findViewById(R.id.create_yubi_key_pin); + mAdminPin = (TextView) view.findViewById(R.id.create_yubi_key_admin_pin); + mBackButton = view.findViewById(R.id.create_key_back_button); + mNextButton = view.findViewById(R.id.create_key_next_button); + + if (mCreateKeyActivity.mYubiKeyPin == null) { + new AsyncTask<Void, Void, Pair<Passphrase, Passphrase>>() { + @Override + protected Pair<Passphrase, Passphrase> doInBackground(Void... unused) { + SecureRandom secureRandom = new SecureRandom(); + // min = 6, we choose 6 + String pin = "" + secureRandom.nextInt(9) + + secureRandom.nextInt(9) + + secureRandom.nextInt(9) + + secureRandom.nextInt(9) + + secureRandom.nextInt(9) + + secureRandom.nextInt(9); + // min = 8, we choose 10, but 6 are equals the PIN + String adminPin = pin + secureRandom.nextInt(9) + + secureRandom.nextInt(9) + + secureRandom.nextInt(9) + + secureRandom.nextInt(9); + + return new Pair<>(new Passphrase(pin), new Passphrase(adminPin)); + } + + @Override + protected void onPostExecute(Pair<Passphrase, Passphrase> pair) { + mCreateKeyActivity.mYubiKeyPin = pair.first; + mCreateKeyActivity.mYubiKeyAdminPin = pair.second; + + mPin.setText(mCreateKeyActivity.mYubiKeyPin.toStringUnsafe()); + mAdminPin.setText(mCreateKeyActivity.mYubiKeyAdminPin.toStringUnsafe()); + } + }.execute(); + } else { + mPin.setText(mCreateKeyActivity.mYubiKeyPin.toStringUnsafe()); + mAdminPin.setText(mCreateKeyActivity.mYubiKeyAdminPin.toStringUnsafe()); + } + + mBackButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + back(); + } + }); + mNextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + nextClicked(); + } + }); + + + return view; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mCreateKeyActivity = (CreateKeyActivity) getActivity(); + } + + + private void nextClicked() { + CreateYubiKeyPinRepeatFragment frag = CreateYubiKeyPinRepeatFragment.newInstance(); + mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); + } + + private void back() { + mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinRepeatFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinRepeatFragment.java new file mode 100644 index 000000000..2e752e609 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinRepeatFragment.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2015 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.ui; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; + +public class CreateYubiKeyPinRepeatFragment extends Fragment { + + // view + CreateKeyActivity mCreateKeyActivity; + EditText mPin; + EditText mAdminPin; + View mBackButton; + View mNextButton; + + /** + * Creates new instance of this fragment + */ + public static CreateYubiKeyPinRepeatFragment newInstance() { + CreateYubiKeyPinRepeatFragment frag = new CreateYubiKeyPinRepeatFragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + /** + * Checks if text of given EditText is not empty. If it is empty an error is + * set and the EditText gets the focus. + * + * @param context + * @param editText + * @return true if EditText is not empty + */ + private static boolean isEditTextNotEmpty(Context context, EditText editText) { + boolean output = true; + if (editText.getText().length() == 0) { + editText.setError(context.getString(R.string.create_key_empty)); + editText.requestFocus(); + output = false; + } else { + editText.setError(null); + } + + return output; + } + + private static boolean checkPin(Context context, EditText editText1, String pin) { + boolean output = editText1.getText().toString().equals(pin); + + if (!output) { + editText1.setError(context.getString(R.string.create_key_yubi_key_pin_not_correct)); + editText1.requestFocus(); + } else { + editText1.setError(null); + } + + return output; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.create_yubi_key_pin_repeat_fragment, container, false); + + mPin = (EditText) view.findViewById(R.id.create_yubi_key_pin_repeat); + mAdminPin = (EditText) view.findViewById(R.id.create_yubi_key_admin_pin_repeat); + mBackButton = view.findViewById(R.id.create_key_back_button); + mNextButton = view.findViewById(R.id.create_key_next_button); + + mPin.requestFocus(); + mBackButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + back(); + } + }); + mNextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + nextClicked(); + } + }); + + return view; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mCreateKeyActivity = (CreateKeyActivity) getActivity(); + } + + private void back() { + hideKeyboard(); + mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); + } + + private void nextClicked() { + if (isEditTextNotEmpty(getActivity(), mPin) + && checkPin(getActivity(), mPin, mCreateKeyActivity.mYubiKeyPin.toStringUnsafe()) + && isEditTextNotEmpty(getActivity(), mAdminPin) + && checkPin(getActivity(), mAdminPin, mCreateKeyActivity.mYubiKeyAdminPin.toStringUnsafe())) { + + CreateKeyFinalFragment frag = CreateKeyFinalFragment.newInstance(); + hideKeyboard(); + mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); + } + } + + private void hideKeyboard() { + if (getActivity() == null) { + return; + } + InputMethodManager inputManager = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + + // check if no view has focus + View v = getActivity().getCurrentFocus(); + if (v == null) + return; + + inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyYubiKeyWaitFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyWaitFragment.java index 0b8586c0a..d45195512 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyYubiKeyWaitFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyWaitFragment.java @@ -28,14 +28,14 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; -public class CreateKeyYubiKeyWaitFragment extends Fragment { +public class CreateYubiKeyWaitFragment extends Fragment { CreateKeyActivity mCreateKeyActivity; View mBackButton; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.create_yubikey_wait_fragment, container, false); + View view = inflater.inflate(R.layout.create_yubi_key_wait_fragment, container, false); mBackButton = view.findViewById(R.id.create_key_back_button); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java new file mode 100644 index 000000000..881190ae2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java @@ -0,0 +1,214 @@ +/* + * 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.ui; + + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; +import org.sufficientlysecure.keychain.pgp.PgpHelper; +import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; + + +public class DecryptActivity extends BaseActivity { + + /* Intents */ + public static final String ACTION_DECRYPT_FROM_CLIPBOARD = "DECRYPT_DATA_CLIPBOARD"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setFullScreenDialogClose(Activity.RESULT_CANCELED, false); + + // Handle intent actions + handleActions(savedInstanceState, getIntent()); + } + + @Override + protected void initLayout() { + setContentView(R.layout.decrypt_files_activity); + } + + /** + * Handles all actions with this intent + */ + private void handleActions(Bundle savedInstanceState, Intent intent) { + + // No need to initialize fragments if we are just being restored + if (savedInstanceState != null) { + return; + } + + ArrayList<Uri> uris = new ArrayList<>(); + + String action = intent.getAction(); + + if (action == null) { + Toast.makeText(this, "Error: No action specified!", Toast.LENGTH_LONG).show(); + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } + + try { + + switch (action) { + case Intent.ACTION_SEND: { + // When sending to Keychain Decrypt via share menu + // Binary via content provider (could also be files) + // override uri to get stream from send + if (intent.hasExtra(Intent.EXTRA_STREAM)) { + uris.add(intent.<Uri>getParcelableExtra(Intent.EXTRA_STREAM)); + } else if (intent.hasExtra(Intent.EXTRA_TEXT)) { + String text = intent.getStringExtra(Intent.EXTRA_TEXT); + Uri uri = readToTempFile(text); + if (uri != null) { + uris.add(uri); + } + } + + break; + } + + case Intent.ACTION_SEND_MULTIPLE: { + if (intent.hasExtra(Intent.EXTRA_STREAM)) { + uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + } else if (intent.hasExtra(Intent.EXTRA_TEXT)) { + for (String text : intent.getStringArrayListExtra(Intent.EXTRA_TEXT)) { + Uri uri = readToTempFile(text); + if (uri != null) { + uris.add(uri); + } + } + } + + break; + } + + case ACTION_DECRYPT_FROM_CLIPBOARD: { + ClipboardManager clipMan = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + if (clipMan == null) { + break; + } + + ClipData clip = clipMan.getPrimaryClip(); + if (clip == null) { + break; + } + + // check if data is available as uri + Uri uri = null; + for (int i = 0; i < clip.getItemCount(); i++) { + ClipData.Item item = clip.getItemAt(i); + Uri itemUri = item.getUri(); + if (itemUri != null) { + uri = itemUri; + break; + } + } + + // otherwise, coerce to text (almost always possible) and work from there + if (uri == null) { + String text = clip.getItemAt(0).coerceToText(this).toString(); + uri = readToTempFile(text); + } + if (uri != null) { + uris.add(uri); + } + + break; + } + + // for everything else, just work on the intent data + case OpenKeychainIntents.DECRYPT_DATA: + case Intent.ACTION_VIEW: + default: + uris.add(intent.getData()); + + } + + } catch (IOException e) { + Toast.makeText(this, R.string.error_reading_text, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + // Definitely need a data uri with the decrypt_data intent + if (uris.isEmpty()) { + Toast.makeText(this, "No data to decrypt!", Toast.LENGTH_LONG).show(); + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } + + displayListFragment(uris); + + } + + @Nullable public Uri readToTempFile(String text) throws IOException { + Uri tempFile = TemporaryStorageProvider.createFile(this); + OutputStream outStream = getContentResolver().openOutputStream(tempFile); + + // clean up ascii armored message, fixing newlines and stuff + String cleanedText = PgpHelper.getPgpContent(text); + if (cleanedText == null) { + return null; + } + + // if cleanup didn't work, just try the raw data + outStream.write(text.getBytes()); + outStream.close(); + return tempFile; + } + + public void displayListFragment(ArrayList<Uri> inputUris) { + + DecryptListFragment frag = DecryptListFragment.newInstance(inputUris); + + FragmentManager fragMan = getSupportFragmentManager(); + + FragmentTransaction trans = fragMan.beginTransaction(); + trans.replace(R.id.decrypt_files_fragment_container, frag); + + // if there already is a fragment, allow going back to that. otherwise, we're top level! + if (fragMan.getFragments() != null && !fragMan.getFragments().isEmpty()) { + trans.addToBackStack("list"); + } + + trans.commit(); + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java deleted file mode 100644 index c9a590c5b..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesActivity.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.ui; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; -import org.sufficientlysecure.keychain.ui.base.BaseActivity; -import org.sufficientlysecure.keychain.util.Log; - -public class DecryptFilesActivity extends BaseActivity { - - /* Intents */ - public static final String ACTION_DECRYPT_DATA = OpenKeychainIntents.DECRYPT_DATA; - - // intern - public static final String ACTION_DECRYPT_DATA_OPEN = Constants.INTENT_PREFIX + "DECRYPT_DATA_OPEN"; - - DecryptFilesFragment mFragment; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setFullScreenDialogClose(new View.OnClickListener() { - @Override - public void onClick(View v) { - setResult(Activity.RESULT_CANCELED); - finish(); - } - }, false); - - // Handle intent actions - handleActions(savedInstanceState, getIntent()); - } - - @Override - protected void initLayout() { - setContentView(R.layout.decrypt_files_activity); - } - - /** - * Handles all actions with this intent - * - * @param intent - */ - private void handleActions(Bundle savedInstanceState, Intent intent) { - String action = intent.getAction(); - String type = intent.getType(); - Uri uri = intent.getData(); - - Bundle mFileFragmentBundle = new Bundle(); - - /* - * Android's Action - */ - if (Intent.ACTION_SEND.equals(action) && type != null) { - // When sending to Keychain Decrypt via share menu - // Binary via content provider (could also be files) - // override uri to get stream from send - uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - action = ACTION_DECRYPT_DATA; - } else if (Intent.ACTION_VIEW.equals(action)) { - // Android's Action when opening file associated to Keychain (see AndroidManifest.xml) - - // override action - action = ACTION_DECRYPT_DATA; - } - - /** - * Main Actions - */ - if (ACTION_DECRYPT_DATA.equals(action) && uri != null) { - mFileFragmentBundle.putParcelable(DecryptFilesFragment.ARG_URI, uri); - - loadFragment(savedInstanceState, uri, false); - } else if (ACTION_DECRYPT_DATA_OPEN.equals(action)) { - loadFragment(savedInstanceState, null, true); - } else if (ACTION_DECRYPT_DATA.equals(action)) { - Log.e(Constants.TAG, - "Include an Uri with setInputData() in your Intent!"); - } - } - - private void loadFragment(Bundle savedInstanceState, Uri uri, boolean openDialog) { - // However, if we're being restored from a previous state, - // then we don't need to do anything and should return or else - // we could end up with overlapping fragments. - if (savedInstanceState != null) { - return; - } - - // Create an instance of the fragment - mFragment = DecryptFilesFragment.newInstance(uri, openDialog); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! - getSupportFragmentManager().beginTransaction() - .replace(R.id.decrypt_files_fragment_container, mFragment) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java deleted file mode 100644 index 234362edc..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFilesFragment.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * 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.ui; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.KeychainIntentService.IOType; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; -import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.Log; - -import java.io.File; - -public class DecryptFilesFragment extends DecryptFragment { - public static final String ARG_URI = "uri"; - public static final String ARG_OPEN_DIRECTLY = "open_directly"; - - private static final int REQUEST_CODE_INPUT = 0x00007003; - private static final int REQUEST_CODE_OUTPUT = 0x00007007; - - // view - private TextView mFilename; - private CheckBox mDeleteAfter; - private View mDecryptButton; - - // model - private Uri mInputUri = null; - private Uri mOutputUri = null; - - private String mCurrentCryptoOperation; - - /** - * Creates new instance of this fragment - */ - public static DecryptFilesFragment newInstance(Uri uri, boolean openDirectly) { - DecryptFilesFragment frag = new DecryptFilesFragment(); - - Bundle args = new Bundle(); - args.putParcelable(ARG_URI, uri); - args.putBoolean(ARG_OPEN_DIRECTLY, openDirectly); - - frag.setArguments(args); - - return frag; - } - - /** - * Inflate the layout for this fragment - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.decrypt_files_fragment, container, false); - - mFilename = (TextView) view.findViewById(R.id.decrypt_files_filename); - mDeleteAfter = (CheckBox) view.findViewById(R.id.decrypt_files_delete_after_decryption); - mDecryptButton = view.findViewById(R.id.decrypt_files_action_decrypt); - view.findViewById(R.id.decrypt_files_browse).setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - FileHelper.openDocument(DecryptFilesFragment.this, "*/*", REQUEST_CODE_INPUT); - } else { - FileHelper.openFile(DecryptFilesFragment.this, mInputUri, "*/*", - REQUEST_CODE_INPUT); - } - } - }); - mDecryptButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - decryptAction(); - } - }); - - return view; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - setInputUri(getArguments().<Uri>getParcelable(ARG_URI)); - - if (getArguments().getBoolean(ARG_OPEN_DIRECTLY, false)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - FileHelper.openDocument(DecryptFilesFragment.this, "*/*", REQUEST_CODE_INPUT); - } else { - FileHelper.openFile(DecryptFilesFragment.this, mInputUri, "*/*", - REQUEST_CODE_INPUT); - } - } - } - - private void setInputUri(Uri inputUri) { - if (inputUri == null) { - mInputUri = null; - mFilename.setText(""); - return; - } - - mInputUri = inputUri; - mFilename.setText(FileHelper.getFilename(getActivity(), mInputUri)); - } - - private void decryptAction() { - if (mInputUri == null) { - Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR).show(); - return; - } - - startDecryptFilenames(); - } - - private String removeEncryptedAppend(String name) { - if (name.endsWith(Constants.FILE_EXTENSION_ASC) - || name.endsWith(Constants.FILE_EXTENSION_PGP_MAIN) - || name.endsWith(Constants.FILE_EXTENSION_PGP_ALTERNATE)) { - return name.substring(0, name.length() - 4); - } - return name; - } - - private void askForOutputFilename(String originalFilename) { - if (TextUtils.isEmpty(originalFilename)) { - originalFilename = removeEncryptedAppend(FileHelper.getFilename(getActivity(), mInputUri)); - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - File file = new File(mInputUri.getPath()); - File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR; - File targetFile = new File(parentDir, originalFilename); - FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file), - getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT); - } else { - FileHelper.saveDocument(this, "*/*", originalFilename, REQUEST_CODE_OUTPUT); - } - } - - private void startDecrypt() { - mCurrentCryptoOperation = KeychainIntentService.ACTION_DECRYPT_VERIFY; - cryptoOperation(new CryptoInputParcel()); - } - - private void startDecryptFilenames() { - mCurrentCryptoOperation = KeychainIntentService.ACTION_DECRYPT_METADATA; - cryptoOperation(new CryptoInputParcel()); - } - - @Override - @SuppressLint("HandlerLeak") - protected void cryptoOperation(CryptoInputParcel cryptoInput) { - // Send all information needed to service to decrypt in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - - // fill values for this action - Bundle data = new Bundle(); - // use current operation, either decrypt metadata or decrypt payload - intent.setAction(mCurrentCryptoOperation); - - // data - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); - - Log.d(Constants.TAG, "mInputUri=" + mInputUri + ", mOutputUri=" + mOutputUri); - - data.putInt(KeychainIntentService.SOURCE, IOType.URI.ordinal()); - data.putParcelable(KeychainIntentService.ENCRYPT_DECRYPT_INPUT_URI, mInputUri); - - data.putInt(KeychainIntentService.TARGET, IOType.URI.ordinal()); - data.putParcelable(KeychainIntentService.ENCRYPT_DECRYPT_OUTPUT_URI, mOutputUri); - - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); - - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Message is received after decrypting is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_decrypting), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - @Override - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - // handle pending messages - if (handlePendingMessage(message)) { - return; - } - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - - DecryptVerifyResult pgpResult = - returnData.getParcelable(DecryptVerifyResult.EXTRA_RESULT); - - if (pgpResult.success()) { - switch (mCurrentCryptoOperation) { - case KeychainIntentService.ACTION_DECRYPT_METADATA: { - askForOutputFilename(pgpResult.getDecryptMetadata().getFilename()); - break; - } - case KeychainIntentService.ACTION_DECRYPT_VERIFY: { - // display signature result in activity - loadVerifyResult(pgpResult); - - if (mDeleteAfter.isChecked()) { - // Create and show dialog to delete original file - DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment.newInstance(mInputUri); - deleteFileDialog.show(getActivity().getSupportFragmentManager(), "deleteDialog"); - setInputUri(null); - } - - /* - // A future open after decryption feature - if () { - Intent viewFile = new Intent(Intent.ACTION_VIEW); - viewFile.setInputData(mOutputUri); - startActivity(viewFile); - } - */ - break; - } - default: { - Log.e(Constants.TAG, "Bug: not supported operation!"); - break; - } - } - } - pgpResult.createNotify(getActivity()).show(DecryptFilesFragment.this); - } - - } - }; - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - saveHandler.showProgressDialog(getActivity()); - - // start service with intent - getActivity().startService(intent); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_CODE_INPUT: { - if (resultCode == Activity.RESULT_OK && data != null) { - setInputUri(data.getData()); - } - return; - } - - case REQUEST_CODE_OUTPUT: { - // This happens after output file was selected, so start our operation - if (resultCode == Activity.RESULT_OK && data != null) { - mOutputUri = data.getData(); - startDecrypt(); - } - return; - } - - default: { - super.onActivityResult(requestCode, resultCode, data); - } - } - } - - @Override - protected void onVerifyLoaded(boolean hideErrorOverlay) { - - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java index e9bc42a4d..37dd6afad 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java @@ -23,43 +23,43 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; +import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.util.Log; import android.view.View; +import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.ViewAnimator; +import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpSignatureResult; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.Preferences; -public abstract class DecryptFragment extends CryptoOperationFragment implements - LoaderManager.LoaderCallbacks<Cursor> { +public abstract class DecryptFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> { public static final int LOADER_ID_UNIFIED = 0; + public static final String ARG_DECRYPT_VERIFY_RESULT = "decrypt_verify_result"; protected LinearLayout mResultLayout; protected ImageView mEncryptionIcon; @@ -71,10 +71,11 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements protected TextView mSignatureEmail; protected TextView mSignatureAction; - private LinearLayout mContentLayout; - private LinearLayout mErrorOverlayLayout; - private OpenPgpSignatureResult mSignatureResult; + private DecryptVerifyResult mDecryptVerifyResult; + private ViewAnimator mOverlayAnimator; + + private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper; @Override public void onViewCreated(View view, Bundle savedInstanceState) { @@ -93,53 +94,55 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements mSignatureAction = (TextView) getActivity().findViewById(R.id.result_signature_action); // Overlay - mContentLayout = (LinearLayout) view.findViewById(R.id.decrypt_content); - mErrorOverlayLayout = (LinearLayout) view.findViewById(R.id.decrypt_error_overlay); + mOverlayAnimator = (ViewAnimator) view; Button vErrorOverlayButton = (Button) view.findViewById(R.id.decrypt_error_overlay_button); - vErrorOverlayButton.setOnClickListener(new View.OnClickListener() { + vErrorOverlayButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + mOverlayAnimator.setDisplayedChild(0); } }); } - private void lookupUnknownKey(long unknownKeyId) { + private void showErrorOverlay(boolean overlay) { + int child = overlay ? 1 : 0; + if (mOverlayAnimator.getDisplayedChild() != child) { + mOverlayAnimator.setDisplayedChild(child); + } + } - // Message is received after importing is done in KeychainIntentService - ServiceProgressHandler serviceHandler = new ServiceProgressHandler(getActivity()) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); + outState.putParcelable(ARG_DECRYPT_VERIFY_RESULT, mDecryptVerifyResult); + } - if (returnData == null) { - return; - } + @Override + public void onViewStateRestored(Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); - final ImportKeyResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); + if (savedInstanceState == null) { + return; + } - result.createNotify(getActivity()).show(); + DecryptVerifyResult result = savedInstanceState.getParcelable(ARG_DECRYPT_VERIFY_RESULT); + if (result != null) { + loadVerifyResult(result); + } + } - getLoaderManager().restartLoader(LOADER_ID_UNIFIED, null, DecryptFragment.this); - } - } - }; + private void lookupUnknownKey(long unknownKeyId) { - // fill values for this action - Bundle data = new Bundle(); + final ArrayList<ParcelableKeyRing> keyList; + final String keyserver; // search config { Preferences prefs = Preferences.getPreferences(getActivity()); Preferences.CloudSearchPrefs cloudPrefs = new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver()); - data.putString(KeychainIntentService.IMPORT_KEY_SERVER, cloudPrefs.keyserver); + keyserver = cloudPrefs.keyserver; } { @@ -148,19 +151,43 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements ArrayList<ParcelableKeyRing> selectedEntries = new ArrayList<>(); selectedEntries.add(keyEntry); - data.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, selectedEntries); + keyList = selectedEntries; } - // Send all information needed to service to query keys in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> callback + = new CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult>() { + + @Override + public ImportKeyringParcel createOperationInput() { + return new ImportKeyringParcel(keyList, keyserver); + } + + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + result.createNotify(getActivity()).show(); + + getLoaderManager().restartLoader(LOADER_ID_UNIFIED, null, DecryptFragment.this); + } + + @Override + public void onCryptoOperationCancelled() { + // do nothing + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + result.createNotify(getActivity()).show(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(serviceHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + mImportOpHelper = new CryptoOperationHelper<>(1, this, callback, R.string.progress_importing); - getActivity().startService(intent); + mImportOpHelper.cryptoOperation(); } private void showKey(long keyId) { @@ -178,43 +205,54 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements } } - /** - * @return returns false if signature is invalid, key is revoked or expired. - */ protected void loadVerifyResult(DecryptVerifyResult decryptVerifyResult) { + mDecryptVerifyResult = decryptVerifyResult; mSignatureResult = decryptVerifyResult.getSignatureResult(); + OpenPgpDecryptionResult decryptionResult = decryptVerifyResult.getDecryptionResult(); + mResultLayout.setVisibility(View.VISIBLE); - // unsigned data - if (mSignatureResult == null) { + switch (decryptionResult.getResult()) { + case OpenPgpDecryptionResult.RESULT_ENCRYPTED: { + mEncryptionText.setText(R.string.decrypt_result_encrypted); + KeyFormattingUtils.setStatusImage(getActivity(), mEncryptionIcon, mEncryptionText, State.ENCRYPTED); + break; + } + + case OpenPgpDecryptionResult.RESULT_INSECURE: { + mEncryptionText.setText(R.string.decrypt_result_insecure); + KeyFormattingUtils.setStatusImage(getActivity(), mEncryptionIcon, mEncryptionText, State.INSECURE); + break; + } + + default: + case OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED: { + mEncryptionText.setText(R.string.decrypt_result_not_encrypted); + KeyFormattingUtils.setStatusImage(getActivity(), mEncryptionIcon, mEncryptionText, State.NOT_ENCRYPTED); + break; + } + } + + if (mSignatureResult.getResult() == OpenPgpSignatureResult.RESULT_NO_SIGNATURE) { + // no signature setSignatureLayoutVisibility(View.GONE); mSignatureText.setText(R.string.decrypt_result_no_signature); KeyFormattingUtils.setStatusImage(getActivity(), mSignatureIcon, mSignatureText, State.NOT_SIGNED); - mEncryptionText.setText(R.string.decrypt_result_encrypted); - KeyFormattingUtils.setStatusImage(getActivity(), mEncryptionIcon, mEncryptionText, State.ENCRYPTED); getLoaderManager().destroyLoader(LOADER_ID_UNIFIED); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); - - return; - } - - if (mSignatureResult.isSignatureOnly()) { - mEncryptionText.setText(R.string.decrypt_result_not_encrypted); - KeyFormattingUtils.setStatusImage(getActivity(), mEncryptionIcon, mEncryptionText, State.NOT_ENCRYPTED); } else { - mEncryptionText.setText(R.string.decrypt_result_encrypted); - KeyFormattingUtils.setStatusImage(getActivity(), mEncryptionIcon, mEncryptionText, State.ENCRYPTED); - } + // signature present - getLoaderManager().restartLoader(LOADER_ID_UNIFIED, null, this); + // after loader is restarted signature results are checked + getLoaderManager().restartLoader(LOADER_ID_UNIFIED, null, this); + } } private void setSignatureLayoutVisibility(int visibility) { @@ -289,8 +327,9 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements // NOTE: Don't use revoked and expired fields from database, they don't show // revoked/expired subkeys - boolean isRevoked = mSignatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_KEY_REVOKED; - boolean isExpired = mSignatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_KEY_EXPIRED; + boolean isRevoked = mSignatureResult.getResult() == OpenPgpSignatureResult.RESULT_INVALID_KEY_REVOKED; + boolean isExpired = mSignatureResult.getResult() == OpenPgpSignatureResult.RESULT_INVALID_KEY_EXPIRED; + boolean isInsecure = mSignatureResult.getResult() == OpenPgpSignatureResult.RESULT_INVALID_INSECURE; boolean isVerified = data.getInt(INDEX_VERIFIED) > 0; boolean isYours = data.getInt(INDEX_HAS_ANY_SECRET) != 0; @@ -301,10 +340,7 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.VISIBLE); - mContentLayout.setVisibility(View.GONE); - - onVerifyLoaded(false); + onVerifyLoaded(true); } else if (isExpired) { mSignatureText.setText(R.string.decrypt_result_signature_expired_key); @@ -313,21 +349,18 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); - } else if (isYours) { - - mSignatureText.setText(R.string.decrypt_result_signature_secret); - KeyFormattingUtils.setStatusImage(getActivity(), mSignatureIcon, mSignatureText, State.VERIFIED); + } else if (isInsecure) { + mSignatureText.setText(R.string.decrypt_result_insecure_cryptography); + KeyFormattingUtils.setStatusImage(getActivity(), mSignatureIcon, mSignatureText, State.INSECURE); setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); @@ -339,6 +372,8 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); + showErrorOverlay(false); + onVerifyLoaded(true); } else if (isVerified) { @@ -348,8 +383,7 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); @@ -360,8 +394,7 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements setSignatureLayoutVisibility(View.VISIBLE); setShowAction(signatureKeyId); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); } @@ -382,9 +415,9 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements final long signatureKeyId = mSignatureResult.getKeyId(); - int result = mSignatureResult.getStatus(); - if (result != OpenPgpSignatureResult.SIGNATURE_KEY_MISSING - && result != OpenPgpSignatureResult.SIGNATURE_ERROR) { + int result = mSignatureResult.getResult(); + if (result != OpenPgpSignatureResult.RESULT_KEY_MISSING + && result != OpenPgpSignatureResult.RESULT_INVALID_SIGNATURE) { Log.e(Constants.TAG, "got missing status for non-missing key, shouldn't happen!"); } @@ -402,9 +435,9 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements getActivity(), mSignatureResult.getKeyId())); } - switch (mSignatureResult.getStatus()) { + switch (mSignatureResult.getResult()) { - case OpenPgpSignatureResult.SIGNATURE_KEY_MISSING: { + case OpenPgpSignatureResult.RESULT_KEY_MISSING: { mSignatureText.setText(R.string.decrypt_result_signature_missing_key); KeyFormattingUtils.setStatusImage(getActivity(), mSignatureIcon, mSignatureText, State.UNKNOWN_KEY); @@ -419,22 +452,20 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements } }); - mErrorOverlayLayout.setVisibility(View.GONE); - mContentLayout.setVisibility(View.VISIBLE); + showErrorOverlay(false); onVerifyLoaded(true); break; } - case OpenPgpSignatureResult.SIGNATURE_ERROR: { + case OpenPgpSignatureResult.RESULT_INVALID_SIGNATURE: { mSignatureText.setText(R.string.decrypt_result_invalid_signature); KeyFormattingUtils.setStatusImage(getActivity(), mSignatureIcon, mSignatureText, State.INVALID); setSignatureLayoutVisibility(View.GONE); - mErrorOverlayLayout.setVisibility(View.VISIBLE); - mContentLayout.setVisibility(View.GONE); + showErrorOverlay(true); onVerifyLoaded(false); break; @@ -446,4 +477,11 @@ public abstract class DecryptFragment extends CryptoOperationFragment implements protected abstract void onVerifyLoaded(boolean hideErrorOverlay); + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mImportOpHelper != null) { + mImportOpHelper.handleActivityResult(requestCode, resultCode, data); + } + super.onActivityResult(requestCode, resultCode, data); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java new file mode 100644 index 000000000..26e56280a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java @@ -0,0 +1,945 @@ +/* + * 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.ui; + + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import android.app.Activity; +import android.content.ClipDescription; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LabeledIntent; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v7.widget.DefaultItemAnimator; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnDismissListener; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import org.openintents.openpgp.OpenPgpMetadata; +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.sufficientlysecure.keychain.BuildConfig; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +// this import NEEDS to be above the ViewModel one, or it won't compile! (as of 06/06/15) +import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder; +import org.sufficientlysecure.keychain.ui.DecryptListFragment.DecryptFilesAdapter.ViewModel; +import org.sufficientlysecure.keychain.ui.adapter.SpacesItemDecoration; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.util.FileHelper; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ParcelableHashMap; + + +public class DecryptListFragment + extends QueueingCryptoOperationFragment<PgpDecryptVerifyInputParcel,DecryptVerifyResult> + implements OnMenuItemClickListener { + + public static final String ARG_INPUT_URIS = "input_uris"; + public static final String ARG_OUTPUT_URIS = "output_uris"; + public static final String ARG_CANCELLED_URIS = "cancelled_uris"; + public static final String ARG_RESULTS = "results"; + + private static final int REQUEST_CODE_OUTPUT = 0x00007007; + public static final String ARG_CURRENT_URI = "current_uri"; + + private ArrayList<Uri> mInputUris; + private HashMap<Uri, Uri> mOutputUris; + private ArrayList<Uri> mPendingInputUris; + private ArrayList<Uri> mCancelledInputUris; + + private Uri mCurrentInputUri; + + private DecryptFilesAdapter mAdapter; + + /** + * Creates new instance of this fragment + */ + public static DecryptListFragment newInstance(ArrayList<Uri> uris) { + DecryptListFragment frag = new DecryptListFragment(); + + Bundle args = new Bundle(); + args.putParcelableArrayList(ARG_INPUT_URIS, uris); + frag.setArguments(args); + + return frag; + } + + public DecryptListFragment() { + super(null); + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.decrypt_files_list_fragment, container, false); + + RecyclerView vFilesList = (RecyclerView) view.findViewById(R.id.decrypted_files_list); + + vFilesList.addItemDecoration(new SpacesItemDecoration( + FormattingUtils.dpToPx(getActivity(), 4))); + vFilesList.setHasFixedSize(true); + // TODO make this a grid, for tablets! + vFilesList.setLayoutManager(new LinearLayoutManager(getActivity())); + vFilesList.setItemAnimator(new DefaultItemAnimator()); + + mAdapter = new DecryptFilesAdapter(getActivity(), this); + vFilesList.setAdapter(mAdapter); + + return view; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putParcelableArrayList(ARG_INPUT_URIS, mInputUris); + + HashMap<Uri,DecryptVerifyResult> results = new HashMap<>(mInputUris.size()); + for (Uri uri : mInputUris) { + if (mPendingInputUris.contains(uri)) { + continue; + } + DecryptVerifyResult result = mAdapter.getItemResult(uri); + if (result != null) { + results.put(uri, result); + } + } + + outState.putParcelable(ARG_RESULTS, new ParcelableHashMap<>(results)); + outState.putParcelable(ARG_OUTPUT_URIS, new ParcelableHashMap<>(mOutputUris)); + outState.putParcelableArrayList(ARG_CANCELLED_URIS, mCancelledInputUris); + outState.putParcelable(ARG_CURRENT_URI, mCurrentInputUri); + + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Bundle args = savedInstanceState != null ? savedInstanceState : getArguments(); + + ArrayList<Uri> inputUris = getArguments().getParcelableArrayList(ARG_INPUT_URIS); + ArrayList<Uri> cancelledUris = args.getParcelableArrayList(ARG_CANCELLED_URIS); + ParcelableHashMap<Uri,Uri> outputUris = args.getParcelable(ARG_OUTPUT_URIS); + ParcelableHashMap<Uri,DecryptVerifyResult> results = args.getParcelable(ARG_RESULTS); + Uri currentInputUri = args.getParcelable(ARG_CURRENT_URI); + + displayInputUris(inputUris, currentInputUri, cancelledUris, + outputUris != null ? outputUris.getMap() : null, + results != null ? results.getMap() : null + ); + } + + private void displayInputUris(ArrayList<Uri> inputUris, Uri currentInputUri, + ArrayList<Uri> cancelledUris, HashMap<Uri,Uri> outputUris, + HashMap<Uri,DecryptVerifyResult> results) { + + mInputUris = inputUris; + mCurrentInputUri = currentInputUri; + mOutputUris = outputUris != null ? outputUris : new HashMap<Uri,Uri>(inputUris.size()); + mCancelledInputUris = cancelledUris != null ? cancelledUris : new ArrayList<Uri>(); + + mPendingInputUris = new ArrayList<>(); + + for (final Uri uri : inputUris) { + mAdapter.add(uri); + + if (uri.equals(mCurrentInputUri)) { + continue; + } + + if (mCancelledInputUris.contains(uri)) { + mAdapter.setCancelled(uri, new OnClickListener() { + @Override + public void onClick(View v) { + retryUri(uri); + } + }); + continue; + } + + if (results != null && results.containsKey(uri)) { + processResult(uri, results.get(uri)); + } else { + mOutputUris.put(uri, TemporaryStorageProvider.createFile(getActivity())); + mPendingInputUris.add(uri); + } + } + + if (mCurrentInputUri == null) { + cryptoOperation(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_OUTPUT: { + // This happens after output file was selected, so start our operation + if (resultCode == Activity.RESULT_OK && data != null) { + Uri decryptedFileUri = mOutputUris.get(mCurrentInputUri); + Uri saveUri = data.getData(); + saveFile(decryptedFileUri, saveUri); + mCurrentInputUri = null; + } + return; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + } + } + } + + private void saveFile(Uri decryptedFileUri, Uri saveUri) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + try { + FileHelper.copyUriData(activity, decryptedFileUri, saveUri); + Notify.create(activity, R.string.file_saved, Style.OK).show(); + } catch (IOException e) { + Log.e(Constants.TAG, "error saving file", e); + Notify.create(activity, R.string.error_saving_file, Style.ERROR).show(); + } + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + mAdapter.setProgress(mCurrentInputUri, progress, max, msg); + return true; + } + + @Override + public void onQueuedOperationError(DecryptVerifyResult result) { + final Uri uri = mCurrentInputUri; + mCurrentInputUri = null; + + mAdapter.addResult(uri, result, null, null, null); + + cryptoOperation(); + } + + @Override + public void onQueuedOperationSuccess(DecryptVerifyResult result) { + Uri uri = mCurrentInputUri; + mCurrentInputUri = null; + + processResult(uri, result); + + cryptoOperation(); + } + + @Override + public void onCryptoOperationCancelled() { + super.onCryptoOperationCancelled(); + + final Uri uri = mCurrentInputUri; + mCurrentInputUri = null; + + mCancelledInputUris.add(uri); + mAdapter.setCancelled(uri, new OnClickListener() { + @Override + public void onClick(View v) { + retryUri(uri); + } + }); + + cryptoOperation(); + + } + + private void processResult(final Uri uri, final DecryptVerifyResult result) { + + new AsyncTask<Void, Void, Drawable>() { + @Override + protected Drawable doInBackground(Void... params) { + + Context context = getActivity(); + if (result.getDecryptionMetadata() == null || context == null) { + return null; + } + + String type = result.getDecryptionMetadata().getMimeType(); + Uri outputUri = mOutputUris.get(uri); + if (type == null || outputUri == null) { + return null; + } + + TemporaryStorageProvider.setMimeType(context, outputUri, type); + + if (ClipDescription.compareMimeTypes(type, "image/*")) { + int px = FormattingUtils.dpToPx(context, 48); + Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px)); + return new BitmapDrawable(context.getResources(), bitmap); + } + + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(outputUri, type); + + final List<ResolveInfo> matches = + context.getPackageManager().queryIntentActivities(intent, 0); + //noinspection LoopStatementThatDoesntLoop + for (ResolveInfo match : matches) { + return match.loadIcon(getActivity().getPackageManager()); + } + + return null; + + } + + @Override + protected void onPostExecute(Drawable icon) { + processResult(uri, result, icon); + } + }.execute(); + + } + + private void processResult(final Uri uri, DecryptVerifyResult result, Drawable icon) { + + OnClickListener onFileClick = null, onKeyClick = null; + + OpenPgpSignatureResult sigResult = result.getSignatureResult(); + if (sigResult != null) { + final long keyId = sigResult.getKeyId(); + if (sigResult.getResult() != OpenPgpSignatureResult.RESULT_KEY_MISSING) { + onKeyClick = new OnClickListener() { + @Override + public void onClick(View view) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + Intent intent = new Intent(activity, ViewKeyActivity.class); + intent.setData(KeyRings.buildUnifiedKeyRingUri(keyId)); + activity.startActivity(intent); + } + }; + } + } + + if (result.success() && result.getDecryptionMetadata() != null) { + onFileClick = new OnClickListener() { + @Override + public void onClick(View view) { + displayWithViewIntent(uri); + } + }; + } + + mAdapter.addResult(uri, result, icon, onFileClick, onKeyClick); + + } + + public void retryUri(Uri uri) { + + // never interrupt running operations! + if (mCurrentInputUri != null) { + return; + } + + // un-cancel this one + mCancelledInputUris.remove(uri); + mPendingInputUris.add(uri); + mAdapter.setCancelled(uri, null); + + cryptoOperation(); + + } + + public void displayWithViewIntent(final Uri uri) { + Activity activity = getActivity(); + if (activity == null || mCurrentInputUri != null) { + return; + } + + final Uri outputUri = mOutputUris.get(uri); + final DecryptVerifyResult result = mAdapter.getItemResult(uri); + if (outputUri == null || result == null) { + return; + } + + final OpenPgpMetadata metadata = result.getDecryptionMetadata(); + + // text/plain is a special case where we extract the uri content into + // the EXTRA_TEXT extra ourselves, and display a chooser which includes + // OpenKeychain's internal viewer + if ("text/plain".equals(metadata.getMimeType())) { + + // this is a significant i/o operation, use an asynctask + new AsyncTask<Void,Void,Intent>() { + + @Override + protected Intent doInBackground(Void... params) { + + Activity activity = getActivity(); + if (activity == null) { + return null; + } + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(outputUri, "text/plain"); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + return intent; + } + + @Override + protected void onPostExecute(Intent intent) { + // for result so we can possibly get a snackbar error from internal viewer + Activity activity = getActivity(); + if (intent == null || activity == null) { + return; + } + + LabeledIntent internalIntent = new LabeledIntent( + new Intent(intent) + .setClass(activity, DisplayTextActivity.class) + .putExtra(DisplayTextActivity.EXTRA_METADATA, result), + BuildConfig.APPLICATION_ID, R.string.view_internal, R.drawable.ic_launcher); + + Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show)); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, + new Parcelable[] { internalIntent }); + + activity.startActivity(chooserIntent); + } + + }.execute(); + + } else { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(outputUri, metadata.getMimeType()); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show)); + chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + activity.startActivity(chooserIntent); + } + + } + + @Override + public PgpDecryptVerifyInputParcel createOperationInput() { + + if (mCurrentInputUri == null) { + if (mPendingInputUris.isEmpty()) { + // nothing left to do + return null; + } + + mCurrentInputUri = mPendingInputUris.remove(0); + } + + Uri currentOutputUri = mOutputUris.get(mCurrentInputUri); + Log.d(Constants.TAG, "mInputUri=" + mCurrentInputUri + ", mOutputUri=" + currentOutputUri); + + return new PgpDecryptVerifyInputParcel(mCurrentInputUri, currentOutputUri) + .setAllowSymmetricDecryption(true); + + } + + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + if (mAdapter.mMenuClickedModel == null || !mAdapter.mMenuClickedModel.hasResult()) { + return false; + } + + // don't process menu items until all items are done! + if (!mPendingInputUris.isEmpty()) { + return true; + } + + Activity activity = getActivity(); + if (activity == null) { + return false; + } + + ViewModel model = mAdapter.mMenuClickedModel; + DecryptVerifyResult result = model.mResult; + switch (menuItem.getItemId()) { + case R.id.view_log: + Intent intent = new Intent(activity, LogDisplayActivity.class); + intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result); + activity.startActivity(intent); + return true; + case R.id.decrypt_save: + OpenPgpMetadata metadata = result.getDecryptionMetadata(); + if (metadata == null) { + return true; + } + mCurrentInputUri = model.mInputUri; + FileHelper.saveDocument(this, metadata.getFilename(), model.mInputUri, metadata.getMimeType(), + R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, REQUEST_CODE_OUTPUT); + return true; + case R.id.decrypt_delete: + deleteFile(activity, model.mInputUri); + return true; + } + return false; + } + + private void deleteFile(Activity activity, Uri uri) { + + if ("file".equals(uri.getScheme())) { + File file = new File(uri.getPath()); + if (file.delete()) { + Notify.create(activity, R.string.file_delete_ok, Style.OK).show(); + } else { + Notify.create(activity, R.string.file_delete_none, Style.WARN).show(); + } + return; + } + + if ("content".equals(uri.getScheme())) { + try { + int deleted = activity.getContentResolver().delete(uri, null, null); + if (deleted > 0) { + Notify.create(activity, R.string.file_delete_ok, Style.OK).show(); + } else { + Notify.create(activity, R.string.file_delete_none, Style.WARN).show(); + } + } catch (Exception e) { + Log.e(Constants.TAG, "exception deleting file", e); + Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show(); + } + return; + } + + Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show(); + + } + + public static class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> { + private Context mContext; + private ArrayList<ViewModel> mDataset; + private OnMenuItemClickListener mMenuItemClickListener; + private ViewModel mMenuClickedModel; + + public class ViewModel { + Context mContext; + Uri mInputUri; + DecryptVerifyResult mResult; + Drawable mIcon; + + OnClickListener mOnFileClickListener; + OnClickListener mOnKeyClickListener; + + int mProgress, mMax; + String mProgressMsg; + OnClickListener mCancelled; + + ViewModel(Context context, Uri uri) { + mContext = context; + mInputUri = uri; + mProgress = 0; + mMax = 100; + mCancelled = null; + } + + void addResult(DecryptVerifyResult result) { + mResult = result; + } + + void addIcon(Drawable icon) { + mIcon = icon; + } + + void setOnClickListeners(OnClickListener onFileClick, OnClickListener onKeyClick) { + mOnFileClickListener = onFileClick; + mOnKeyClickListener = onKeyClick; + } + + boolean hasResult() { + return mResult != null; + } + + void setCancelled(OnClickListener retryListener) { + mCancelled = retryListener; + } + + void setProgress(int progress, int max, String msg) { + if (msg != null) { + mProgressMsg = msg; + } + mProgress = progress; + mMax = max; + } + + // Depends on inputUri only + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ViewModel viewModel = (ViewModel) o; + return !(mInputUri != null ? !mInputUri.equals(viewModel.mInputUri) + : viewModel.mInputUri != null); + } + + // Depends on inputUri only + @Override + public int hashCode() { + return mResult != null ? mResult.hashCode() : 0; + } + + @Override + public String toString() { + return mResult.toString(); + } + } + + // Provide a suitable constructor (depends on the kind of dataset) + public DecryptFilesAdapter(Context context, OnMenuItemClickListener menuItemClickListener) { + mContext = context; + mMenuItemClickListener = menuItemClickListener; + mDataset = new ArrayList<>(); + } + + // Create new views (invoked by the layout manager) + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + //inflate your layout and pass it to view holder + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.decrypt_list_entry, parent, false); + return new ViewHolder(v); + } + + // Replace the contents of a view (invoked by the layout manager) + @Override + public void onBindViewHolder(ViewHolder holder, final int position) { + // - get element from your dataset at this position + // - replace the contents of the view with that element + final ViewModel model = mDataset.get(position); + + if (model.mCancelled != null) { + bindItemCancelled(holder, model); + return; + } + + if (!model.hasResult()) { + bindItemProgress(holder, model); + return; + } + + if (model.mResult.success()) { + bindItemSuccess(holder, model); + } else { + bindItemFailure(holder, model); + } + + } + + private void bindItemCancelled(ViewHolder holder, ViewModel model) { + if (holder.vAnimator.getDisplayedChild() != 3) { + holder.vAnimator.setDisplayedChild(3); + } + + holder.vCancelledRetry.setOnClickListener(model.mCancelled); + } + + private void bindItemProgress(ViewHolder holder, ViewModel model) { + if (holder.vAnimator.getDisplayedChild() != 0) { + holder.vAnimator.setDisplayedChild(0); + } + + holder.vProgress.setProgress(model.mProgress); + holder.vProgress.setMax(model.mMax); + if (model.mProgressMsg != null) { + holder.vProgressMsg.setText(model.mProgressMsg); + } + } + + private void bindItemSuccess(ViewHolder holder, final ViewModel model) { + if (holder.vAnimator.getDisplayedChild() != 1) { + holder.vAnimator.setDisplayedChild(1); + } + + KeyFormattingUtils.setStatus(mContext, holder, model.mResult); + + final OpenPgpMetadata metadata = model.mResult.getDecryptionMetadata(); + + String filename; + if (metadata == null) { + filename = mContext.getString(R.string.filename_unknown); + } else if (TextUtils.isEmpty(metadata.getFilename())) { + filename = mContext.getString("text/plain".equals(metadata.getMimeType()) + ? R.string.filename_unknown_text : R.string.filename_unknown); + } else { + filename = metadata.getFilename(); + } + holder.vFilename.setText(filename); + + long size = metadata == null ? 0 : metadata.getOriginalSize(); + if (size == -1 || size == 0) { + holder.vFilesize.setText(""); + } else { + holder.vFilesize.setText(FileHelper.readableFileSize(size)); + } + + if (model.mIcon != null) { + holder.vThumbnail.setImageDrawable(model.mIcon); + } else { + holder.vThumbnail.setImageResource(R.drawable.ic_doc_generic_am); + } + + holder.vFile.setOnClickListener(model.mOnFileClickListener); + holder.vSignatureLayout.setOnClickListener(model.mOnKeyClickListener); + + holder.vContextMenu.setTag(model); + holder.vContextMenu.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + mMenuClickedModel = model; + PopupMenu menu = new PopupMenu(mContext, view); + menu.inflate(R.menu.decrypt_item_context_menu); + menu.setOnMenuItemClickListener(mMenuItemClickListener); + menu.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(PopupMenu popupMenu) { + mMenuClickedModel = null; + } + }); + menu.show(); + } + }); + } + + private void bindItemFailure(ViewHolder holder, final ViewModel model) { + if (holder.vAnimator.getDisplayedChild() != 2) { + holder.vAnimator.setDisplayedChild(2); + } + + holder.vErrorMsg.setText(model.mResult.getLog().getLast().mType.getMsgId()); + + holder.vErrorViewLog.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(mContext, LogDisplayActivity.class); + intent.putExtra(LogDisplayFragment.EXTRA_RESULT, model.mResult); + mContext.startActivity(intent); + } + }); + + } + + // Return the size of your dataset (invoked by the layout manager) + @Override + public int getItemCount() { + return mDataset.size(); + } + + public DecryptVerifyResult getItemResult(Uri uri) { + ViewModel model = new ViewModel(mContext, uri); + int pos = mDataset.indexOf(model); + if (pos == -1) { + return null; + } + model = mDataset.get(pos); + + return model.mResult; + } + + public void add(Uri uri) { + ViewModel newModel = new ViewModel(mContext, uri); + mDataset.add(newModel); + notifyItemInserted(mDataset.size()); + } + + public void setProgress(Uri uri, int progress, int max, String msg) { + ViewModel newModel = new ViewModel(mContext, uri); + int pos = mDataset.indexOf(newModel); + mDataset.get(pos).setProgress(progress, max, msg); + notifyItemChanged(pos); + } + + public void setCancelled(Uri uri, OnClickListener retryListener) { + ViewModel newModel = new ViewModel(mContext, uri); + int pos = mDataset.indexOf(newModel); + mDataset.get(pos).setCancelled(retryListener); + notifyItemChanged(pos); + } + + public void addResult(Uri uri, DecryptVerifyResult result, Drawable icon, + OnClickListener onFileClick, OnClickListener onKeyClick) { + + ViewModel model = new ViewModel(mContext, uri); + int pos = mDataset.indexOf(model); + model = mDataset.get(pos); + + model.addResult(result); + if (icon != null) { + model.addIcon(icon); + } + model.setOnClickListeners(onFileClick, onKeyClick); + + notifyItemChanged(pos); + } + + } + + + // Provide a reference to the views for each data item + // Complex data items may need more than one view per item, and + // you provide access to all the views for a data item in a view holder + public static class ViewHolder extends RecyclerView.ViewHolder implements StatusHolder { + public ViewAnimator vAnimator; + + public ProgressBar vProgress; + public TextView vProgressMsg; + + public View vFile; + public TextView vFilename; + public TextView vFilesize; + public ImageView vThumbnail; + + public ImageView vEncStatusIcon; + public TextView vEncStatusText; + + public ImageView vSigStatusIcon; + public TextView vSigStatusText; + public View vSignatureLayout; + public TextView vSignatureName; + public TextView vSignatureMail; + public TextView vSignatureAction; + public View vContextMenu; + + public TextView vErrorMsg; + public ImageView vErrorViewLog; + + public ImageView vCancelledRetry; + + public ViewHolder(View itemView) { + super(itemView); + + vAnimator = (ViewAnimator) itemView.findViewById(R.id.view_animator); + + vProgress = (ProgressBar) itemView.findViewById(R.id.progress); + vProgressMsg = (TextView) itemView.findViewById(R.id.progress_msg); + + vFile = itemView.findViewById(R.id.file); + vFilename = (TextView) itemView.findViewById(R.id.filename); + vFilesize = (TextView) itemView.findViewById(R.id.filesize); + vThumbnail = (ImageView) itemView.findViewById(R.id.thumbnail); + + vEncStatusIcon = (ImageView) itemView.findViewById(R.id.result_encryption_icon); + vEncStatusText = (TextView) itemView.findViewById(R.id.result_encryption_text); + + vSigStatusIcon = (ImageView) itemView.findViewById(R.id.result_signature_icon); + vSigStatusText = (TextView) itemView.findViewById(R.id.result_signature_text); + vSignatureLayout = itemView.findViewById(R.id.result_signature_layout); + vSignatureName = (TextView) itemView.findViewById(R.id.result_signature_name); + vSignatureMail= (TextView) itemView.findViewById(R.id.result_signature_email); + vSignatureAction = (TextView) itemView.findViewById(R.id.result_signature_action); + + vContextMenu = itemView.findViewById(R.id.context_menu); + + vErrorMsg = (TextView) itemView.findViewById(R.id.result_error_msg); + vErrorViewLog = (ImageView) itemView.findViewById(R.id.result_error_log); + + vCancelledRetry = (ImageView) itemView.findViewById(R.id.cancel_retry); + + } + + @Override + public ImageView getEncryptionStatusIcon() { + return vEncStatusIcon; + } + + @Override + public TextView getEncryptionStatusText() { + return vEncStatusText; + } + + @Override + public ImageView getSignatureStatusIcon() { + return vSigStatusIcon; + } + + @Override + public TextView getSignatureStatusText() { + return vSigStatusText; + } + + @Override + public View getSignatureLayout() { + return vSignatureLayout; + } + + @Override + public TextView getSignatureAction() { + return vSignatureAction; + } + + @Override + public TextView getSignatureUserName() { + return vSignatureName; + } + + @Override + public TextView getSignatureUserEmail() { + return vSignatureMail; + } + + @Override + public boolean hasEncrypt() { + return true; + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextActivity.java deleted file mode 100644 index e2eba3947..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextActivity.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2012-2015 Dominik Schürmann <dominik@dominikschuermann.de> - * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> - * - * 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.ui; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.View; -import android.widget.Toast; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; -import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.operations.results.SingletonResult; -import org.sufficientlysecure.keychain.pgp.PgpHelper; -import org.sufficientlysecure.keychain.ui.base.BaseActivity; -import org.sufficientlysecure.keychain.util.Log; - -import java.util.regex.Matcher; - -public class DecryptTextActivity extends BaseActivity { - - /* Intents */ - public static final String ACTION_DECRYPT_TEXT = OpenKeychainIntents.DECRYPT_TEXT; - public static final String EXTRA_TEXT = OpenKeychainIntents.DECRYPT_EXTRA_TEXT; - - // intern - public static final String ACTION_DECRYPT_FROM_CLIPBOARD = Constants.INTENT_PREFIX + "DECRYPT_TEXT_FROM_CLIPBOARD"; - - DecryptTextFragment mFragment; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setFullScreenDialogClose(new View.OnClickListener() { - @Override - public void onClick(View v) { - setResult(Activity.RESULT_CANCELED); - finish(); - } - }, false); - - // Handle intent actions - handleActions(savedInstanceState, getIntent()); - } - - @Override - protected void initLayout() { - setContentView(R.layout.decrypt_text_activity); - } - - /** - * Fixing broken PGP MESSAGE Strings coming from GMail/AOSP Mail - */ - private String fixPgpMessage(String message) { - // windows newline -> unix newline - message = message.replaceAll("\r\n", "\n"); - // Mac OS before X newline -> unix newline - message = message.replaceAll("\r", "\n"); - - // remove whitespaces before newline - message = message.replaceAll(" +\n", "\n"); - // only two consecutive newlines are allowed - message = message.replaceAll("\n\n+", "\n\n"); - - // replace non breakable spaces - message = message.replaceAll("\\xa0", " "); - - return message; - } - - /** - * Fixing broken PGP SIGNED MESSAGE Strings coming from GMail/AOSP Mail - */ - private String fixPgpCleartextSignature(CharSequence input) { - if (!TextUtils.isEmpty(input)) { - String text = input.toString(); - - // windows newline -> unix newline - text = text.replaceAll("\r\n", "\n"); - // Mac OS before X newline -> unix newline - text = text.replaceAll("\r", "\n"); - - return text; - } else { - return null; - } - } - - private String getPgpContent(CharSequence input) { - // only decrypt if clipboard content is available and a pgp message or cleartext signature - if (!TextUtils.isEmpty(input)) { - Log.dEscaped(Constants.TAG, "input: " + input); - - Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input); - if (matcher.matches()) { - String text = matcher.group(1); - text = fixPgpMessage(text); - - Log.dEscaped(Constants.TAG, "input fixed: " + text); - return text; - } else { - matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input); - if (matcher.matches()) { - String text = matcher.group(1); - text = fixPgpCleartextSignature(text); - - Log.dEscaped(Constants.TAG, "input fixed: " + text); - return text; - } else { - return null; - } - } - } else { - return null; - } - } - - /** - * Handles all actions with this intent - */ - private void handleActions(Bundle savedInstanceState, Intent intent) { - String action = intent.getAction(); - Bundle extras = intent.getExtras(); - String type = intent.getType(); - - if (extras == null) { - extras = new Bundle(); - } - - if (Intent.ACTION_SEND.equals(action) && type != null) { - Log.d(Constants.TAG, "ACTION_SEND"); - Log.logDebugBundle(extras, "SEND extras"); - - // When sending to Keychain Decrypt via share menu - if ("text/plain".equals(type)) { - String sharedText = extras.getString(Intent.EXTRA_TEXT); - sharedText = getPgpContent(sharedText); - - if (sharedText != null) { - loadFragment(savedInstanceState, sharedText); - } else { - Log.e(Constants.TAG, "EXTRA_TEXT does not contain PGP content!"); - Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); - finish(); - } - } else { - Log.e(Constants.TAG, "ACTION_SEND received non-plaintext, this should not happen in this activity!"); - Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); - finish(); - } - } else if (ACTION_DECRYPT_TEXT.equals(action)) { - Log.d(Constants.TAG, "ACTION_DECRYPT_TEXT"); - - String extraText = extras.getString(EXTRA_TEXT); - extraText = getPgpContent(extraText); - - if (extraText != null) { - loadFragment(savedInstanceState, extraText); - } else { - Log.e(Constants.TAG, "EXTRA_TEXT does not contain PGP content!"); - Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); - finish(); - } - } else if (ACTION_DECRYPT_FROM_CLIPBOARD.equals(action)) { - Log.d(Constants.TAG, "ACTION_DECRYPT_FROM_CLIPBOARD"); - - CharSequence clipboardText = ClipboardReflection.getClipboardText(this); - String text = getPgpContent(clipboardText); - - if (text != null) { - loadFragment(savedInstanceState, text); - } else { - returnInvalidResult(); - } - } else if (ACTION_DECRYPT_TEXT.equals(action)) { - Log.e(Constants.TAG, "Include the extra 'text' in your Intent!"); - Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); - finish(); - } - } - - private void returnInvalidResult() { - SingletonResult result = new SingletonResult( - SingletonResult.RESULT_ERROR, OperationResult.LogType.MSG_NO_VALID_ENC); - Intent intent = new Intent(); - intent.putExtra(SingletonResult.EXTRA_RESULT, result); - setResult(RESULT_OK, intent); - finish(); - } - - private void loadFragment(Bundle savedInstanceState, String ciphertext) { - // However, if we're being restored from a previous state, - // then we don't need to do anything and should return or else - // we could end up with overlapping fragments. - if (savedInstanceState != null) { - return; - } - - // Create an instance of the fragment - mFragment = DecryptTextFragment.newInstance(ciphertext); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! - getSupportFragmentManager().beginTransaction() - .replace(R.id.decrypt_text_fragment_container, mFragment) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextFragment.java deleted file mode 100644 index 381da6f0d..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptTextFragment.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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.ui; - -import android.app.ProgressDialog; -import android.content.Intent; -import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.KeychainIntentService.IOType; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; -import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.util.ShareHelper; - -import java.io.UnsupportedEncodingException; - -public class DecryptTextFragment extends DecryptFragment { - public static final String ARG_CIPHERTEXT = "ciphertext"; - - // view - private TextView mText; - - // model - private String mCiphertext; - private boolean mShowMenuOptions = false; - - /** - * Creates new instance of this fragment - */ - public static DecryptTextFragment newInstance(String ciphertext) { - DecryptTextFragment frag = new DecryptTextFragment(); - - Bundle args = new Bundle(); - args.putString(ARG_CIPHERTEXT, ciphertext); - - frag.setArguments(args); - - return frag; - } - - /** - * Inflate the layout for this fragment - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.decrypt_text_fragment, container, false); - mText = (TextView) view.findViewById(R.id.decrypt_text_plaintext); - - return view; - } - - /** - * Create Intent Chooser but exclude decrypt activites - */ - private Intent sendWithChooserExcludingDecrypt(String text) { - Intent prototype = createSendIntent(text); - String title = getString(R.string.title_share_message); - - // we don't want to decrypt the decrypted, no inception ;) - String[] blacklist = new String[]{ - Constants.PACKAGE_NAME + ".ui.DecryptTextActivity", - "org.thialfihar.android.apg.ui.DecryptActivity" - }; - - return new ShareHelper(getActivity()).createChooserExcluding(prototype, title, blacklist); - } - - private Intent createSendIntent(String text) { - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, text); - sendIntent.setType("text/plain"); - return sendIntent; - } - - private void copyToClipboard(String text) { - ClipboardReflection.copyToClipboard(getActivity(), text); - Notify.create(getActivity(), R.string.text_copied_to_clipboard, Notify.Style.OK).show(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setHasOptionsMenu(true); - - String ciphertext = getArguments().getString(ARG_CIPHERTEXT); - if (ciphertext != null) { - mCiphertext = ciphertext; - cryptoOperation(new CryptoInputParcel()); - } - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - if (mShowMenuOptions) { - inflater.inflate(R.menu.decrypt_menu, menu); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.decrypt_share: { - startActivity(sendWithChooserExcludingDecrypt(mText.getText().toString())); - break; - } - case R.id.decrypt_copy: { - copyToClipboard(mText.getText().toString()); - break; - } - default: { - return super.onOptionsItemSelected(item); - } - } - - return true; - } - - @Override - protected void cryptoOperation(CryptoInputParcel cryptoInput) { - // Send all information needed to service to decrypt in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - - // fill values for this action - Bundle data = new Bundle(); - - intent.setAction(KeychainIntentService.ACTION_DECRYPT_VERIFY); - - // data - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); - data.putInt(KeychainIntentService.TARGET, IOType.BYTES.ordinal()); - data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, mCiphertext.getBytes()); - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); - - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Message is received after encrypting is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_decrypting), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - // handle pending messages - if (handlePendingMessage(message)) { - return; - } - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - - DecryptVerifyResult pgpResult = - returnData.getParcelable(DecryptVerifyResult.EXTRA_RESULT); - - if (pgpResult.success()) { - byte[] decryptedMessage = returnData - .getByteArray(KeychainIntentService.RESULT_DECRYPTED_BYTES); - String displayMessage; - if (pgpResult.getCharset() != null) { - try { - displayMessage = new String(decryptedMessage, pgpResult.getCharset()); - } catch (UnsupportedEncodingException e) { - // if we can't decode properly, just fall back to utf-8 - displayMessage = new String(decryptedMessage); - } - } else { - displayMessage = new String(decryptedMessage); - } - mText.setText(displayMessage); - - // display signature result in activity - loadVerifyResult(pgpResult); - } else { - // TODO: show also invalid layout with different text? - } - pgpResult.createNotify(getActivity()).show(DecryptTextFragment.this); - } - } - }; - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - saveHandler.showProgressDialog(getActivity()); - - // start service with intent - getActivity().startService(intent); - } - - @Override - protected void onVerifyLoaded(boolean hideErrorOverlay) { - mShowMenuOptions = hideErrorOverlay; - getActivity().supportInvalidateOptionsMenu(); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java new file mode 100644 index 000000000..478259133 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.ui; + +import android.app.Activity; +import android.support.v7.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Spinner; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.DeleteResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.operations.results.RevokeResult; +import org.sufficientlysecure.keychain.pgp.KeyRing; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.DeleteKeyringParcel; +import org.sufficientlysecure.keychain.service.RevokeKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.Date; +import java.util.HashMap; + +public class DeleteKeyDialogActivity extends FragmentActivity { + public static final String EXTRA_DELETE_MASTER_KEY_IDS = "extra_delete_master_key_ids"; + public static final String EXTRA_HAS_SECRET = "extra_has_secret"; + public static final String EXTRA_KEYSERVER = "extra_keyserver"; + + private CryptoOperationHelper<DeleteKeyringParcel, DeleteResult> mDeleteOpHelper; + private CryptoOperationHelper<RevokeKeyringParcel, RevokeResult> mRevokeOpHelper; + + private long[] mMasterKeyIds; + private boolean mHasSecret; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mDeleteOpHelper = new CryptoOperationHelper<>(1, DeleteKeyDialogActivity.this, + getDeletionCallback(), R.string.progress_deleting); + + mRevokeOpHelper = new CryptoOperationHelper<>(2, this, + getRevocationCallback(), R.string.progress_revoking_uploading); + + mMasterKeyIds = getIntent().getLongArrayExtra(EXTRA_DELETE_MASTER_KEY_IDS); + mHasSecret = getIntent().getBooleanExtra(EXTRA_HAS_SECRET, false); + + if (mMasterKeyIds.length > 1 && mHasSecret) { + // secret keys can only be deleted individually + OperationResult.OperationLog log = new OperationResult.OperationLog(); + log.add(OperationResult.LogType.MSG_DEL_ERROR_MULTI_SECRET, 0); + returnResult(new DeleteResult(OperationResult.RESULT_ERROR, log, 0, + mMasterKeyIds.length)); + } + + if (mMasterKeyIds.length == 1 && mHasSecret) { + // if mMasterKeyIds.length == 0 we let the DeleteOperation respond + try { + HashMap<String, Object> data = new ProviderHelper(this).getUnifiedData( + mMasterKeyIds[0], new String[]{ + KeychainContract.KeyRings.USER_ID, + KeychainContract.KeyRings.IS_REVOKED + }, new int[]{ + ProviderHelper.FIELD_TYPE_STRING, + ProviderHelper.FIELD_TYPE_INTEGER + } + ); + + String name; + KeyRing.UserId mainUserId = KeyRing.splitUserId( + (String) data.get(KeychainContract.KeyRings.USER_ID)); + if (mainUserId.name != null) { + name = mainUserId.name; + } else { + name = getString(R.string.user_id_no_name); + } + + if ((long) data.get(KeychainContract.KeyRings.IS_REVOKED) > 0) { + showNormalDeleteDialog(); + } else { + showRevokeDeleteDialog(name); + } + } catch (ProviderHelper.NotFoundException e) { + Log.e(Constants.TAG, + "Secret key to delete not found at DeleteKeyDialogActivity for " + + mMasterKeyIds[0], e); + finish(); + } + } else { + showNormalDeleteDialog(); + } + } + + private void showNormalDeleteDialog() { + + DeleteKeyDialogFragment deleteKeyDialogFragment + = DeleteKeyDialogFragment.newInstance(mMasterKeyIds, mHasSecret); + + deleteKeyDialogFragment.show(getSupportFragmentManager(), "deleteKeyDialog"); + + } + + private void showRevokeDeleteDialog(String keyname) { + + RevokeDeleteDialogFragment fragment = RevokeDeleteDialogFragment.newInstance(keyname); + fragment.show(getSupportFragmentManager(), "deleteRevokeDialog"); + } + + private void startRevocationOperation() { + mRevokeOpHelper.cryptoOperation(new CryptoInputParcel(new Date(), false)); + } + + private void startDeletionOperation() { + mDeleteOpHelper.cryptoOperation(); + } + + private CryptoOperationHelper.Callback<RevokeKeyringParcel, RevokeResult> getRevocationCallback() { + + return new CryptoOperationHelper.Callback<RevokeKeyringParcel, RevokeResult>() { + @Override + public RevokeKeyringParcel createOperationInput() { + return new RevokeKeyringParcel(mMasterKeyIds[0], true, + getIntent().getStringExtra(EXTRA_KEYSERVER)); + } + + @Override + public void onCryptoOperationSuccess(RevokeResult result) { + returnResult(result); + } + + @Override + public void onCryptoOperationCancelled() { + setResult(RESULT_CANCELED); + finish(); + } + + @Override + public void onCryptoOperationError(RevokeResult result) { + returnResult(result); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + } + + private CryptoOperationHelper.Callback<DeleteKeyringParcel, DeleteResult> getDeletionCallback() { + + return new CryptoOperationHelper.Callback<DeleteKeyringParcel, DeleteResult>() { + @Override + public DeleteKeyringParcel createOperationInput() { + return new DeleteKeyringParcel(mMasterKeyIds, mHasSecret); + } + + @Override + public void onCryptoOperationSuccess(DeleteResult result) { + returnResult(result); + } + + @Override + public void onCryptoOperationCancelled() { + setResult(RESULT_CANCELED); + finish(); + } + + @Override + public void onCryptoOperationError(DeleteResult result) { + returnResult(result); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + } + + private void returnResult(OperationResult result) { + Intent intent = new Intent(); + intent.putExtra(OperationResult.EXTRA_RESULT, result); + setResult(RESULT_OK, intent); + finish(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + mDeleteOpHelper.handleActivityResult(requestCode, resultCode, data); + mRevokeOpHelper.handleActivityResult(requestCode, resultCode, data); + } + + public static class DeleteKeyDialogFragment extends DialogFragment { + + private static final String ARG_DELETE_MASTER_KEY_IDS = "delete_master_key_ids"; + private static final String ARG_HAS_SECRET = "has_secret"; + + private TextView mMainMessage; + private View mInflateView; + + /** + * Creates new instance of this delete file dialog fragment + */ + public static DeleteKeyDialogFragment newInstance(long[] masterKeyIds, boolean hasSecret) { + DeleteKeyDialogFragment frag = new DeleteKeyDialogFragment(); + Bundle args = new Bundle(); + + args.putLongArray(ARG_DELETE_MASTER_KEY_IDS, masterKeyIds); + args.putBoolean(ARG_HAS_SECRET, hasSecret); + + frag.setArguments(args); + + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final FragmentActivity activity = getActivity(); + + final long[] masterKeyIds = getArguments().getLongArray(ARG_DELETE_MASTER_KEY_IDS); + final boolean hasSecret = getArguments().getBoolean(ARG_HAS_SECRET); + + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); + + CustomAlertDialogBuilder builder = new CustomAlertDialogBuilder(theme); + + // Setup custom View to display in AlertDialog + LayoutInflater inflater = LayoutInflater.from(theme); + mInflateView = inflater.inflate(R.layout.view_key_delete_fragment, null); + builder.setView(mInflateView); + + mMainMessage = (TextView) mInflateView.findViewById(R.id.mainMessage); + + // If only a single key has been selected + if (masterKeyIds.length == 1) { + long masterKeyId = masterKeyIds[0]; + + try { + HashMap<String, Object> data = new ProviderHelper(activity).getUnifiedData( + masterKeyId, new String[]{ + KeychainContract.KeyRings.USER_ID, + KeychainContract.KeyRings.HAS_ANY_SECRET + }, new int[]{ + ProviderHelper.FIELD_TYPE_STRING, + ProviderHelper.FIELD_TYPE_INTEGER + } + ); + String name; + KeyRing.UserId mainUserId = KeyRing.splitUserId((String) data.get(KeychainContract.KeyRings.USER_ID)); + if (mainUserId.name != null) { + name = mainUserId.name; + } else { + name = getString(R.string.user_id_no_name); + } + + if (hasSecret) { + // show title only for secret key deletions, + // see http://www.google.com/design/spec/components/dialogs.html#dialogs-behavior + builder.setTitle(getString(R.string.title_delete_secret_key, name)); + mMainMessage.setText(getString(R.string.secret_key_deletion_confirmation, name)); + } else { + mMainMessage.setText(getString(R.string.public_key_deletetion_confirmation, name)); + } + } catch (ProviderHelper.NotFoundException e) { + dismiss(); + return null; + } + } else { + mMainMessage.setText(R.string.key_deletion_confirmation_multi); + } + + builder.setPositiveButton(R.string.btn_delete, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + ((DeleteKeyDialogActivity) getActivity()).startDeletionOperation(); + } + }); + + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + + return builder.show(); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + getActivity().setResult(RESULT_CANCELED); + getActivity().finish(); + } + } + + public static class RevokeDeleteDialogFragment extends DialogFragment { + + public static final String ARG_KEY_NAME = "arg_key_name"; + + public static RevokeDeleteDialogFragment newInstance(String keyName) { + Bundle args = new Bundle(); + args.putString(ARG_KEY_NAME, keyName); + RevokeDeleteDialogFragment frag = new RevokeDeleteDialogFragment(); + frag.setArguments(args); + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + final String CHOICE_REVOKE = getString(R.string.del_rev_dialog_choice_rev_upload); + final String CHOICE_DELETE = getString(R.string.del_rev_dialog_choice_delete); + + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); + + CustomAlertDialogBuilder builder = new CustomAlertDialogBuilder(theme); + builder.setTitle(getString(R.string.del_rev_dialog_title, + getArguments().get(ARG_KEY_NAME))); + + LayoutInflater inflater = LayoutInflater.from(theme); + View view = inflater.inflate(R.layout.del_rev_dialog, null); + builder.setView(view); + + final Spinner spinner = (Spinner) view.findViewById(R.id.spinner); + + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + + builder.setPositiveButton(R.string.del_rev_dialog_btn_revoke, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + String choice = spinner.getSelectedItem().toString(); + if (choice.equals(CHOICE_REVOKE)) { + ((DeleteKeyDialogActivity) activity) + .startRevocationOperation(); + } else if (choice.equals(CHOICE_DELETE)) { + ((DeleteKeyDialogActivity) activity) + .showNormalDeleteDialog(); + } else { + throw new AssertionError( + "Unsupported delete type in RevokeDeleteDialogFragment"); + } + } + }); + + final AlertDialog alertDialog = builder.show(); + + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) { + + String choice = parent.getItemAtPosition(pos).toString(); + + if (choice.equals(CHOICE_REVOKE)) { + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) + .setText(R.string.del_rev_dialog_btn_revoke); + } else if (choice.equals(CHOICE_DELETE)) { + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) + .setText(R.string.del_rev_dialog_btn_delete); + } else { + throw new AssertionError( + "Unsupported delete type in RevokeDeleteDialogFragment"); + } + } + + public void onNothingSelected(AdapterView<?> parent) { + } + }); + + return alertDialog; + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + getActivity().setResult(RESULT_CANCELED); + getActivity().finish(); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java new file mode 100644 index 000000000..3c8e972b9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2012-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> + * + * 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.ui; + + +import java.io.IOException; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.View; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; +import org.sufficientlysecure.keychain.util.FileHelper; + +public class DisplayTextActivity extends BaseActivity { + + public static final String EXTRA_METADATA = "metadata"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setFullScreenDialogClose(Activity.RESULT_CANCELED, false); + + // Handle intent actions + handleActions(savedInstanceState, getIntent()); + } + + @Override + protected void initLayout() { + setContentView(R.layout.decrypt_text_activity); + } + + /** + * Handles all actions with this intent + */ + private void handleActions(Bundle savedInstanceState, Intent intent) { + if (savedInstanceState != null) { + return; + } + + DecryptVerifyResult result = intent.getParcelableExtra(EXTRA_METADATA); + + String plaintext; + try { + plaintext = FileHelper.readTextFromUri(this, intent.getData(), result.getCharset()); + } catch (IOException e) { + Toast.makeText(this, R.string.error_preparing_data, Toast.LENGTH_LONG).show(); + return; + } + + if (plaintext != null) { + loadFragment(plaintext, result); + } else { + Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); + finish(); + } + } + + private void loadFragment(String plaintext, DecryptVerifyResult result) { + // Create an instance of the fragment + Fragment frag = DisplayTextFragment.newInstance(plaintext, result); + + // Add the fragment to the 'fragment_container' FrameLayout + // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + getSupportFragmentManager().beginTransaction() + .replace(R.id.decrypt_text_fragment_container, frag) + .commitAllowingStateLoss(); + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java new file mode 100644 index 000000000..dc06e9115 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java @@ -0,0 +1,164 @@ +/* + * 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.ui; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.util.ShareHelper; + +public class DisplayTextFragment extends DecryptFragment { + + public static final String ARG_PLAINTEXT = "plaintext"; + + // view + private TextView mText; + + // model (no state to persist though, that's all in arguments!) + private boolean mShowMenuOptions = false; + + public static DisplayTextFragment newInstance(String plaintext, DecryptVerifyResult result) { + DisplayTextFragment frag = new DisplayTextFragment(); + + Bundle args = new Bundle(); + args.putString(ARG_PLAINTEXT, plaintext); + args.putParcelable(ARG_DECRYPT_VERIFY_RESULT, result); + + frag.setArguments(args); + + return frag; + } + + /** + * Create Intent Chooser but exclude decrypt activites + */ + private Intent sendWithChooserExcludingDecrypt(String text) { + Intent prototype = createSendIntent(text); + String title = getString(R.string.title_share_message); + + // we don't want to decrypt the decrypted, no inception ;) + String[] blacklist = new String[]{ + Constants.PACKAGE_NAME + ".ui.DecryptActivity", + "org.thialfihar.android.apg.ui.DecryptActivity" + }; + + return new ShareHelper(getActivity()).createChooserExcluding(prototype, title, blacklist); + } + + private Intent createSendIntent(String text) { + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, text); + sendIntent.setType("text/plain"); + return sendIntent; + } + + private void copyToClipboard(String text) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipMan == null) { + Notify.create(activity, R.string.error_clipboard_copy, Style.ERROR).show(); + return; + } + + clipMan.setPrimaryClip(ClipData.newPlainText(Constants.CLIPBOARD_LABEL, text)); + Notify.create(activity, R.string.text_copied_to_clipboard, Style.OK).show(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.decrypt_text_fragment, container, false); + mText = (TextView) view.findViewById(R.id.decrypt_text_plaintext); + return view; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + Bundle args = getArguments(); + + String plaintext = args.getString(ARG_PLAINTEXT); + DecryptVerifyResult result = args.getParcelable(ARG_DECRYPT_VERIFY_RESULT); + + // display signature result in activity + mText.setText(plaintext); + loadVerifyResult(result); + + } + + @Override + protected void onVerifyLoaded(boolean hideErrorOverlay) { + mShowMenuOptions = hideErrorOverlay; + getActivity().supportInvalidateOptionsMenu(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (mShowMenuOptions) { + inflater.inflate(R.menu.decrypt_menu, menu); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.decrypt_share: { + startActivity(sendWithChooserExcludingDecrypt(mText.getText().toString())); + break; + } + case R.id.decrypt_copy: { + copyToClipboard(mText.getText().toString()); + break; + } + default: { + return super.onOptionsItemSelected(item); + } + } + + return true; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java index 897de8490..07b0a12d3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java @@ -18,7 +18,6 @@ package org.sufficientlysecure.keychain.ui; import android.app.Activity; -import android.app.ProgressDialog; import android.content.Intent; import android.database.Cursor; import android.net.Uri; @@ -42,6 +41,7 @@ import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.SingletonResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; @@ -49,8 +49,6 @@ import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange; @@ -59,15 +57,21 @@ import org.sufficientlysecure.keychain.ui.adapter.SubkeysAdapter; import org.sufficientlysecure.keychain.ui.adapter.SubkeysAddedAdapter; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAddedAdapter; -import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; -import org.sufficientlysecure.keychain.ui.dialog.*; +import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.dialog.AddSubkeyDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.AddUserIdDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.EditSubkeyDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.EditSubkeyExpiryDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.EditUserIdDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; +import java.util.Date; -public class EditKeyFragment extends CryptoOperationFragment implements - LoaderManager.LoaderCallbacks<Cursor> { +public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyringParcel, OperationResult> + implements LoaderManager.LoaderCallbacks<Cursor> { public static final String ARG_DATA_URI = "uri"; public static final String ARG_SAVE_KEYRING_PARCEL = "save_keyring_parcel"; @@ -149,7 +153,7 @@ public class EditKeyFragment extends CryptoOperationFragment implements if (mDataUri == null) { returnKeyringParcel(); } else { - cryptoOperation(new CryptoInputParcel()); + cryptoOperation(new CryptoInputParcel(new Date())); } } }, new OnClickListener() { @@ -190,7 +194,7 @@ public class EditKeyFragment extends CryptoOperationFragment implements private void loadData(Uri dataUri) { mDataUri = dataUri; - Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); + Log.i(Constants.TAG, "mDataUri: " + mDataUri); // load the secret key ring. we do verify here that the passphrase is correct, so cached won't do try { @@ -415,15 +419,71 @@ public class EditKeyFragment extends CryptoOperationFragment implements mSaveKeyringParcel.mRevokeSubKeys.add(keyId); } break; - case EditSubkeyDialogFragment.MESSAGE_STRIP: + case EditSubkeyDialogFragment.MESSAGE_STRIP: { + SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position); + if (secretKeyType == SecretKeyType.GNU_DUMMY) { + // Key is already stripped; this is a no-op. + break; + } + SubkeyChange change = mSaveKeyringParcel.getSubkeyChange(keyId); if (change == null) { - mSaveKeyringParcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, null)); + mSaveKeyringParcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, false)); break; } // toggle change.mDummyStrip = !change.mDummyStrip; + if (change.mDummyStrip && change.mMoveKeyToCard) { + // User had chosen to divert key, but now wants to strip it instead. + change.mMoveKeyToCard = false; + } + break; + } + case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: { + // TODO: enable later when Admin PIN handling is resolved + Notify.create(getActivity(), + "This feature will be available in an upcoming OpenKeychain version.", + Notify.Style.WARN).show(); break; + +// Activity activity = EditKeyFragment.this.getActivity(); +// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position); +// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD || +// secretKeyType == SecretKeyType.GNU_DUMMY) { +// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR) +// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); +// break; +// } +// int algorithm = mSubkeysAdapter.getAlgorithm(position); +// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN +// if (algorithm != 1 && algorithm != 2 && algorithm != 3) { +// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR) +// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); +// break; +// } +// if (mSubkeysAdapter.getKeySize(position) != 2048) { +// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR) +// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); +// break; +// } +// +// +// SubkeyChange change; +// change = mSaveKeyringParcel.getSubkeyChange(keyId); +// if (change == null) { +// mSaveKeyringParcel.mChangeSubKeys.add( +// new SubkeyChange(keyId, false, true) +// ); +// break; +// } +// // toggle +// change.mMoveKeyToCard = !change.mMoveKeyToCard; +// if (change.mMoveKeyToCard && change.mDummyStrip) { +// // User had chosen to strip key, but now wants to divert it. +// change.mDummyStrip = false; +// } +// break; + } } getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad(); } @@ -521,7 +581,7 @@ public class EditKeyFragment extends CryptoOperationFragment implements addSubkeyDialogFragment.show(getActivity().getSupportFragmentManager(), "addSubkeyDialog"); } - private void returnKeyringParcel() { + protected void returnKeyringParcel() { if (mSaveKeyringParcel.mAddUserIds.size() == 0) { Notify.create(getActivity(), R.string.edit_key_error_add_identity, Notify.Style.ERROR).show(); return; @@ -540,76 +600,6 @@ public class EditKeyFragment extends CryptoOperationFragment implements getActivity().finish(); } - @Override - protected void cryptoOperation(CryptoInputParcel cryptoInput) { - - Log.d(Constants.TAG, "cryptoInput:\n" + cryptoInput); - Log.d(Constants.TAG, "mSaveKeyringParcel:\n" + mSaveKeyringParcel); - - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_saving), - ProgressDialog.STYLE_HORIZONTAL, - true, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (handlePendingMessage(message)) { - return; - } - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - - // get returned data bundle - Bundle returnData = message.getData(); - if (returnData == null) { - return; - } - final OperationResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null) { - return; - } - - // if bad -> display here! - if (!result.success()) { - result.createNotify(getActivity()).show(); - return; - } - - // if good -> finish, return result to showkey and display there! - Intent intent = new Intent(); - intent.putExtra(OperationResult.EXTRA_RESULT, result); - getActivity().setResult(EditKeyActivity.RESULT_OK, intent); - getActivity().finish(); - - } - } - }; - - // Send all information needed to service to import key in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_EDIT_KEYRING); - - // fill values for this action - Bundle data = new Bundle(); - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); - data.putParcelable(KeychainIntentService.EDIT_KEYRING_PARCEL, mSaveKeyringParcel); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - saveHandler.showProgressDialog(getActivity()); - - // start service with intent - getActivity().startService(intent); - } - /** * Closes this activity, returning a result parcel with a single error log entry. */ @@ -624,4 +614,23 @@ public class EditKeyFragment extends CryptoOperationFragment implements getActivity().finish(); } + @Override + public SaveKeyringParcel createOperationInput() { + return mSaveKeyringParcel; + } + + @Override + public void onQueuedOperationSuccess(OperationResult result) { + + // null-protected from Queueing*Fragment + Activity activity = getActivity(); + + // if good -> finish, return result to showkey and display there! + Intent intent = new Intent(); + intent.putExtra(OperationResult.EXTRA_RESULT, result); + activity.setResult(EditKeyActivity.RESULT_OK, intent); + activity.finish(); + + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java new file mode 100644 index 000000000..4361705f9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * 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.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuItem; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; + +public class EncryptActivity extends BaseActivity { + + // preselect ids, for internal use + public static final String EXTRA_SIGNATURE_KEY_ID = Constants.EXTRA_PREFIX + "EXTRA_SIGNATURE_KEY_ID"; + public static final String EXTRA_ENCRYPTION_KEY_IDS = Constants.EXTRA_PREFIX + "EXTRA_SIGNATURE_KEY_IDS"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + + if (extras == null) { + extras = new Bundle(); + } + + if (savedInstanceState == null) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + + // preselect keys given by intent + long signingKeyId = extras.getLong(EXTRA_SIGNATURE_KEY_ID); + long[] encryptionKeyIds = extras.getLongArray(EXTRA_ENCRYPTION_KEY_IDS); + + Fragment modeFragment = EncryptModeAsymmetricFragment.newInstance(signingKeyId, encryptionKeyIds); + transaction.replace(R.id.encrypt_mode_container, modeFragment); + transaction.commit(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.check_use_symmetric: { + item.setChecked(!item.isChecked()); + setModeFragment(item.isChecked()); + return true; + } + default: { + return super.onOptionsItemSelected(item); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.encrypt_activity, menu); + + Fragment frag = + getSupportFragmentManager().findFragmentById(R.id.encrypt_mode_container); + boolean isSymmetric = frag instanceof EncryptModeSymmetricFragment; + menu.findItem(R.id.check_use_symmetric).setChecked(isSymmetric); + + return super.onCreateOptionsMenu(menu); + } + + private void setModeFragment(boolean symmetric) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace(R.id.encrypt_mode_container, + symmetric + ? EncryptModeSymmetricFragment.newInstance() + : EncryptModeAsymmetricFragment.newInstance(0, null) + ); + + // doesn't matter if the user doesn't look at the activity + transaction.commitAllowingStateLoss(); + } + + public EncryptModeFragment getModeFragment() { + return (EncryptModeFragment) + getSupportFragmentManager().findFragmentById(R.id.encrypt_mode_container); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java index a6fad8881..84660ca7a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java @@ -18,27 +18,36 @@ package org.sufficientlysecure.keychain.ui; + +import java.util.regex.Matcher; + +import android.app.Activity; import android.content.Intent; +import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.PgpHelper; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker; - -import java.util.regex.Matcher; +import org.sufficientlysecure.keychain.util.FileHelper; public class EncryptDecryptOverviewFragment extends Fragment { View mClipboardIcon; + private static final int REQUEST_CODE_INPUT = 0x00007003; + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -74,31 +83,44 @@ public class EncryptDecryptOverviewFragment extends Fragment { mDecryptFile.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Intent filesDecrypt = new Intent(getActivity(), DecryptFilesActivity.class); - filesDecrypt.setAction(DecryptFilesActivity.ACTION_DECRYPT_DATA_OPEN); - startActivity(filesDecrypt); + FileHelper.openDocument(EncryptDecryptOverviewFragment.this, null, "*/*", false, REQUEST_CODE_INPUT); } }); mDecryptFromClipboard.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Intent clipboardDecrypt = new Intent(getActivity(), DecryptTextActivity.class); - clipboardDecrypt.setAction(DecryptTextActivity.ACTION_DECRYPT_FROM_CLIPBOARD); - startActivityForResult(clipboardDecrypt, 0); + decryptFromClipboard(); } }); return view; } + private void decryptFromClipboard() { + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + final CharSequence clipboardText = ClipboardReflection.getClipboardText(activity); + if (clipboardText == null || TextUtils.isEmpty(clipboardText)) { + Notify.create(activity, R.string.error_clipboard_empty, Style.ERROR).show(); + return; + } + + Intent clipboardDecrypt = new Intent(getActivity(), DecryptActivity.class); + clipboardDecrypt.setAction(DecryptActivity.ACTION_DECRYPT_FROM_CLIPBOARD); + startActivityForResult(clipboardDecrypt, 0); + } + @Override public void onResume() { super.onResume(); // get text from clipboard - final CharSequence clipboardText = - ClipboardReflection.getClipboardText(getActivity()); + final CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity()); // if it's null, nothing to do here /o/ if (clipboardText == null) { @@ -135,12 +157,23 @@ public class EncryptDecryptOverviewFragment extends Fragment { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - // if a result has been returned, display a notify - if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { - OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); - result.createNotify(getActivity()).show(); - } else { - super.onActivityResult(requestCode, resultCode, data); + if (requestCode != REQUEST_CODE_INPUT) { + return; + } + + if (resultCode == Activity.RESULT_OK && data != null) { + Uri uri = data.getData(); + if (uri == null) { + Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR).show(); + return; + } + + Intent intent = new Intent(getActivity(), DecryptActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(uri); + startActivity(intent); + } } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesActivity.java index b3ec60890..136787984 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesActivity.java @@ -22,76 +22,37 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.view.View; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; -import org.sufficientlysecure.keychain.ui.base.BaseActivity; -import org.sufficientlysecure.keychain.util.Passphrase; import java.util.ArrayList; -public class EncryptFilesActivity extends BaseActivity implements - EncryptModeAsymmetricFragment.IAsymmetric, EncryptModeSymmetricFragment.ISymmetric, - EncryptFilesFragment.IMode { +public class EncryptFilesActivity extends EncryptActivity { - /* Intents */ + // Intents public static final String ACTION_ENCRYPT_DATA = OpenKeychainIntents.ENCRYPT_DATA; // enables ASCII Armor for file encryption when uri is given public static final String EXTRA_ASCII_ARMOR = OpenKeychainIntents.ENCRYPT_EXTRA_ASCII_ARMOR; - // preselect ids, for internal use - public static final String EXTRA_SIGNATURE_KEY_ID = Constants.EXTRA_PREFIX + "EXTRA_SIGNATURE_KEY_ID"; - public static final String EXTRA_ENCRYPTION_KEY_IDS = Constants.EXTRA_PREFIX + "EXTRA_ENCRYPTION_IDS"; - - Fragment mModeFragment; - EncryptFilesFragment mEncryptFragment; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setFullScreenDialogClose(new View.OnClickListener() { - @Override - public void onClick(View v) { - finish(); - } - }, false); - - // Handle intent actions - handleActions(getIntent(), savedInstanceState); - } + setFullScreenDialogClose(Activity.RESULT_OK, false); - @Override - protected void initLayout() { - setContentView(R.layout.encrypt_files_activity); - } - - /** - * Handles all actions with this intent - */ - private void handleActions(Intent intent, Bundle savedInstanceState) { + Intent intent = getIntent(); String action = intent.getAction(); - Bundle extras = intent.getExtras(); String type = intent.getType(); ArrayList<Uri> uris = new ArrayList<>(); - if (extras == null) { - extras = new Bundle(); - } - if (intent.getData() != null) { uris.add(intent.getData()); } - /* - * Android's Action - */ - // When sending to OpenKeychain Encrypt via share menu if (Intent.ACTION_SEND.equals(action) && type != null) { // Files via content provider, override uri and action @@ -103,56 +64,19 @@ public class EncryptFilesActivity extends BaseActivity implements uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); } - long mSigningKeyId = extras.getLong(EXTRA_SIGNATURE_KEY_ID); - long[] mEncryptionKeyIds = extras.getLongArray(EXTRA_ENCRYPTION_KEY_IDS); - boolean useArmor = extras.getBoolean(EXTRA_ASCII_ARMOR, false); - if (savedInstanceState == null) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - mModeFragment = EncryptModeAsymmetricFragment.newInstance(mSigningKeyId, mEncryptionKeyIds); - transaction.replace(R.id.encrypt_mode_container, mModeFragment, "mode"); - - mEncryptFragment = EncryptFilesFragment.newInstance(uris, useArmor); - transaction.replace(R.id.encrypt_file_container, mEncryptFragment, "files"); - + EncryptFilesFragment encryptFragment = EncryptFilesFragment.newInstance(uris); + transaction.replace(R.id.encrypt_file_container, encryptFragment); transaction.commit(); - - getSupportFragmentManager().executePendingTransactions(); } - } - - @Override - public void onModeChanged(boolean symmetric) { - // switch fragments - getSupportFragmentManager().beginTransaction() - .replace(R.id.encrypt_mode_container, - symmetric - ? EncryptModeSymmetricFragment.newInstance() - : EncryptModeAsymmetricFragment.newInstance(0, null) - ) - .commitAllowingStateLoss(); - getSupportFragmentManager().executePendingTransactions(); - } - - @Override - public void onSignatureKeyIdChanged(long signatureKeyId) { - mEncryptFragment.setSigningKeyId(signatureKeyId); - } - @Override - public void onEncryptionKeyIdsChanged(long[] encryptionKeyIds) { - mEncryptFragment.setEncryptionKeyIds(encryptionKeyIds); } @Override - public void onEncryptionUserIdsChanged(String[] encryptionUserIds) { - mEncryptFragment.setEncryptionUserIds(encryptionUserIds); - } - - @Override - public void onPassphraseChanged(Passphrase passphrase) { - mEncryptFragment.setPassphrase(passphrase); + protected void initLayout() { + setContentView(R.layout.encrypt_files_activity); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java index 458810541..8572a5712 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java @@ -17,8 +17,17 @@ package org.sufficientlysecure.keychain.ui; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import android.app.Activity; -import android.app.ProgressDialog; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -26,8 +35,7 @@ import android.graphics.Point; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; +import android.support.v4.app.FragmentActivity; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -41,106 +49,74 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; -import org.spongycastle.bcpg.CompressionAlgorithmTags; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.PgpConstants; +import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.adapter.SpacesItemDecoration; -import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.base.CachingCryptoOperationFragment; import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; +import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.ShareHelper; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public class EncryptFilesFragment extends CryptoOperationFragment { - - public interface IMode { - public void onModeChanged(boolean symmetric); - } +public class EncryptFilesFragment + extends CachingCryptoOperationFragment<SignEncryptParcel, SignEncryptResult> { + public static final String ARG_DELETE_AFTER_ENCRYPT = "delete_after_encrypt"; + public static final String ARG_ENCRYPT_FILENAMES = "encrypt_filenames"; + public static final String ARG_USE_COMPRESSION = "use_compression"; public static final String ARG_USE_ASCII_ARMOR = "use_ascii_armor"; public static final String ARG_URIS = "uris"; - private static final int REQUEST_CODE_INPUT = 0x00007003; + public static final int REQUEST_CODE_INPUT = 0x00007003; private static final int REQUEST_CODE_OUTPUT = 0x00007007; - private IMode mModeInterface; - - private boolean mSymmetricMode = false; - private boolean mUseArmor = false; - private boolean mUseCompression = true; - private boolean mDeleteAfterEncrypt = false; - private boolean mShareAfterEncrypt = false; - private boolean mEncryptFilenames = true; + private boolean mUseArmor; + private boolean mUseCompression; + private boolean mDeleteAfterEncrypt; + private boolean mEncryptFilenames; private boolean mHiddenRecipients = false; - private long mEncryptionKeyIds[] = null; - private String mEncryptionUserIds[] = null; - private long mSigningKeyId = Constants.key.none; - private Passphrase mPassphrase = new Passphrase(); + private AfterEncryptAction mAfterEncryptAction; + private enum AfterEncryptAction { + SAVE, SHARE, COPY; + } - private ArrayList<Uri> mOutputUris = new ArrayList<>(); + private ArrayList<Uri> mOutputUris; private RecyclerView mSelectedFiles; - ArrayList<FilesAdapter.ViewModel> mFilesModels; FilesAdapter mFilesAdapter; /** * Creates new instance of this fragment */ - public static EncryptFilesFragment newInstance(ArrayList<Uri> uris, boolean useArmor) { + public static EncryptFilesFragment newInstance(ArrayList<Uri> uris) { EncryptFilesFragment frag = new EncryptFilesFragment(); Bundle args = new Bundle(); - args.putBoolean(ARG_USE_ASCII_ARMOR, useArmor); args.putParcelableArrayList(ARG_URIS, uris); frag.setArguments(args); return frag; } - public void setEncryptionKeyIds(long[] encryptionKeyIds) { - mEncryptionKeyIds = encryptionKeyIds; - } - - public void setEncryptionUserIds(String[] encryptionUserIds) { - mEncryptionUserIds = encryptionUserIds; - } - - public void setSigningKeyId(long signingKeyId) { - mSigningKeyId = signingKeyId; - } - - public void setPassphrase(Passphrase passphrase) { - mPassphrase = passphrase; - } - @Override public void onAttach(Activity activity) { super.onAttach(activity); - try { - mModeInterface = (IMode) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity + " must be IMode"); + if ( ! (activity instanceof EncryptActivity) ) { + throw new AssertionError(activity + " must inherit from EncryptionActivity"); } } @@ -158,38 +134,70 @@ public class EncryptFilesFragment extends CryptoOperationFragment { mSelectedFiles.setLayoutManager(new LinearLayoutManager(getActivity())); mSelectedFiles.setItemAnimator(new DefaultItemAnimator()); - mFilesModels = new ArrayList<>(); - mFilesAdapter = new FilesAdapter(getActivity(), mFilesModels, new View.OnClickListener() { + mFilesAdapter = new FilesAdapter(getActivity(), new View.OnClickListener() { @Override public void onClick(View v) { addInputUri(); } }); - ArrayList<Uri> inputUris = getArguments().getParcelableArrayList(ARG_URIS); + Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState; + + ArrayList<Uri> inputUris = args.getParcelableArrayList(ARG_URIS); if (inputUris != null) { mFilesAdapter.addAll(inputUris); } - mUseArmor = getArguments().getBoolean(ARG_USE_ASCII_ARMOR); mSelectedFiles.setAdapter(mFilesAdapter); return view; } @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(ARG_DELETE_AFTER_ENCRYPT, mDeleteAfterEncrypt); + outState.putBoolean(ARG_USE_ASCII_ARMOR, mUseArmor); + outState.putBoolean(ARG_USE_COMPRESSION, mUseCompression); + outState.putBoolean(ARG_ENCRYPT_FILENAMES, mEncryptFilenames); + + outState.putParcelableArrayList(ARG_URIS, mFilesAdapter.getAsArrayList()); + } + + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + Preferences prefs = Preferences.getPreferences(getActivity()); + + Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState; + mDeleteAfterEncrypt = args.getBoolean(ARG_DELETE_AFTER_ENCRYPT, false); + + if (args.containsKey(ARG_USE_ASCII_ARMOR)) { + mUseArmor = args.getBoolean(ARG_USE_ASCII_ARMOR, false); + } else { + mUseArmor = prefs.getUseArmor(); + } + + if (args.containsKey(ARG_USE_COMPRESSION)) { + mUseCompression = args.getBoolean(ARG_USE_COMPRESSION, true); + } else { + mUseCompression = prefs.getFilesUseCompression(); + } + + if (args.containsKey(ARG_ENCRYPT_FILENAMES)) { + mEncryptFilenames = args.getBoolean(ARG_ENCRYPT_FILENAMES, true); + } else { + mEncryptFilenames = prefs.getEncryptFilenames(); + } + setHasOptionsMenu(true); } private void addInputUri() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - FileHelper.openDocument(EncryptFilesFragment.this, "*/*", true, REQUEST_CODE_INPUT); - } else { - FileHelper.openFile(EncryptFilesFragment.this, mFilesModels.isEmpty() ? - null : mFilesModels.get(mFilesModels.size() - 1).inputUri, - "*/*", REQUEST_CODE_INPUT); - } + FileHelper.openDocument(EncryptFilesFragment.this, mFilesAdapter.getModelCount() == 0 ? + null : mFilesAdapter.getModelItem(mFilesAdapter.getModelCount() - 1).inputUri, + "*/*", true, REQUEST_CODE_INPUT); } private void addInputUri(Uri inputUri) { @@ -209,49 +217,16 @@ public class EncryptFilesFragment extends CryptoOperationFragment { } private void showOutputFileDialog() { - if (mFilesModels.size() > 1 || mFilesModels.isEmpty()) { + if (mFilesAdapter.getModelCount() != 1) { throw new IllegalStateException(); } - FilesAdapter.ViewModel model = mFilesModels.get(0); + FilesAdapter.ViewModel model = mFilesAdapter.getModelItem(0); String targetName = (mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri)) + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - File file = new File(model.inputUri.getPath()); - File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR; - File targetFile = new File(parentDir, targetName); - FileHelper.saveFile(this, getString(R.string.title_encrypt_to_file), - getString(R.string.specify_file_to_encrypt_to), targetFile, REQUEST_CODE_OUTPUT); - } else { - FileHelper.saveDocument(this, "*/*", targetName, REQUEST_CODE_OUTPUT); - } - } - - private void encryptClicked(boolean share) { - if (mFilesModels.isEmpty()) { - Notify.create(getActivity(), R.string.error_no_file_selected, - Notify.Style.ERROR).show(this); - return; - } - if (share) { - mOutputUris.clear(); - int filenameCounter = 1; - for (FilesAdapter.ViewModel model : mFilesModels) { - String targetName = - (mEncryptFilenames ? String.valueOf(filenameCounter) : FileHelper.getFilename(getActivity(), model.inputUri)) - + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); - mOutputUris.add(TemporaryStorageProvider.createFile(getActivity(), targetName)); - filenameCounter++; - } - startEncrypt(true); - } else { - if (mFilesModels.size() > 1) { - Notify.create(getActivity(), R.string.error_multi_not_supported, - Notify.Style.ERROR).show(this); - return; - } - showOutputFileDialog(); - } + Uri inputUri = model.inputUri; + FileHelper.saveDocument(this, targetName, inputUri, + R.string.title_encrypt_to_file, R.string.specify_file_to_encrypt_to, REQUEST_CODE_OUTPUT); } public void addFile(Intent data) { @@ -276,41 +251,49 @@ public class EncryptFilesFragment extends CryptoOperationFragment { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.encrypt_file_fragment, menu); + + menu.findItem(R.id.check_delete_after_encrypt).setChecked(mDeleteAfterEncrypt); + menu.findItem(R.id.check_use_armor).setChecked(mUseArmor); + menu.findItem(R.id.check_enable_compression).setChecked(mUseCompression); + menu.findItem(R.id.check_encrypt_filenames).setChecked(mEncryptFilenames); } @Override public boolean onOptionsItemSelected(MenuItem item) { - if (item.isCheckable()) { - item.setChecked(!item.isChecked()); - } switch (item.getItemId()) { case R.id.encrypt_save: { - encryptClicked(false); + hideKeyboard(); + mAfterEncryptAction = AfterEncryptAction.SAVE; + cryptoOperation(new CryptoInputParcel(new Date())); break; } case R.id.encrypt_share: { - encryptClicked(true); + hideKeyboard(); + mAfterEncryptAction = AfterEncryptAction.SHARE; + cryptoOperation(new CryptoInputParcel(new Date())); break; } - case R.id.check_use_symmetric: { - mSymmetricMode = item.isChecked(); - mModeInterface.onModeChanged(mSymmetricMode); + case R.id.encrypt_copy: { + hideKeyboard(); + mAfterEncryptAction = AfterEncryptAction.COPY; + cryptoOperation(new CryptoInputParcel(new Date())); break; } case R.id.check_use_armor: { - mUseArmor = item.isChecked(); + toggleUseArmor(item, !item.isChecked()); break; } case R.id.check_delete_after_encrypt: { + item.setChecked(!item.isChecked()); mDeleteAfterEncrypt = item.isChecked(); break; } case R.id.check_enable_compression: { - mUseCompression = item.isChecked(); + toggleEnableCompression(item, !item.isChecked()); break; } case R.id.check_encrypt_filenames: { - mEncryptFilenames = item.isChecked(); + toggleEncryptFilenamesCheck(item, !item.isChecked()); break; } // case R.id.check_hidden_recipients: { @@ -325,111 +308,282 @@ public class EncryptFilesFragment extends CryptoOperationFragment { return true; } - protected boolean inputIsValid() { - // file checks + public void toggleUseArmor(MenuItem item, final boolean useArmor) { - if (mFilesModels.isEmpty()) { - Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR) - .show(this); - return false; - } else if (mFilesModels.size() > 1 && !mShareAfterEncrypt) { - Log.e(Constants.TAG, "Aborting: mInputUris.size() > 1 && !mShareAfterEncrypt"); - // This should be impossible... - return false; - } else if (mFilesModels.size() != mOutputUris.size()) { - Log.e(Constants.TAG, "Aborting: mInputUris.size() != mOutputUris.size()"); - // This as well - return false; - } + mUseArmor = useArmor; + item.setChecked(useArmor); - if (mSymmetricMode) { - // symmetric encryption checks + Notify.create(getActivity(), useArmor + ? R.string.snack_armor_on + : R.string.snack_armor_off, + Notify.LENGTH_LONG, Style.OK, new ActionListener() { + @Override + public void onAction() { + Preferences.getPreferences(getActivity()).setUseArmor(useArmor); + Notify.create(getActivity(), useArmor + ? R.string.snack_armor_on + : R.string.snack_armor_off, + Notify.LENGTH_SHORT, Style.OK, null, R.string.btn_saved) + .show(EncryptFilesFragment.this, false); + } + }, R.string.btn_save_default).show(this); - if (mPassphrase == null) { - Notify.create(getActivity(), R.string.passphrases_do_not_match, Notify.Style.ERROR) - .show(this); - return false; - } - if (mPassphrase.isEmpty()) { - Notify.create(getActivity(), R.string.passphrase_must_not_be_empty, Notify.Style.ERROR) - .show(this); - return false; - } + } - } else { - // asymmetric encryption checks + public void toggleEnableCompression(MenuItem item, final boolean compress) { - boolean gotEncryptionKeys = (mEncryptionKeyIds != null - && mEncryptionKeyIds.length > 0); + mUseCompression = compress; + item.setChecked(compress); - // Files must be encrypted, only text can be signed-only right now - if (!gotEncryptionKeys) { - Notify.create(getActivity(), R.string.select_encryption_key, Notify.Style.ERROR) - .show(this); - return false; + Notify.create(getActivity(), compress + ? R.string.snack_compression_on + : R.string.snack_compression_off, + Notify.LENGTH_LONG, Style.OK, new ActionListener() { + @Override + public void onAction() { + Preferences.getPreferences(getActivity()).setFilesUseCompression(compress); + Notify.create(getActivity(), compress + ? R.string.snack_compression_on + : R.string.snack_compression_off, + Notify.LENGTH_SHORT, Style.OK, null, R.string.btn_saved) + .show(EncryptFilesFragment.this, false); + } + }, R.string.btn_save_default).show(this); + + } + + public void toggleEncryptFilenamesCheck(MenuItem item, final boolean encryptFilenames) { + + mEncryptFilenames = encryptFilenames; + item.setChecked(encryptFilenames); + + Notify.create(getActivity(), encryptFilenames + ? R.string.snack_encrypt_filenames_on + : R.string.snack_encrypt_filenames_off, + Notify.LENGTH_LONG, Style.OK, new ActionListener() { + @Override + public void onAction() { + Preferences.getPreferences(getActivity()).setEncryptFilenames(encryptFilenames); + Notify.create(getActivity(), encryptFilenames + ? R.string.snack_encrypt_filenames_on + : R.string.snack_encrypt_filenames_off, + Notify.LENGTH_SHORT, Style.OK, null, R.string.btn_saved) + .show(EncryptFilesFragment.this, false); } - } - return true; + }, R.string.btn_save_default).show(this); + } - public void onEncryptSuccess(final SignEncryptResult result) { + @Override + public void onQueuedOperationSuccess(final SignEncryptResult result) { + super.onQueuedOperationSuccess(result); + + hideKeyboard(); + + // protected by Queueing*Fragment + FragmentActivity activity = getActivity(); + if (mDeleteAfterEncrypt) { + // TODO make behavior coherent here DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment.newInstance(mFilesAdapter.getAsArrayList()); deleteFileDialog.setOnDeletedListener(new DeleteFileDialogFragment.OnDeletedListener() { @Override public void onDeleted() { - if (mShareAfterEncrypt) { + if (mAfterEncryptAction == AfterEncryptAction.SHARE) { // Share encrypted message/file startActivity(sendWithChooserExcludingEncrypt()); } else { + Activity activity = getActivity(); + if (activity == null) { + // it's gone, there's nothing we can do here + return; + } // Save encrypted file - result.createNotify(getActivity()).show(); + result.createNotify(activity).show(); } } }); - deleteFileDialog.show(getActivity().getSupportFragmentManager(), "deleteDialog"); + deleteFileDialog.show(activity.getSupportFragmentManager(), "deleteDialog"); } else { - if (mShareAfterEncrypt) { - // Share encrypted message/file - startActivity(sendWithChooserExcludingEncrypt()); - } else { - // Save encrypted file - result.createNotify(getActivity()).show(); + + switch (mAfterEncryptAction) { + + case SHARE: + // Share encrypted message/file + startActivity(sendWithChooserExcludingEncrypt()); + break; + + case COPY: + + ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipMan == null) { + Notify.create(activity, R.string.error_clipboard_copy, Style.ERROR).show(); + break; + } + ClipData clip = new ClipData(getString(R.string.label_clip_title), + // make available as application/pgp-encrypted + new String[] { "text/plain" }, + new ClipData.Item(mOutputUris.get(0)) + ); + clipMan.setPrimaryClip(clip); + result.createNotify(activity).show(); + break; + + case SAVE: + // Encrypted file was saved already, just show notification + result.createNotify(activity).show(); + break; } } + + } + + // prepares mOutputUris, either directly and returns false, or indirectly + // which returns true and will call cryptoOperation after mOutputUris has + // been set at a later point. + private boolean prepareOutputStreams() { + + switch (mAfterEncryptAction) { + default: + case SHARE: + mOutputUris = new ArrayList<>(); + int filenameCounter = 1; + for (FilesAdapter.ViewModel model : mFilesAdapter.mDataset) { + String targetName = (mEncryptFilenames + ? String.valueOf(filenameCounter) : FileHelper.getFilename(getActivity(), model.inputUri)) + + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); + mOutputUris.add(TemporaryStorageProvider.createFile(getActivity(), targetName)); + filenameCounter++; + } + return false; + + case SAVE: + if (mFilesAdapter.getModelCount() > 1) { + Notify.create(getActivity(), R.string.error_multi_files, Notify.Style.ERROR).show(this); + return true; + } + showOutputFileDialog(); + return true; + + case COPY: + // nothing to do here, but make sure + if (mFilesAdapter.getModelCount() > 1) { + Notify.create(getActivity(), R.string.error_multi_clipboard, Notify.Style.ERROR).show(this); + return true; + } + mOutputUris = new ArrayList<>(); + String targetName = (mEncryptFilenames + ? String.valueOf(1) : FileHelper.getFilename(getActivity(), + mFilesAdapter.getModelItem(0).inputUri)) + Constants.FILE_EXTENSION_ASC; + mOutputUris.add(TemporaryStorageProvider.createFile(getActivity(), targetName, "text/plain")); + return false; + } + } - protected SignEncryptParcel createEncryptBundle() { + public SignEncryptParcel createOperationInput() { + + SignEncryptParcel actionsParcel = getCachedActionsParcel(); + + // we have three cases here: nothing cached, cached except output, fully cached + if (actionsParcel == null) { + + // clear output uris for now, they will be created by prepareOutputStreams later + mOutputUris = null; + + actionsParcel = createIncompleteCryptoInput(); + // this is null if invalid, just return in that case + if (actionsParcel == null) { + return null; + } + + cacheActionsParcel(actionsParcel); + + } + + // if it's incomplete, prepare output streams + if (actionsParcel.isIncomplete()) { + // if this is still null, prepare output streams again + if (mOutputUris == null) { + // this may interrupt the flow, and call us again from onActivityResult + if (prepareOutputStreams()) { + return null; + } + } + + actionsParcel.addOutputUris(mOutputUris); + cacheActionsParcel(actionsParcel); + + } + + return actionsParcel; + + } + + protected SignEncryptParcel createIncompleteCryptoInput() { + + if (mFilesAdapter.getModelCount() == 0) { + Notify.create(getActivity(), R.string.error_no_file_selected, Notify.Style.ERROR).show(this); + return null; + } + // fill values for this action SignEncryptParcel data = new SignEncryptParcel(); data.addInputUris(mFilesAdapter.getAsArrayList()); - data.addOutputUris(mOutputUris); if (mUseCompression) { - data.setCompressionId(PgpConstants.sPreferredCompressionAlgorithms.get(0)); + data.setCompressionAlgorithm( + PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.USE_DEFAULT); } else { - data.setCompressionId(CompressionAlgorithmTags.UNCOMPRESSED); + data.setCompressionAlgorithm( + PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.UNCOMPRESSED); } data.setHiddenRecipients(mHiddenRecipients); - data.setEnableAsciiArmorOutput(mUseArmor); - data.setSymmetricEncryptionAlgorithm(PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED); - data.setSignatureHashAlgorithm(PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED); + data.setEnableAsciiArmorOutput(mAfterEncryptAction == AfterEncryptAction.COPY || mUseArmor); + data.setSymmetricEncryptionAlgorithm( + PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT); + data.setSignatureHashAlgorithm( + PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT); + + EncryptActivity encryptActivity = (EncryptActivity) getActivity(); + EncryptModeFragment modeFragment = encryptActivity.getModeFragment(); - if (mSymmetricMode) { - Log.d(Constants.TAG, "Symmetric encryption enabled!"); - Passphrase passphrase = mPassphrase; + if (modeFragment.isAsymmetric()) { + long[] encryptionKeyIds = modeFragment.getAsymmetricEncryptionKeyIds(); + long signingKeyId = modeFragment.getAsymmetricSigningKeyId(); + + boolean gotEncryptionKeys = (encryptionKeyIds != null && encryptionKeyIds.length > 0); + + if (!gotEncryptionKeys && signingKeyId != 0) { + Notify.create(getActivity(), R.string.error_detached_signature, Notify.Style.ERROR).show(this); + return null; + } + if (!gotEncryptionKeys) { + Notify.create(getActivity(), R.string.select_encryption_key, Notify.Style.ERROR).show(this); + return null; + } + + data.setEncryptionMasterKeyIds(encryptionKeyIds); + data.setSignatureMasterKeyId(signingKeyId); + } else { + Passphrase passphrase = modeFragment.getSymmetricPassphrase(); + if (passphrase == null) { + Notify.create(getActivity(), R.string.passphrases_do_not_match, Notify.Style.ERROR) + .show(this); + return null; + } if (passphrase.isEmpty()) { - passphrase = null; + Notify.create(getActivity(), R.string.passphrase_must_not_be_empty, Notify.Style.ERROR) + .show(this); + return null; } data.setSymmetricPassphrase(passphrase); - } else { - data.setEncryptionMasterKeyIds(mEncryptionKeyIds); - data.setSignatureMasterKeyId(mSigningKeyId); } + return data; } @@ -442,7 +596,7 @@ public class EncryptFilesFragment extends CryptoOperationFragment { // we don't want to encrypt the encrypted, no inception ;) String[] blacklist = new String[]{ - Constants.PACKAGE_NAME + ".ui.EncryptFileActivity", + Constants.PACKAGE_NAME + ".ui.EncryptFilesActivity", "org.thialfihar.android.apg.ui.EncryptActivity" }; @@ -461,81 +615,28 @@ public class EncryptFilesFragment extends CryptoOperationFragment { } sendIntent.setType(Constants.ENCRYPTED_FILES_MIME); - if (!mSymmetricMode && mEncryptionUserIds != null) { - Set<String> users = new HashSet<>(); - for (String user : mEncryptionUserIds) { - KeyRing.UserId userId = KeyRing.splitUserId(user); - if (userId.email != null) { - users.add(userId.email); - } - } - sendIntent.putExtra(Intent.EXTRA_EMAIL, users.toArray(new String[users.size()])); + EncryptActivity modeInterface = (EncryptActivity) getActivity(); + EncryptModeFragment modeFragment = modeInterface.getModeFragment(); + if (!modeFragment.isAsymmetric()) { + return sendIntent; } - return sendIntent; - } - - public void startEncrypt(boolean share) { - mShareAfterEncrypt = share; - cryptoOperation(); - } - @Override - protected void cryptoOperation(CryptoInputParcel cryptoInput) { - - if (!inputIsValid()) { - // Notify was created by inputIsValid. - Log.d(Constants.TAG, "Input not valid!"); - return; + String[] encryptionUserIds = modeFragment.getAsymmetricEncryptionUserIds(); + if (encryptionUserIds == null) { + return sendIntent; } - Log.d(Constants.TAG, "Input valid!"); - - // Send all information needed to service to edit key in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_SIGN_ENCRYPT); - - final SignEncryptParcel input = createEncryptBundle(); - - Bundle data = new Bundle(); - data.putParcelable(KeychainIntentService.SIGN_ENCRYPT_PARCEL, input); - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Message is received after encrypting is done in KeychainIntentService - ServiceProgressHandler serviceHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_encrypting), - ProgressDialog.STYLE_HORIZONTAL, - true, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - // handle pending messages - if (handlePendingMessage(message)) { - return; - } - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - SignEncryptResult result = - message.getData().getParcelable(SignEncryptResult.EXTRA_RESULT); - if (result.success()) { - onEncryptSuccess(result); - } else { - result.createNotify(getActivity()).show(); - } - } + Set<String> users = new HashSet<>(); + for (String user : encryptionUserIds) { + KeyRing.UserId userId = KeyRing.splitUserId(user); + if (userId.email != null) { + users.add(userId.email); } - }; - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(serviceHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - serviceHandler.showProgressDialog(getActivity()); + } + // pass trough email addresses as extra for email applications + sendIntent.putExtra(Intent.EXTRA_EMAIL, users.toArray(new String[users.size()])); - // start service with intent - getActivity().startService(intent); + return sendIntent; } @Override @@ -550,9 +651,11 @@ public class EncryptFilesFragment extends CryptoOperationFragment { case REQUEST_CODE_OUTPUT: { // This happens after output file was selected, so start our operation if (resultCode == Activity.RESULT_OK && data != null) { - mOutputUris.clear(); + mOutputUris = new ArrayList<>(1); mOutputUris.add(data.getData()); - startEncrypt(false); + // make sure this is correct at this point + mAfterEncryptAction = AfterEncryptAction.SAVE; + cryptoOperation(new CryptoInputParcel(new Date())); } return; } @@ -640,9 +743,9 @@ public class EncryptFilesFragment extends CryptoOperationFragment { } // Provide a suitable constructor (depends on the kind of dataset) - public FilesAdapter(Activity activity, List<ViewModel> myDataset, View.OnClickListener onFooterClickListener) { + public FilesAdapter(Activity activity, View.OnClickListener onFooterClickListener) { mActivity = activity; - mDataset = myDataset; + mDataset = new ArrayList<>(); mFooterOnClickListener = onFooterClickListener; } @@ -696,7 +799,8 @@ public class EncryptFilesFragment extends CryptoOperationFragment { // Return the size of your dataset (invoked by the layout manager) @Override public int getItemCount() { - return mDataset.size() + 1; + // one extra for the footer! + return mDataset.size() +1; } @Override @@ -727,7 +831,7 @@ public class EncryptFilesFragment extends CryptoOperationFragment { for (Uri inputUri : inputUris) { ViewModel newModel = new ViewModel(mActivity, inputUri); if (mDataset.contains(newModel)) { - Log.e(Constants.TAG, "Skipped duplicate " + inputUri.toString()); + Log.e(Constants.TAG, "Skipped duplicate " + inputUri); } else { mDataset.add(newModel); } @@ -736,6 +840,14 @@ public class EncryptFilesFragment extends CryptoOperationFragment { } } + public int getModelCount() { + return mDataset.size(); + } + + public ViewModel getModelItem(int position) { + return mDataset.get(position); + } + public void remove(ViewModel model) { int position = mDataset.indexOf(model); mDataset.remove(position); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java index 6f56f2dc4..355c649e7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java @@ -17,18 +17,15 @@ package org.sufficientlysecure.keychain.ui; -import android.app.Activity; import android.os.Bundle; -import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ViewAnimator; -import com.tokenautocomplete.TokenCompleteTextView; - +import com.tokenautocomplete.TokenCompleteTextView.TokenListener; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; @@ -38,39 +35,21 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView; import org.sufficientlysecure.keychain.ui.widget.KeySpinner; +import org.sufficientlysecure.keychain.ui.widget.KeySpinner.OnKeyChangedListener; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.Passphrase; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -public class EncryptModeAsymmetricFragment extends Fragment { - - public interface IAsymmetric { - - public void onSignatureKeyIdChanged(long signatureKeyId); - - public void onEncryptionKeyIdsChanged(long[] encryptionKeyIds); - - public void onEncryptionUserIdsChanged(String[] encryptionUserIds); - } +public class EncryptModeAsymmetricFragment extends EncryptModeFragment { ProviderHelper mProviderHelper; - // view - private KeySpinner mSign; + private KeySpinner mSignKeySpinner; private EncryptKeyCompletionView mEncryptKeyView; - // model - private IAsymmetric mEncryptInterface; - -// @Override -// public void updateUi() { -// if (mSign != null) { -// mSign.setSelectedKeyId(mEncryptInterface.getSignatureKey()); -// } -// } - public static final String ARG_SINGATURE_KEY_ID = "signature_key_id"; public static final String ARG_ENCRYPTION_KEY_IDS = "encryption_key_ids"; @@ -89,16 +68,6 @@ public class EncryptModeAsymmetricFragment extends Fragment { return frag; } - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - try { - mEncryptInterface = (IAsymmetric) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity + " must implement IAsymmetric"); - } - } - /** * Inflate the layout for this fragment */ @@ -106,15 +75,38 @@ public class EncryptModeAsymmetricFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.encrypt_asymmetric_fragment, container, false); - mSign = (KeySpinner) view.findViewById(R.id.sign); - mSign.setOnKeyChangedListener(new KeySpinner.OnKeyChangedListener() { + mSignKeySpinner = (KeySpinner) view.findViewById(R.id.sign); + mEncryptKeyView = (EncryptKeyCompletionView) view.findViewById(R.id.recipient_list); + mEncryptKeyView.setThreshold(1); // Start working from first character + + final ViewAnimator vSignatureIcon = (ViewAnimator) view.findViewById(R.id.result_signature_icon); + mSignKeySpinner.setOnKeyChangedListener(new OnKeyChangedListener() { @Override public void onKeyChanged(long masterKeyId) { - mEncryptInterface.onSignatureKeyIdChanged(masterKeyId); + int child = masterKeyId != Constants.key.none ? 1 : 0; + if (vSignatureIcon.getDisplayedChild() != child) { + vSignatureIcon.setDisplayedChild(child); + } + } + }); + + final ViewAnimator vEncryptionIcon = (ViewAnimator) view.findViewById(R.id.result_encryption_icon); + mEncryptKeyView.setTokenListener(new TokenListener() { + @Override + public void onTokenAdded(Object o) { + if (vEncryptionIcon.getDisplayedChild() != 1) { + vEncryptionIcon.setDisplayedChild(1); + } + } + + @Override + public void onTokenRemoved(Object o) { + int child = mEncryptKeyView.getObjects().isEmpty() ? 0 : 1; + if (vEncryptionIcon.getDisplayedChild() != child) { + vEncryptionIcon.setDisplayedChild(child); + } } }); - mEncryptKeyView = (EncryptKeyCompletionView) view.findViewById(R.id.recipient_list); - mEncryptKeyView.setThreshold(1); // Start working from first character return view; } @@ -124,39 +116,28 @@ public class EncryptModeAsymmetricFragment extends Fragment { super.onActivityCreated(savedInstanceState); mProviderHelper = new ProviderHelper(getActivity()); - // preselect keys given - long signatureKeyId = getArguments().getLong(ARG_SINGATURE_KEY_ID); - long[] encryptionKeyIds = getArguments().getLongArray(ARG_ENCRYPTION_KEY_IDS); - preselectKeys(signatureKeyId, encryptionKeyIds); - - mEncryptKeyView.setTokenListener(new TokenCompleteTextView.TokenListener() { - @Override - public void onTokenAdded(Object token) { - if (token instanceof KeyItem) { - updateEncryptionKeys(); - } + // preselect keys given, from state or arguments + if (savedInstanceState == null) { + Long signatureKeyId = getArguments().getLong(ARG_SINGATURE_KEY_ID); + if (signatureKeyId == Constants.key.none) { + signatureKeyId = null; } + long[] encryptionKeyIds = getArguments().getLongArray(ARG_ENCRYPTION_KEY_IDS); + preselectKeys(signatureKeyId, encryptionKeyIds); + } - @Override - public void onTokenRemoved(Object token) { - if (token instanceof KeyItem) { - updateEncryptionKeys(); - } - } - }); } /** * If an Intent gives a signatureMasterKeyId and/or encryptionMasterKeyIds, preselect those! */ - private void preselectKeys(long signatureKeyId, long[] encryptionKeyIds) { - if (signatureKeyId != Constants.key.none) { + private void preselectKeys(Long signatureKeyId, long[] encryptionKeyIds) { + if (signatureKeyId != null) { try { CachedPublicKeyRing keyring = mProviderHelper.getCachedPublicKeyRing( KeyRings.buildUnifiedKeyRingUri(signatureKeyId)); if (keyring.hasAnySecret()) { - mEncryptInterface.onSignatureKeyIdChanged(keyring.getMasterKeyId()); - mSign.setSelectedKeyId(signatureKeyId); + mSignKeySpinner.setPreSelectedKeyId(signatureKeyId); } } catch (PgpKeyNotFoundException e) { Log.e(Constants.TAG, "key not found!", e); @@ -175,27 +156,55 @@ public class EncryptModeAsymmetricFragment extends Fragment { } // This is to work-around a rendering bug in TokenCompleteTextView mEncryptKeyView.requestFocus(); - updateEncryptionKeys(); } } - private void updateEncryptionKeys() { - List<Object> objects = mEncryptKeyView.getObjects(); + @Override + public boolean isAsymmetric() { + return true; + } + + @Override + public long getAsymmetricSigningKeyId() { + return mSignKeySpinner.getSelectedKeyId(); + } + + @Override + public long[] getAsymmetricEncryptionKeyIds() { List<Long> keyIds = new ArrayList<>(); - List<String> userIds = new ArrayList<>(); - for (Object object : objects) { + for (Object object : mEncryptKeyView.getObjects()) { if (object instanceof KeyItem) { keyIds.add(((KeyItem) object).mKeyId); - userIds.add(((KeyItem) object).mUserIdFull); } } + long[] keyIdsArr = new long[keyIds.size()]; Iterator<Long> iterator = keyIds.iterator(); for (int i = 0; i < keyIds.size(); i++) { keyIdsArr[i] = iterator.next(); } - mEncryptInterface.onEncryptionKeyIdsChanged(keyIdsArr); - mEncryptInterface.onEncryptionUserIdsChanged(userIds.toArray(new String[userIds.size()])); + + return keyIdsArr; + } + + @Override + public String[] getAsymmetricEncryptionUserIds() { + + List<String> userIds = new ArrayList<>(); + for (Object object : mEncryptKeyView.getObjects()) { + if (object instanceof KeyItem) { + userIds.add(((KeyItem) object).mUserIdFull); + } + } + + return userIds.toArray(new String[userIds.size()]); + } + + @Override + public Passphrase getSymmetricPassphrase() { + throw new UnsupportedOperationException("should never happen, this is a programming error!"); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeFragment.java new file mode 100644 index 000000000..0b9672654 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeFragment.java @@ -0,0 +1,19 @@ +package org.sufficientlysecure.keychain.ui; + + +import android.support.v4.app.Fragment; + +import org.sufficientlysecure.keychain.util.Passphrase; + + +public abstract class EncryptModeFragment extends Fragment { + + public abstract boolean isAsymmetric(); + + public abstract long getAsymmetricSigningKeyId(); + public abstract long[] getAsymmetricEncryptionKeyIds(); + public abstract String[] getAsymmetricEncryptionUserIds(); + + public abstract Passphrase getSymmetricPassphrase(); + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeSymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeSymmetricFragment.java index 48b1f4983..b92a73731 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeSymmetricFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeSymmetricFragment.java @@ -17,11 +17,7 @@ package org.sufficientlysecure.keychain.ui; -import android.app.Activity; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.text.Editable; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -30,14 +26,7 @@ import android.widget.EditText; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.util.Passphrase; -public class EncryptModeSymmetricFragment extends Fragment { - - public interface ISymmetric { - - public void onPassphraseChanged(Passphrase passphrase); - } - - private ISymmetric mEncryptInterface; +public class EncryptModeSymmetricFragment extends EncryptModeFragment { private EditText mPassphrase; private EditText mPassphraseAgain; @@ -55,52 +44,53 @@ public class EncryptModeSymmetricFragment extends Fragment { } @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - try { - mEncryptInterface = (ISymmetric) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() + " must implement ISymmetric"); - } - } - - /** - * Inflate the layout for this fragment - */ - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.encrypt_symmetric_fragment, container, false); mPassphrase = (EditText) view.findViewById(R.id.passphrase); mPassphraseAgain = (EditText) view.findViewById(R.id.passphraseAgain); - TextWatcher textWatcher = new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } + return view; + } + + @Override + public boolean isAsymmetric() { + return false; + } - @Override - public void afterTextChanged(Editable s) { - // update passphrase in EncryptActivity - Passphrase p1 = new Passphrase(mPassphrase.getText()); - Passphrase p2 = new Passphrase(mPassphraseAgain.getText()); - boolean passesEquals = (p1.equals(p2)); + @Override + public long getAsymmetricSigningKeyId() { + throw new UnsupportedOperationException("should never happen, this is a programming error!"); + } + + @Override + public long[] getAsymmetricEncryptionKeyIds() { + throw new UnsupportedOperationException("should never happen, this is a programming error!"); + } + + @Override + public String[] getAsymmetricEncryptionUserIds() { + throw new UnsupportedOperationException("should never happen, this is a programming error!"); + } + + @Override + public Passphrase getSymmetricPassphrase() { + Passphrase p1 = null, p2 = null; + try { + p1 = new Passphrase(mPassphrase.getText()); + p2 = new Passphrase(mPassphraseAgain.getText()); + if (!p1.equals(p2)) { + return null; + } + return new Passphrase(mPassphrase.getText()); + } finally { + if (p1 != null) { p1.removeFromMemory(); + } + if (p2 != null) { p2.removeFromMemory(); - if (passesEquals) { - mEncryptInterface.onPassphraseChanged(new Passphrase(mPassphrase.getText())); - } else { - mEncryptInterface.onPassphraseChanged(null); - } } - }; - mPassphrase.addTextChangedListener(textWatcher); - mPassphraseAgain.addTextChangedListener(textWatcher); - - return view; + } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java index 52d098adc..a849cdf12 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java @@ -18,22 +18,17 @@ package org.sufficientlysecure.keychain.ui; +import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.view.View; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; -import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.Passphrase; -public class EncryptTextActivity extends BaseActivity implements - EncryptModeAsymmetricFragment.IAsymmetric, EncryptModeSymmetricFragment.ISymmetric, - EncryptTextFragment.IMode { +public class EncryptTextActivity extends EncryptActivity { /* Intents */ public static final String ACTION_ENCRYPT_TEXT = OpenKeychainIntents.ENCRYPT_TEXT; @@ -41,40 +36,13 @@ public class EncryptTextActivity extends BaseActivity implements /* EXTRA keys for input */ public static final String EXTRA_TEXT = OpenKeychainIntents.ENCRYPT_EXTRA_TEXT; - // preselect ids, for internal use - public static final String EXTRA_SIGNATURE_KEY_ID = Constants.EXTRA_PREFIX + "EXTRA_SIGNATURE_KEY_ID"; - public static final String EXTRA_ENCRYPTION_KEY_IDS = Constants.EXTRA_PREFIX + "EXTRA_SIGNATURE_KEY_IDS"; - - Fragment mModeFragment; - EncryptTextFragment mEncryptFragment; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setFullScreenDialogClose(new View.OnClickListener() { - @Override - public void onClick(View v) { - finish(); - } - }, false); - - // Handle intent actions - handleActions(getIntent(), savedInstanceState); - } - - @Override - protected void initLayout() { - setContentView(R.layout.encrypt_text_activity); - } - + setFullScreenDialogClose(Activity.RESULT_OK, false); - /** - * Handles all actions with this intent - * - * @param intent - */ - private void handleActions(Intent intent, Bundle savedInstanceState) { + Intent intent = getIntent(); String action = intent.getAction(); Bundle extras = intent.getExtras(); String type = intent.getType(); @@ -83,10 +51,6 @@ public class EncryptTextActivity extends BaseActivity implements extras = new Bundle(); } - /* - * Android's Action - */ - // When sending to OpenKeychain Encrypt via share menu if (Intent.ACTION_SEND.equals(action) && type != null) { Log.logDebugBundle(extras, "extras"); @@ -108,55 +72,19 @@ public class EncryptTextActivity extends BaseActivity implements textData = ""; } - // preselect keys given by intent - long mSigningKeyId = extras.getLong(EXTRA_SIGNATURE_KEY_ID); - long[] mEncryptionKeyIds = extras.getLongArray(EXTRA_ENCRYPTION_KEY_IDS); - if (savedInstanceState == null) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - mModeFragment = EncryptModeAsymmetricFragment.newInstance(mSigningKeyId, mEncryptionKeyIds); - transaction.replace(R.id.encrypt_mode_container, mModeFragment, "mode"); - - mEncryptFragment = EncryptTextFragment.newInstance(textData); - transaction.replace(R.id.encrypt_text_container, mEncryptFragment, "text"); - + EncryptTextFragment encryptFragment = EncryptTextFragment.newInstance(textData); + transaction.replace(R.id.encrypt_text_container, encryptFragment); transaction.commit(); - - getSupportFragmentManager().executePendingTransactions(); } - } - - @Override - public void onModeChanged(boolean symmetric) { - // switch fragments - getSupportFragmentManager().beginTransaction() - .replace(R.id.encrypt_mode_container, - symmetric - ? EncryptModeSymmetricFragment.newInstance() - : EncryptModeAsymmetricFragment.newInstance(0, null) - ) - .commitAllowingStateLoss(); - getSupportFragmentManager().executePendingTransactions(); - } - - @Override - public void onSignatureKeyIdChanged(long signatureKeyId) { - mEncryptFragment.setSigningKeyId(signatureKeyId); - } - @Override - public void onEncryptionKeyIdsChanged(long[] encryptionKeyIds) { - mEncryptFragment.setEncryptionKeyIds(encryptionKeyIds); } @Override - public void onEncryptionUserIdsChanged(String[] encryptionUserIds) { - mEncryptFragment.setEncryptionUserIds(encryptionUserIds); + protected void initLayout() { + setContentView(R.layout.encrypt_text_activity); } - @Override - public void onPassphraseChanged(Passphrase passphrase) { - mEncryptFragment.setSymmetricPassphrase(passphrase); - } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java index 3f9147cc4..ab676285e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java @@ -18,11 +18,11 @@ package org.sufficientlysecure.keychain.ui; import android.app.Activity; -import android.app.ProgressDialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; @@ -33,67 +33,37 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import org.spongycastle.bcpg.CompressionAlgorithmTags; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.pgp.PgpConstants; +import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.base.CachingCryptoOperationFragment; import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.Passphrase; +import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.ShareHelper; +import java.util.Date; import java.util.HashSet; import java.util.Set; -public class EncryptTextFragment extends CryptoOperationFragment { - - public interface IMode { - public void onModeChanged(boolean symmetric); - } +public class EncryptTextFragment + extends CachingCryptoOperationFragment<SignEncryptParcel, SignEncryptResult> { public static final String ARG_TEXT = "text"; + public static final String ARG_USE_COMPRESSION = "use_compression"; - private IMode mModeInterface; - - private boolean mSymmetricMode = false; - private boolean mShareAfterEncrypt = false; - private boolean mUseCompression = true; + private boolean mShareAfterEncrypt; + private boolean mUseCompression; private boolean mHiddenRecipients = false; - private long mEncryptionKeyIds[] = null; - private String mEncryptionUserIds[] = null; - // TODO Constants.key.none? What's wrong with a null value? - private long mSigningKeyId = Constants.key.none; - private Passphrase mSymmetricPassphrase = new Passphrase(); private String mMessage = ""; - private TextView mText; - - public void setEncryptionKeyIds(long[] encryptionKeyIds) { - mEncryptionKeyIds = encryptionKeyIds; - } - - public void setEncryptionUserIds(String[] encryptionUserIds) { - mEncryptionUserIds = encryptionUserIds; - } - - public void setSigningKeyId(long signingKeyId) { - mSigningKeyId = signingKeyId; - } - - public void setSymmetricPassphrase(Passphrase passphrase) { - mSymmetricPassphrase = passphrase; - } - /** * Creates new instance of this fragment */ @@ -110,10 +80,8 @@ public class EncryptTextFragment extends CryptoOperationFragment { @Override public void onAttach(Activity activity) { super.onAttach(activity); - try { - mModeInterface = (IMode) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() + " must implement IMode"); + if ( ! (activity instanceof EncryptActivity) ) { + throw new AssertionError(activity + " must inherit from EncryptionActivity"); } } @@ -124,8 +92,8 @@ public class EncryptTextFragment extends CryptoOperationFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.encrypt_text_fragment, container, false); - mText = (TextView) view.findViewById(R.id.encrypt_text_text); - mText.addTextChangedListener(new TextWatcher() { + TextView textView = (TextView) view.findViewById(R.id.encrypt_text_text); + textView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { @@ -144,39 +112,53 @@ public class EncryptTextFragment extends CryptoOperationFragment { // set initial text if (mMessage != null) { - mText.setText(mMessage); + textView.setText(mMessage); } return view; } @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(ARG_USE_COMPRESSION, mUseCompression); + } + + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mMessage = getArguments().getString(ARG_TEXT); + if (savedInstanceState == null) { + mMessage = getArguments().getString(ARG_TEXT); + } + + Preferences prefs = Preferences.getPreferences(getActivity()); + + Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState; + + mUseCompression = args.getBoolean(ARG_USE_COMPRESSION, true); + if (args.containsKey(ARG_USE_COMPRESSION)) { + mUseCompression = args.getBoolean(ARG_USE_COMPRESSION, true); + } else { + mUseCompression = prefs.getTextUseCompression(); + } setHasOptionsMenu(true); + } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.encrypt_text_fragment, menu); + + menu.findItem(R.id.check_enable_compression).setChecked(mUseCompression); } @Override public boolean onOptionsItemSelected(MenuItem item) { - if (item.isCheckable()) { - item.setChecked(!item.isChecked()); - } switch (item.getItemId()) { - case R.id.check_use_symmetric: { - mSymmetricMode = item.isChecked(); - mModeInterface.onModeChanged(mSymmetricMode); - break; - } case R.id.check_enable_compression: { - mUseCompression = item.isChecked(); + toggleEnableCompression(item, !item.isChecked()); break; } // case R.id.check_hidden_recipients: { @@ -185,11 +167,15 @@ public class EncryptTextFragment extends CryptoOperationFragment { // break; // } case R.id.encrypt_copy: { - startEncrypt(false); + hideKeyboard(); + mShareAfterEncrypt = false; + cryptoOperation(new CryptoInputParcel(new Date())); break; } case R.id.encrypt_share: { - startEncrypt(true); + hideKeyboard(); + mShareAfterEncrypt = true; + cryptoOperation(new CryptoInputParcel(new Date())); break; } default: { @@ -199,22 +185,36 @@ public class EncryptTextFragment extends CryptoOperationFragment { return true; } + public void toggleEnableCompression(MenuItem item, final boolean compress) { + + mUseCompression = compress; + item.setChecked(compress); + + Notify.create(getActivity(), compress + ? R.string.snack_compression_on + : R.string.snack_compression_off, + Notify.LENGTH_LONG, Style.OK, new ActionListener() { + @Override + public void onAction() { + Preferences.getPreferences(getActivity()).setTextUseCompression(compress); + Notify.create(getActivity(), compress + ? R.string.snack_compression_on + : R.string.snack_compression_off, + Notify.LENGTH_SHORT, Style.OK, null, R.string.btn_saved) + .show(EncryptTextFragment.this, false); + } + }, R.string.btn_save_default).show(this); - protected void onEncryptSuccess(SignEncryptResult result) { - if (mShareAfterEncrypt) { - // Share encrypted message/file - startActivity(sendWithChooserExcludingEncrypt(result.getResultBytes())); - } else { - // Copy to clipboard - copyToClipboard(result.getResultBytes()); - result.createNotify(getActivity()).show(); - // Notify.create(EncryptTextActivity.this, - // R.string.encrypt_sign_clipboard_successful, Notify.Style.OK) - // .show(getSupportFragmentManager().findFragmentById(R.id.encrypt_text_fragment)); - } } - protected SignEncryptParcel createEncryptBundle() { + public SignEncryptParcel createOperationInput() { + + if (mMessage == null || mMessage.isEmpty()) { + Notify.create(getActivity(), R.string.error_empty_text, Notify.Style.ERROR) + .show(this); + return null; + } + // fill values for this action SignEncryptParcel data = new SignEncryptParcel(); @@ -222,33 +222,71 @@ public class EncryptTextFragment extends CryptoOperationFragment { data.setCleartextSignature(true); if (mUseCompression) { - data.setCompressionId(PgpConstants.sPreferredCompressionAlgorithms.get(0)); + data.setCompressionAlgorithm( + PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.USE_DEFAULT); } else { - data.setCompressionId(CompressionAlgorithmTags.UNCOMPRESSED); + data.setCompressionAlgorithm( + PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.UNCOMPRESSED); } data.setHiddenRecipients(mHiddenRecipients); - data.setSymmetricEncryptionAlgorithm(PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED); - data.setSignatureHashAlgorithm(PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED); + data.setSymmetricEncryptionAlgorithm( + PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT); + data.setSignatureHashAlgorithm( + PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT); // Always use armor for messages data.setEnableAsciiArmorOutput(true); - if (mSymmetricMode) { - Log.d(Constants.TAG, "Symmetric encryption enabled!"); - Passphrase passphrase = mSymmetricPassphrase; + EncryptActivity modeInterface = (EncryptActivity) getActivity(); + EncryptModeFragment modeFragment = modeInterface.getModeFragment(); + + if (modeFragment.isAsymmetric()) { + long[] encryptionKeyIds = modeFragment.getAsymmetricEncryptionKeyIds(); + long signingKeyId = modeFragment.getAsymmetricSigningKeyId(); + + boolean gotEncryptionKeys = (encryptionKeyIds != null + && encryptionKeyIds.length > 0); + + if (!gotEncryptionKeys && signingKeyId == Constants.key.none) { + Notify.create(getActivity(), R.string.error_no_encryption_or_signature_key, Notify.Style.ERROR) + .show(this); + return null; + } + + data.setEncryptionMasterKeyIds(encryptionKeyIds); + data.setSignatureMasterKeyId(signingKeyId); + } else { + Passphrase passphrase = modeFragment.getSymmetricPassphrase(); + if (passphrase == null) { + Notify.create(getActivity(), R.string.passphrases_do_not_match, Notify.Style.ERROR) + .show(this); + return null; + } if (passphrase.isEmpty()) { - passphrase = null; + Notify.create(getActivity(), R.string.passphrase_must_not_be_empty, Notify.Style.ERROR) + .show(this); + return null; } data.setSymmetricPassphrase(passphrase); - } else { - data.setEncryptionMasterKeyIds(mEncryptionKeyIds); - data.setSignatureMasterKeyId(mSigningKeyId); } return data; } - private void copyToClipboard(byte[] resultBytes) { - ClipboardReflection.copyToClipboard(getActivity(), new String(resultBytes)); + private void copyToClipboard(SignEncryptResult result) { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipMan == null) { + Notify.create(activity, R.string.error_clipboard_copy, Style.ERROR).show(); + return; + } + + ClipData clip = ClipData.newPlainText(Constants.CLIPBOARD_LABEL, new String(result.getResultBytes())); + clipMan.setPrimaryClip(clip); + result.createNotify(activity).show(); } /** @@ -273,114 +311,44 @@ public class EncryptTextFragment extends CryptoOperationFragment { sendIntent.setType(Constants.ENCRYPTED_TEXT_MIME); sendIntent.putExtra(Intent.EXTRA_TEXT, new String(resultBytes)); - if (!mSymmetricMode && mEncryptionUserIds != null) { - Set<String> users = new HashSet<>(); - for (String user : mEncryptionUserIds) { - KeyRing.UserId userId = KeyRing.splitUserId(user); - if (userId.email != null) { - users.add(userId.email); - } - } - // pass trough email addresses as extra for email applications - sendIntent.putExtra(Intent.EXTRA_EMAIL, users.toArray(new String[users.size()])); + EncryptActivity modeInterface = (EncryptActivity) getActivity(); + EncryptModeFragment modeFragment = modeInterface.getModeFragment(); + if (!modeFragment.isAsymmetric()) { + return sendIntent; } - return sendIntent; - } - protected boolean inputIsValid() { - if (mMessage == null || mMessage.isEmpty()) { - Notify.create(getActivity(), R.string.error_empty_text, Notify.Style.ERROR) - .show(this); - return false; + String[] encryptionUserIds = modeFragment.getAsymmetricEncryptionUserIds(); + if (encryptionUserIds == null) { + return sendIntent; } - if (mSymmetricMode) { - // symmetric encryption checks - - if (mSymmetricPassphrase == null) { - Notify.create(getActivity(), R.string.passphrases_do_not_match, Notify.Style.ERROR) - .show(this); - return false; - } - if (mSymmetricPassphrase.isEmpty()) { - Notify.create(getActivity(), R.string.passphrase_must_not_be_empty, Notify.Style.ERROR) - .show(this); - return false; - } - - } else { - // asymmetric encryption checks - - boolean gotEncryptionKeys = (mEncryptionKeyIds != null - && mEncryptionKeyIds.length > 0); - - if (!gotEncryptionKeys && mSigningKeyId == 0) { - Notify.create(getActivity(), R.string.select_encryption_or_signature_key, Notify.Style.ERROR) - .show(this); - return false; + Set<String> users = new HashSet<>(); + for (String user : encryptionUserIds) { + KeyRing.UserId userId = KeyRing.splitUserId(user); + if (userId.email != null) { + users.add(userId.email); } } - return true; - } + // pass trough email addresses as extra for email applications + sendIntent.putExtra(Intent.EXTRA_EMAIL, users.toArray(new String[users.size()])); - - public void startEncrypt(boolean share) { - mShareAfterEncrypt = share; - cryptoOperation(); + return sendIntent; } @Override - protected void cryptoOperation(CryptoInputParcel cryptoInput) { - if (!inputIsValid()) { - // Notify was created by inputIsValid. - return; - } + public void onQueuedOperationSuccess(SignEncryptResult result) { + super.onQueuedOperationSuccess(result); - // Send all information needed to service to edit key in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_SIGN_ENCRYPT); - - final SignEncryptParcel input = createEncryptBundle(); - final Bundle data = new Bundle(); - data.putParcelable(KeychainIntentService.SIGN_ENCRYPT_PARCEL, input); - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Message is received after encrypting is done in KeychainIntentService - ServiceProgressHandler serviceHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_encrypting), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (handlePendingMessage(message)) { - return; - } - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - SignEncryptResult result = - message.getData().getParcelable(SignEncryptResult.EXTRA_RESULT); - - if (result.success()) { - onEncryptSuccess(result); - } else { - result.createNotify(getActivity()).show(); - } - } - } - }; - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(serviceHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + hideKeyboard(); - // show progress dialog - serviceHandler.showProgressDialog(getActivity()); + if (mShareAfterEncrypt) { + // Share encrypted message/file + startActivity(sendWithChooserExcludingEncrypt(result.getResultBytes())); + } else { + // Copy to clipboard + copyToClipboard(result); + } - // start service with intent - getActivity().startService(intent); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java index ac4b94d64..7a1e167bb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2012-2015 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 @@ -51,14 +51,11 @@ public class HelpAboutFragment extends Fragment { try { String html = new Markdown4jProcessor().process( getActivity().getResources().openRawResource(R.raw.help_about)); - aboutTextView.setHtmlFromString(html, true); + aboutTextView.setHtmlFromString(html, new HtmlTextView.LocalImageGetter()); } catch (IOException e) { Log.e(Constants.TAG, "IOException", e); } - // no flickering when clicking textview for Android < 4 - aboutTextView.setTextColor(getResources().getColor(android.R.color.black)); - return view; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpMarkdownFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpMarkdownFragment.java index 97d39feb1..15098b8d6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpMarkdownFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpMarkdownFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2012-2015 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 @@ -17,7 +17,6 @@ package org.sufficientlysecure.keychain.ui; -import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.util.TypedValue; @@ -34,9 +33,6 @@ import org.sufficientlysecure.keychain.util.Log; import java.io.IOException; public class HelpMarkdownFragment extends Fragment { - private Activity mActivity; - - private int mHtmlFile; public static final String ARG_MARKDOWN_RES = "htmlFile"; @@ -56,15 +52,13 @@ public class HelpMarkdownFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mActivity = getActivity(); - - mHtmlFile = getArguments().getInt(ARG_MARKDOWN_RES); + int mHtmlFile = getArguments().getInt(ARG_MARKDOWN_RES); - ScrollView scroller = new ScrollView(mActivity); - HtmlTextView text = new HtmlTextView(mActivity); + ScrollView scroller = new ScrollView(getActivity()); + HtmlTextView text = new HtmlTextView(getActivity()); // padding - int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, mActivity + int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, getActivity() .getResources().getDisplayMetrics()); text.setPadding(padding, padding, padding, 0); @@ -72,15 +66,13 @@ public class HelpMarkdownFragment extends Fragment { // load markdown from raw resource try { - String html = new Markdown4jProcessor().process(getActivity().getResources().openRawResource(mHtmlFile)); - text.setHtmlFromString(html, true); + String html = new Markdown4jProcessor().process( + getActivity().getResources().openRawResource(mHtmlFile)); + text.setHtmlFromString(html, new HtmlTextView.LocalImageGetter()); } catch (IOException e) { Log.e(Constants.TAG, "IOException", e); } - // no flickering when clicking textview for Android < 4 - text.setTextColor(getResources().getColor(android.R.color.black)); - return scroller; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java index 4cba62d5b..4ef6c40dc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -17,12 +17,11 @@ package org.sufficientlysecure.keychain.ui; -import android.app.ProgressDialog; +import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Message; -import android.os.Messenger; import android.support.v4.app.Fragment; import android.view.View; import android.view.View.OnClickListener; @@ -35,11 +34,10 @@ import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; -import org.sufficientlysecure.keychain.service.CloudImportService; import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Log; @@ -49,7 +47,8 @@ import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize import java.io.IOException; import java.util.ArrayList; -public class ImportKeysActivity extends BaseNfcActivity { +public class ImportKeysActivity extends BaseNfcActivity + implements CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> { public static final String ACTION_IMPORT_KEY = OpenKeychainIntents.IMPORT_KEY; public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER = OpenKeychainIntents.IMPORT_KEY_FROM_KEYSERVER; @@ -84,10 +83,17 @@ public class ImportKeysActivity extends BaseNfcActivity { private Fragment mTopFragment; private View mImportButton; + // for CryptoOperationHelper.Callback + private String mKeyserver; + private ArrayList<ParcelableKeyRing> mKeyList; + + private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mOperationHelper; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setFullScreenDialogClose(Activity.RESULT_CANCELED, true); mImportButton = findViewById(R.id.import_import); mImportButton.setOnClickListener(new OnClickListener() { @Override @@ -220,9 +226,9 @@ public class ImportKeysActivity extends BaseNfcActivity { Notify.Style.WARN).show(mTopFragment); // we just set the keyserver startCloudFragment(savedInstanceState, null, false, keyserver); - // it's not necessary to set the keyserver for ImportKeysListFragment since - // it'll be taken care of by ImportKeysCloudFragment when the user clicks - // the search button + // we don't set the keyserver for ImportKeysListFragment since + // it'll be set in the cloudSearchPrefs of ImportKeysCloudFragment + // which is used when the user clicks on the search button startListFragment(savedInstanceState, null, null, null, null); } else { // we allow our users to edit the query if they wish @@ -265,7 +271,7 @@ public class ImportKeysActivity extends BaseNfcActivity { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. - if (savedInstanceState != null) { + if (mListFragment != null) { return; } @@ -285,7 +291,7 @@ public class ImportKeysActivity extends BaseNfcActivity { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. - if (savedInstanceState != null) { + if (mTopFragment != null) { return; } @@ -312,11 +318,12 @@ public class ImportKeysActivity extends BaseNfcActivity { * specified in user preferences */ - private void startCloudFragment(Bundle savedInstanceState, String query, boolean disableQueryEdit, String keyserver) { + private void startCloudFragment(Bundle savedInstanceState, String query, boolean disableQueryEdit, String + keyserver) { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. - if (savedInstanceState != null) { + if (mTopFragment != null) { return; } @@ -342,7 +349,7 @@ public class ImportKeysActivity extends BaseNfcActivity { } } - public void loadCallback(ImportKeysListFragment.LoaderState loaderState) { + public void loadCallback(final ImportKeysListFragment.LoaderState loaderState) { mListFragment.loadNew(loaderState); } @@ -383,32 +390,20 @@ public class ImportKeysActivity extends BaseNfcActivity { * Import keys with mImportData */ public void importKeys() { - ImportKeysListFragment.LoaderState ls = mListFragment.getLoaderState(); - if (ls instanceof ImportKeysListFragment.BytesLoaderState) { - Log.d(Constants.TAG, "importKeys started"); - - ServiceProgressHandler serviceHandler = new ServiceProgressHandler( - this, - getString(R.string.progress_importing), - ProgressDialog.STYLE_HORIZONTAL, - true, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - ImportKeysActivity.this.handleMessage(message); - } - }; - // TODO: Currently not using CloudImport here due to https://github.com/open-keychain/open-keychain/issues/1221 - // Send all information needed to service to import key in other thread - Intent intent = new Intent(this, KeychainIntentService.class); + if (mListFragment.getSelectedEntries().size() == 0) { + Notify.create(this, R.string.error_nothing_import_selected, Notify.Style.ERROR) + .show((ViewGroup) findViewById(R.id.import_snackbar)); + return; + } - intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); + mOperationHelper = new CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult>( + 1, this, this, R.string.progress_importing + ); - // fill values for this action - Bundle data = new Bundle(); + ImportKeysListFragment.LoaderState ls = mListFragment.getLoaderState(); + if (ls instanceof ImportKeysListFragment.BytesLoaderState) { + Log.d(Constants.TAG, "importKeys started"); // get DATA from selected key entries IteratorWithSize<ParcelableKeyRing> selectedEntries = mListFragment.getSelectedData(); @@ -423,46 +418,18 @@ public class ImportKeysActivity extends BaseNfcActivity { new ParcelableFileCache<>(this, "key_import.pcl"); cache.writeCache(selectedEntries); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(serviceHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - serviceHandler.showProgressDialog(this); + mKeyList = null; + mKeyserver = null; + mOperationHelper.cryptoOperation(); - // start service with intent - startService(intent); } catch (IOException e) { Log.e(Constants.TAG, "Problem writing cache file", e); Notify.create(this, "Problem writing cache file!", Notify.Style.ERROR) .show((ViewGroup) findViewById(R.id.import_snackbar)); } } else if (ls instanceof ImportKeysListFragment.CloudLoaderState) { - ImportKeysListFragment.CloudLoaderState sls = (ImportKeysListFragment.CloudLoaderState) ls; - - ServiceProgressHandler serviceHandler = new ServiceProgressHandler( - this, - getString(R.string.progress_importing), - ProgressDialog.STYLE_HORIZONTAL, - true, - ProgressDialogFragment.ServiceType.CLOUD_IMPORT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - ImportKeysActivity.this.handleMessage(message); - } - }; - - // Send all information needed to service to query keys in other thread - Intent intent = new Intent(this, CloudImportService.class); - - // fill values for this action - Bundle data = new Bundle(); - - data.putString(CloudImportService.IMPORT_KEY_SERVER, sls.mCloudPrefs.keyserver); + ImportKeysListFragment.CloudLoaderState sls = + (ImportKeysListFragment.CloudLoaderState) ls; // get selected key entries ArrayList<ParcelableKeyRing> keys = new ArrayList<>(); @@ -475,23 +442,70 @@ public class ImportKeysActivity extends BaseNfcActivity { ); } } - data.putParcelableArrayList(CloudImportService.IMPORT_KEY_LIST, keys); - intent.putExtra(CloudImportService.EXTRA_DATA, data); + mKeyList = keys; + mKeyserver = sls.mCloudPrefs.keyserver; + mOperationHelper.cryptoOperation(); - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(serviceHandler); - intent.putExtra(CloudImportService.EXTRA_MESSENGER, messenger); + } + } - // show progress dialog - serviceHandler.showProgressDialog(this); + @Override + protected void onNfcPostExecute() throws IOException { + // either way, finish after NFC AsyncTask + finish(); + } - // start service with intent - startService(intent); - } else { - Notify.create(this, R.string.error_nothing_import, Notify.Style.ERROR) - .show((ViewGroup) findViewById(R.id.import_snackbar)); + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mOperationHelper == null || + !mOperationHelper.handleActivityResult(requestCode, resultCode, data)) { + super.onActivityResult(requestCode, resultCode, data); + } + } + + public void handleResult(ImportKeyResult result) { + if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(getIntent().getAction()) + || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(getIntent().getAction())) { + Intent intent = new Intent(); + intent.putExtra(ImportKeyResult.EXTRA_RESULT, result); + ImportKeysActivity.this.setResult(RESULT_OK, intent); + ImportKeysActivity.this.finish(); + return; } + if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(getIntent().getAction())) { + ImportKeysActivity.this.setResult(RESULT_OK, mPendingIntentData); + ImportKeysActivity.this.finish(); + return; + } + + result.createNotify(ImportKeysActivity.this) + .show((ViewGroup) findViewById(R.id.import_snackbar)); + } + // methods from CryptoOperationHelper.Callback + + @Override + public ImportKeyringParcel createOperationInput() { + return new ImportKeyringParcel(mKeyList, mKeyserver); } + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + handleResult(result); + } + + @Override + public void onCryptoOperationCancelled() { + // do nothing + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + handleResult(result); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java index 538fa16c7..746c75600 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java @@ -64,8 +64,8 @@ public class ImportKeysFileFragment extends Fragment { // open .asc or .gpg files // setting it to text/plain prevents Cyanogenmod's file manager from selecting asc // or gpg types! - FileHelper.openFile(ImportKeysFileFragment.this, Uri.fromFile(Constants.Path.APP_DIR), - "*/*", REQUEST_CODE_FILE); + FileHelper.openDocument(ImportKeysFileFragment.this, + Uri.fromFile(Constants.Path.APP_DIR), "*/*", false, REQUEST_CODE_FILE); } }); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java index bf7e41045..8502798cd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java @@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.ui; import android.app.Activity; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; @@ -34,6 +35,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.GetKeyResult; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.adapter.AsyncTaskResultWrapper; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListCloudLoader; @@ -41,7 +43,9 @@ import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListLoader; import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; +import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; @@ -62,6 +66,7 @@ public class ImportKeysListFragment extends ListFragment implements private Activity mActivity; private ImportKeysAdapter mAdapter; + private ParcelableProxy mParcelableProxy; private LoaderState mLoaderState; @@ -71,6 +76,8 @@ public class ImportKeysListFragment extends ListFragment implements private LongSparseArray<ParcelableKeyRing> mCachedKeyData; private boolean mNonInteractive; + private boolean mShowingOrbotDialog; + public LoaderState getLoaderState() { return mLoaderState; } @@ -126,6 +133,7 @@ public class ImportKeysListFragment extends ListFragment implements /** * Creates an interactive ImportKeyListFragment which reads keyrings from bytes, or file specified * by dataUri, or searches a keyserver for serverQuery, if parameter is not null, in that order + * Will immediately load data if non-null bytes/dataUri/serverQuery * * @param bytes byte data containing list of keyrings to be imported * @param dataUri file from which keyrings are to be imported @@ -141,7 +149,7 @@ public class ImportKeysListFragment extends ListFragment implements /** * Visually consists of a list of keyrings with checkboxes to specify which are to be imported - * Can immediately load keyrings specified by any of its parameters + * Will immediately load data if non-null bytes/dataUri/serverQuery is supplied * * @param bytes byte data containing list of keyrings to be imported * @param dataUri file from which keyrings are to be imported @@ -259,6 +267,7 @@ public class ImportKeysListFragment extends ListFragment implements } public void loadNew(LoaderState loaderState) { + mLoaderState = loaderState; restartLoaders(); @@ -301,7 +310,8 @@ public class ImportKeysListFragment extends ListFragment implements } case LOADER_ID_CLOUD: { CloudLoaderState ls = (CloudLoaderState) mLoaderState; - return new ImportKeysListCloudLoader(getActivity(), ls.mServerQuery, ls.mCloudPrefs); + return new ImportKeysListCloudLoader(getActivity(), ls.mServerQuery, ls.mCloudPrefs, + mParcelableProxy); } default: @@ -349,6 +359,52 @@ public class ImportKeysListFragment extends ListFragment implements if (getKeyResult.success()) { // No error + } else if (getKeyResult.isPending()) { + if (getKeyResult.getRequiredInputParcel().mType == + RequiredInputParcel.RequiredInputType.ENABLE_ORBOT) { + if (mShowingOrbotDialog) { + // to prevent dialogs stacking + return; + } + + // this is because we can't commit fragment dialogs in onLoadFinished + Runnable showOrbotDialog = new Runnable() { + @Override + public void run() { + OrbotHelper.DialogActions dialogActions = + new OrbotHelper.DialogActions() { + @Override + public void onOrbotStarted() { + mShowingOrbotDialog = false; + restartLoaders(); + } + + @Override + public void onNeutralButton() { + mParcelableProxy = ParcelableProxy + .getForNoProxy(); + mShowingOrbotDialog = false; + restartLoaders(); + } + + @Override + public void onCancel() { + mShowingOrbotDialog = false; + } + }; + + if (OrbotHelper.putOrbotInRequiredState(dialogActions, + getActivity())) { + // looks like we didn't have to show the + // dialog after all + mShowingOrbotDialog = false; + restartLoaders(); + } + } + }; + new Handler().post(showOrbotDialog); + mShowingOrbotDialog = true; + } } else { getKeyResult.createNotify(getActivity()).show(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java index dc8752d1a..b60f3984c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java @@ -18,7 +18,6 @@ package org.sufficientlysecure.keychain.ui; import android.annotation.TargetApi; -import android.app.ProgressDialog; import android.content.Intent; import android.content.pm.ActivityInfo; import android.net.Uri; @@ -26,8 +25,6 @@ import android.nfc.NdefMessage; import android.nfc.NfcAdapter; import android.os.Build; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; import android.os.Parcelable; import android.support.v4.app.FragmentActivity; import android.widget.Toast; @@ -41,10 +38,10 @@ import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; 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.SingletonResult; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; @@ -55,7 +52,8 @@ import java.util.Locale; /** * Proxy activity (just a transparent content view) to scan QR Codes using the Barcode Scanner app */ -public class ImportKeysProxyActivity extends FragmentActivity { +public class ImportKeysProxyActivity extends FragmentActivity + implements CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> { public static final String ACTION_QR_CODE_API = OpenKeychainIntents.IMPORT_KEY_FROM_QR_CODE; // implies activity returns scanned fingerprint as extra and does not import @@ -64,6 +62,11 @@ public class ImportKeysProxyActivity extends FragmentActivity { public static final String EXTRA_FINGERPRINT = "fingerprint"; + // for CryptoOperationHelper + private String mKeyserver; + private ArrayList<ParcelableKeyRing> mKeyList; + private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -106,6 +109,19 @@ public class ImportKeysProxyActivity extends FragmentActivity { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mImportOpHelper != null) { + if (!mImportOpHelper.handleActivityResult(requestCode, resultCode, data)) { + // if a result has been returned, and it does not belong to mImportOpHelper, + // return it down to other activity + if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { + returnResult(data); + } else { + super.onActivityResult(requestCode, resultCode, data); + finish(); + } + } + } + if (requestCode == IntentIntegratorSupportV4.REQUEST_CODE) { IntentResult scanResult = IntentIntegratorSupportV4.parseActivityResult(requestCode, resultCode, data); @@ -121,13 +137,6 @@ public class ImportKeysProxyActivity extends FragmentActivity { return; } - // if a result has been returned, return it down to other activity - if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { - returnResult(data); - } else { - super.onActivityResult(requestCode, resultCode, data); - finish(); - } } private void processScannedContent(String content) { @@ -141,24 +150,34 @@ public class ImportKeysProxyActivity extends FragmentActivity { Log.d(Constants.TAG, "scanned: " + uri); // example: openpgp4fpr:73EE2314F65FA92EC2390D3A718C070100012282 - if (uri != null && uri.getScheme() != null && uri.getScheme().toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) { - String fingerprint = uri.getEncodedSchemeSpecificPart().toLowerCase(Locale.ENGLISH); - - if (ACTION_SCAN_WITH_RESULT.equals(action)) { - Intent result = new Intent(); - result.putExtra(EXTRA_FINGERPRINT, fingerprint); - setResult(RESULT_OK, result); - finish(); - } else { - importKeys(fingerprint); - } - } else { + if (uri == null || uri.getScheme() == null || + !uri.getScheme().toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) { SingletonResult result = new SingletonResult( - SingletonResult.RESULT_ERROR, OperationResult.LogType.MSG_WRONG_QR_CODE); + SingletonResult.RESULT_ERROR, LogType.MSG_WRONG_QR_CODE); Intent intent = new Intent(); intent.putExtra(SingletonResult.EXTRA_RESULT, result); returnResult(intent); + return; + } + final String fingerprint = uri.getEncodedSchemeSpecificPart().toLowerCase(Locale.ENGLISH); + if (!fingerprint.matches("[a-fA-F0-9]{40}")) { + SingletonResult result = new SingletonResult( + SingletonResult.RESULT_ERROR, LogType.MSG_WRONG_QR_CODE_FP); + Intent intent = new Intent(); + intent.putExtra(SingletonResult.EXTRA_RESULT, result); + returnResult(intent); + return; + } + + if (ACTION_SCAN_WITH_RESULT.equals(action)) { + Intent result = new Intent(); + result.putExtra(EXTRA_FINGERPRINT, fingerprint); + setResult(RESULT_OK, result); + finish(); + } else { + importKeys(fingerprint); } + } public void returnResult(Intent data) { @@ -194,77 +213,55 @@ public class ImportKeysProxyActivity extends FragmentActivity { private void startImportService(ArrayList<ParcelableKeyRing> keyRings) { - // Message is received after importing is done in KeychainIntentService - ServiceProgressHandler serviceHandler = new ServiceProgressHandler( - this, - getString(R.string.progress_importing), - ProgressDialog.STYLE_HORIZONTAL, - true, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - if (returnData == null) { - finish(); - return; - } - final ImportKeyResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null) { - Log.e(Constants.TAG, "result == null"); - finish(); - return; - } - - if (!result.success()) { - // only return if no success... - Intent data = new Intent(); - data.putExtras(returnData); - returnResult(data); - return; - } - - Intent certifyIntent = new Intent(ImportKeysProxyActivity.this, - CertifyKeyActivity.class); - certifyIntent.putExtra(CertifyKeyActivity.EXTRA_RESULT, result); - certifyIntent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, - result.getImportedMasterKeyIds()); - startActivityForResult(certifyIntent, 0); - } - } - }; - - // fill values for this action - Bundle data = new Bundle(); - // search config { Preferences prefs = Preferences.getPreferences(this); Preferences.CloudSearchPrefs cloudPrefs = new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver()); - data.putString(KeychainIntentService.IMPORT_KEY_SERVER, cloudPrefs.keyserver); + mKeyserver = cloudPrefs.keyserver; } - data.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, keyRings); + mKeyList = keyRings; - // Send all information needed to service to query keys in other thread - Intent intent = new Intent(this, KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + mImportOpHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_importing); - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(serviceHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + mImportOpHelper.cryptoOperation(); + } + + + // CryptoOperationHelper.Callback methods - // show progress dialog - serviceHandler.showProgressDialog(this); + @Override + public ImportKeyringParcel createOperationInput() { + return new ImportKeyringParcel(mKeyList, mKeyserver); + } - // start service with intent - startService(intent); + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + Intent certifyIntent = new Intent(this, CertifyKeyActivity.class); + certifyIntent.putExtra(CertifyKeyActivity.EXTRA_RESULT, result); + certifyIntent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, + result.getImportedMasterKeyIds()); + startActivityForResult(certifyIntent, 0); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + Bundle returnData = new Bundle(); + returnData.putParcelable(OperationResult.EXTRA_RESULT, result); + Intent data = new Intent(); + data.putExtras(returnData); + returnResult(data); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; } /** @@ -276,7 +273,8 @@ public class ImportKeysProxyActivity extends FragmentActivity { // only one message sent during the beam NdefMessage msg = (NdefMessage) rawMsgs[0]; // record 0 contains the MIME type, record 1 is the AAR, if present - byte[] receivedKeyringBytes = msg.getRecords()[0].getPayload(); + final byte[] receivedKeyringBytes = msg.getRecords()[0].getPayload(); + importKeys(receivedKeyringBytes); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index d8c3e0350..ce6994ba4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -21,22 +21,21 @@ package org.sufficientlysecure.keychain.ui; import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.app.Activity; -import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.MergeCursor; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.os.Messenger; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; +import android.text.TextUtils; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; @@ -52,51 +51,45 @@ import android.widget.TextView; import com.getbase.floatingactionbutton.FloatingActionButton; import com.getbase.floatingactionbutton.FloatingActionsMenu; - import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; -import org.sufficientlysecure.keychain.operations.results.DeleteResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.CloudImportService; -import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.ConsolidateInputParcel; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; -import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.service.PassphraseCacheService; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.util.ExportHelper; import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; -import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; -import se.emilsjolander.stickylistheaders.StickyListHeadersListView; - /** * Public key list with sticky list headers. It does _not_ extend ListFragment because it uses * StickyListHeaders library which does not extend upon ListView. */ public class KeyListFragment extends LoaderFragment implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener, - LoaderManager.LoaderCallbacks<Cursor>, FabContainer { - - static final int REQUEST_REPEAT_PASSPHRASE = 1; - static final int REQUEST_ACTION = 2; + LoaderManager.LoaderCallbacks<Cursor>, FabContainer, + CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> { - ExportHelper mExportHelper; + static final int REQUEST_ACTION = 1; + private static final int REQUEST_DELETE = 2; + private static final int REQUEST_VIEW_KEY = 3; private KeyListAdapter mAdapter; private StickyListHeadersListView mStickyList; @@ -108,17 +101,13 @@ public class KeyListFragment extends LoaderFragment private FloatingActionsMenu mFab; - // This ids for multiple key export. - private ArrayList<Long> mIdsForRepeatAskPassphrase; - // This index for remembering the number of master key. - private int mIndex; + // for CryptoOperationHelper import + private ArrayList<ParcelableKeyRing> mKeyList; + private String mKeyserver; + private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mExportHelper = new ExportHelper(getActivity()); - } + // for ConsolidateOperation + private CryptoOperationHelper<ConsolidateInputParcel, ConsolidateResult> mConsolidateOpHelper; /** * Load custom layout with StickyListView from library @@ -230,19 +219,7 @@ public class KeyListFragment extends LoaderFragment } case R.id.menu_key_list_multi_delete: { ids = mAdapter.getCurrentSelectedMasterKeyIds(); - showDeleteKeyDialog(mode, ids, mAdapter.isAnySecretSelected()); - break; - } - case R.id.menu_key_list_multi_export: { - ids = mAdapter.getCurrentSelectedMasterKeyIds(); - showMultiExportDialog(ids); - break; - } - case R.id.menu_key_list_multi_select_all: { - // select all - for (int i = 0; i < mAdapter.getCount(); i++) { - mStickyList.setItemChecked(i, true); - } + showDeleteKeyDialog(ids, mAdapter.isAnySecretSelected()); break; } } @@ -289,7 +266,6 @@ public class KeyListFragment extends LoaderFragment static final String ORDER = KeyRings.HAS_ANY_SECRET + " DESC, UPPER(" + KeyRings.USER_ID + ") ASC"; - @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This @@ -322,6 +298,22 @@ public class KeyListFragment extends LoaderFragment // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) mAdapter.setSearchQuery(mQuery); + + if (data != null && (mQuery == null || TextUtils.isEmpty(mQuery))) { + boolean isSecret = data.moveToFirst() && data.getInt(KeyListAdapter.INDEX_HAS_ANY_SECRET) != 0; + if (!isSecret) { + MatrixCursor headerCursor = new MatrixCursor(KeyListAdapter.PROJECTION); + Long[] row = new Long[KeyListAdapter.PROJECTION.length]; + row[KeyListAdapter.INDEX_HAS_ANY_SECRET] = 1L; + row[KeyListAdapter.INDEX_MASTER_KEY_ID] = 0L; + headerCursor.addRow(row); + + Cursor dataCursor = data; + data = new MergeCursor(new Cursor[] { + headerCursor, dataCursor + }); + } + } mAdapter.swapCursor(data); mStickyList.setAdapter(mAdapter); @@ -358,7 +350,7 @@ public class KeyListFragment extends LoaderFragment Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class); viewIntent.setData( KeyRings.buildGenericKeyRingUri(mAdapter.getMasterKeyId(position))); - startActivity(viewIntent); + startActivityForResult(viewIntent, REQUEST_VIEW_KEY); } protected void encrypt(ActionMode mode, long[] masterKeyIds) { @@ -376,38 +368,15 @@ public class KeyListFragment extends LoaderFragment * * @param hasSecret must contain whether the list of masterKeyIds contains a secret key or not */ - public void showDeleteKeyDialog(final ActionMode mode, long[] masterKeyIds, boolean hasSecret) { - // Can only work on singular secret keys - if (hasSecret && masterKeyIds.length > 1) { - Notify.create(getActivity(), R.string.secret_cannot_multiple, - Notify.Style.ERROR).show(); - return; + public void showDeleteKeyDialog(long[] masterKeyIds, boolean hasSecret) { + Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class); + intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, masterKeyIds); + intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret); + if (hasSecret) { + intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER, + Preferences.getPreferences(getActivity()).getPreferredKeyserver()); } - - // Message is received after key is deleted - Handler returnHandler = new Handler() { - @Override - public void handleMessage(Message message) { - if (message.arg1 == DeleteKeyDialogFragment.MESSAGE_OKAY) { - Bundle data = message.getData(); - if (data != null) { - DeleteResult result = data.getParcelable(DeleteResult.EXTRA_RESULT); - if (result != null) { - result.createNotify(getActivity()).show(); - } - } - mode.finish(); - } - } - }; - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(returnHandler); - - DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger, - masterKeyIds); - - deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog"); + startActivityForResult(intent, REQUEST_DELETE); } @@ -462,18 +431,10 @@ public class KeyListFragment extends LoaderFragment createKey(); return true; - case R.id.menu_key_list_export: - mExportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true); - return true; - case R.id.menu_key_list_update_all_keys: updateAllKeys(); return true; - case R.id.menu_key_list_debug_cons: - consolidate(); - return true; - case R.id.menu_key_list_debug_read: try { KeychainDatabase.debugBackup(getActivity(), true); @@ -504,6 +465,10 @@ public class KeyListFragment extends LoaderFragment getActivity().finish(); return true; + case R.id.menu_key_list_debug_cons: + consolidate(); + return true; + default: return super.onOptionsItemSelected(item); } @@ -555,9 +520,12 @@ public class KeyListFragment extends LoaderFragment } private void updateAllKeys() { - Context context = getActivity(); + Activity activity = getActivity(); + if (activity == null) { + return; + } - ProviderHelper providerHelper = new ProviderHelper(context); + ProviderHelper providerHelper = new ProviderHelper(activity); Cursor cursor = providerHelper.getContentResolver().query( KeyRings.buildUnifiedKeyRingsUri(), new String[]{ @@ -565,182 +533,115 @@ public class KeyListFragment extends LoaderFragment }, null, null, null ); - ArrayList<ParcelableKeyRing> keyList = new ArrayList<>(); - - while (cursor.moveToNext()) { - byte[] blob = cursor.getBlob(0);//fingerprint column is 0 - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob); - ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null, null); - keyList.add(keyEntry); + if (cursor == null) { + Notify.create(activity, R.string.error_loading_keys, Notify.Style.ERROR); + return; } - ServiceProgressHandler serviceHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_updating), - ProgressDialog.STYLE_HORIZONTAL, - true, - ProgressDialogFragment.ServiceType.CLOUD_IMPORT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - if (returnData == null) { - return; - } - final ImportKeyResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null) { - Log.e(Constants.TAG, "result == null"); - return; - } - - result.createNotify(getActivity()).show(); - } + ArrayList<ParcelableKeyRing> keyList = new ArrayList<>(); + try { + while (cursor.moveToNext()) { + byte[] blob = cursor.getBlob(0);//fingerprint column is 0 + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob); + ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null, null); + keyList.add(keyEntry); } - }; - - // Send all information needed to service to query keys in other thread - Intent intent = new Intent(getActivity(), CloudImportService.class); - - // fill values for this action - Bundle data = new Bundle(); + mKeyList = keyList; + } finally { + cursor.close(); + } // search config { Preferences prefs = Preferences.getPreferences(getActivity()); Preferences.CloudSearchPrefs cloudPrefs = new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver()); - data.putString(CloudImportService.IMPORT_KEY_SERVER, cloudPrefs.keyserver); + mKeyserver = cloudPrefs.keyserver; } - data.putParcelableArrayList(CloudImportService.IMPORT_KEY_LIST, keyList); - - intent.putExtra(CloudImportService.EXTRA_DATA, data); - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(serviceHandler); - intent.putExtra(CloudImportService.EXTRA_MESSENGER, messenger); - - // show progress dialog - serviceHandler.showProgressDialog(getActivity()); - - // start service with intent - getActivity().startService(intent); + mImportOpHelper = new CryptoOperationHelper<>(1, this, + this, R.string.progress_updating); + mImportOpHelper.cryptoOperation(); } private void consolidate() { - // Message is received after importing is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_importing), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - if (returnData == null) { - return; - } - final ConsolidateResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null) { - return; - } - result.createNotify(getActivity()).show(); - } + CryptoOperationHelper.Callback<ConsolidateInputParcel, ConsolidateResult> callback + = new CryptoOperationHelper.Callback<ConsolidateInputParcel, ConsolidateResult>() { + + @Override + public ConsolidateInputParcel createOperationInput() { + return new ConsolidateInputParcel(false); // we want to perform a full consolidate } - }; - // Send all information needed to service to import key in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); + @Override + public void onCryptoOperationSuccess(ConsolidateResult result) { + result.createNotify(getActivity()).show(); + } - intent.setAction(KeychainIntentService.ACTION_CONSOLIDATE); + @Override + public void onCryptoOperationCancelled() { - // fill values for this action - Bundle data = new Bundle(); + } - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + @Override + public void onCryptoOperationError(ConsolidateResult result) { + result.createNotify(getActivity()).show(); + } - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; - // show progress dialog - saveHandler.showProgressDialog(getActivity()); + mConsolidateOpHelper = + new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing); - // start service with intent - getActivity().startService(intent); + mConsolidateOpHelper.cryptoOperation(); } - private void showMultiExportDialog(long[] masterKeyIds) { - mIdsForRepeatAskPassphrase = new ArrayList<>(); - for (long id : masterKeyIds) { - try { - if (PassphraseCacheService.getCachedPassphrase( - getActivity(), id, id) == null) { - mIdsForRepeatAskPassphrase.add(id); - } - } catch (PassphraseCacheService.KeyNotFoundException e) { - // This happens when the master key is stripped - // and ignore this key. - } - } - mIndex = 0; - if (mIdsForRepeatAskPassphrase.size() != 0) { - startPassphraseActivity(); - return; + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mImportOpHelper != null) { + mImportOpHelper.handleActivityResult(requestCode, resultCode, data); } - long[] idsForMultiExport = new long[mIdsForRepeatAskPassphrase.size()]; - for (int i = 0; i < mIdsForRepeatAskPassphrase.size(); ++i) { - idsForMultiExport[i] = mIdsForRepeatAskPassphrase.get(i); + + if (mConsolidateOpHelper != null) { + mConsolidateOpHelper.handleActivityResult(requestCode, resultCode, data); } - mExportHelper.showExportKeysDialog(idsForMultiExport, - Constants.Path.APP_DIR_FILE, - mAdapter.isAnySecretSelected()); - } - private void startPassphraseActivity() { - Intent intent = new Intent(getActivity(), PassphraseDialogActivity.class); - long masterKeyId = mIdsForRepeatAskPassphrase.get(mIndex++); - intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, masterKeyId); - startActivityForResult(intent, REQUEST_REPEAT_PASSPHRASE); - } + switch (requestCode) { + case REQUEST_DELETE: + if (mActionMode != null) { + mActionMode.finish(); + } + if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { + OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); + result.createNotify(getActivity()).show(); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + break; - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_REPEAT_PASSPHRASE) { - if (resultCode != Activity.RESULT_OK) { - return; - } - if (mIndex < mIdsForRepeatAskPassphrase.size()) { - startPassphraseActivity(); - return; - } - long[] idsForMultiExport = new long[mIdsForRepeatAskPassphrase.size()]; - for (int i = 0; i < mIdsForRepeatAskPassphrase.size(); ++i) { - idsForMultiExport[i] = mIdsForRepeatAskPassphrase.get(i); - } - mExportHelper.showExportKeysDialog(idsForMultiExport, - Constants.Path.APP_DIR_FILE, - mAdapter.isAnySecretSelected()); - } + case REQUEST_ACTION: + // if a result has been returned, display a notify + if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { + OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); + result.createNotify(getActivity()).show(); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + break; - if (requestCode == REQUEST_ACTION) { - // if a result has been returned, display a notify - if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { - OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); - result.createNotify(getActivity()).show(); - } else { - super.onActivityResult(requestCode, resultCode, data); - } + case REQUEST_VIEW_KEY: + if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { + OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); + result.createNotify(getActivity()).show(); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + break; } } @@ -761,12 +662,41 @@ public class KeyListFragment extends LoaderFragment anim.start(); } + // CryptoOperationHelper.Callback methods + @Override + public ImportKeyringParcel createOperationInput() { + return new ImportKeyringParcel(mKeyList, mKeyserver); + } + + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + result.createNotify(getActivity()).show(); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + result.createNotify(getActivity()).show(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + public class KeyListAdapter extends KeyAdapter implements StickyListHeadersAdapter { private HashMap<Integer, Boolean> mSelection = new HashMap<>(); + private Context mContext; + public KeyListAdapter(Context context, Cursor c, int flags) { super(context, c, flags); + mContext = context; } @Override @@ -795,9 +725,11 @@ public class KeyListFragment extends LoaderFragment // let the adapter handle setting up the row views View v = super.getView(position, convertView, parent); + int colorEmphasis = FormattingUtils.getColorFromAttr(mContext, R.attr.colorEmphasis); + if (mSelection.get(position) != null) { // selected position color - v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis)); + v.setBackgroundColor(colorEmphasis); } else { // default color v.setBackgroundColor(Color.TRANSPARENT); @@ -806,6 +738,29 @@ public class KeyListFragment extends LoaderFragment return v; } + @Override + public void bindView(View view, Context context, Cursor cursor) { + boolean isSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0; + long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); + if (isSecret && masterKeyId == 0L) { + + // sort of a hack: if this item isn't enabled, we make it clickable + // to intercept its click events + view.setClickable(true); + + KeyItemViewHolder h = (KeyItemViewHolder) view.getTag(); + h.setDummy(new OnClickListener() { + @Override + public void onClick(View v) { + createKey(); + } + }); + return; + } + + super.bindView(view, context, cursor); + } + private class HeaderViewHolder { TextView mText; TextView mCount; @@ -844,6 +799,10 @@ public class KeyListFragment extends LoaderFragment if (mCursor.getInt(INDEX_HAS_ANY_SECRET) != 0) { { // set contact count int num = mCursor.getCount(); + // If this is a dummy secret key, subtract one + if (mCursor.getLong(INDEX_MASTER_KEY_ID) == 0L) { + num -= 1; + } String contactsTotal = mContext.getResources().getQuantityString(R.plurals.n_keys, num, num); holder.mCount.setText(contactsTotal); holder.mCount.setVisibility(View.VISIBLE); @@ -902,8 +861,9 @@ public class KeyListFragment extends LoaderFragment public boolean isAnySecretSelected() { for (int pos : mSelection.keySet()) { - if (isSecretAvailable(pos)) + if (isSecretAvailable(pos)) { return true; + } } return false; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java index 138f2f4e7..4de83337e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java @@ -43,13 +43,13 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogLevel; import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.Log; import java.io.File; import java.io.FileNotFoundException; import java.io.PrintWriter; -import java.util.Iterator; public class LogDisplayFragment extends ListFragment implements OnItemClickListener { @@ -58,9 +58,12 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe OperationResult mResult; public static final String EXTRA_RESULT = "log"; + protected int mTextColor; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mTextColor = FormattingUtils.getColorFromAttr(getActivity(), R.attr.colorText); setHasOptionsMenu(true); } @@ -75,7 +78,12 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe return; } - mResult = intent.getParcelableExtra(EXTRA_RESULT); + if (savedInstanceState != null) { + mResult = savedInstanceState.getParcelable(EXTRA_RESULT); + } else { + mResult = intent.getParcelableExtra(EXTRA_RESULT); + } + if (mResult == null) { getActivity().finish(); return; @@ -92,6 +100,14 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe } @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + // need to parcel this again, logs are only single-instance parcelable + outState.putParcelable(EXTRA_RESULT, mResult); + } + + @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { inflater.inflate(R.menu.log_display, menu); @@ -110,7 +126,6 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe } private void exportLog() { - showExportLogDialog(new File(Constants.Path.APP_DIR, "export.log")); } @@ -142,7 +157,9 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe } } - if (!error) currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_SUCCESS, 1); + if (!error) { + currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_SUCCESS, 1); + } int opResultCode = error ? OperationResult.RESULT_ERROR : OperationResult.RESULT_OK; OperationResult opResult = new LogExportResult(opResultCode, currLog); @@ -158,8 +175,8 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe */ private String getPrintableOperationLog(OperationResult.OperationLog opLog, String basePadding) { String log = ""; - for (Iterator<LogEntryParcel> logIterator = opLog.iterator(); logIterator.hasNext(); ) { - log += getPrintableLogEntry(logIterator.next(), basePadding) + "\n"; + for (LogEntryParcel anOpLog : opLog) { + log += getPrintableLogEntry(anOpLog, basePadding) + "\n"; } log = log.substring(0, log.length() - 1);//gets rid of extra new line return log; @@ -235,7 +252,7 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe String message = this.getString(R.string.specify_file_to_export_log_to); - FileHelper.saveFile(new FileHelper.FileDialogCallback() { + FileHelper.saveDocumentDialog(new FileHelper.FileDialogCallback() { @Override public void onFileSelected(File file, boolean checked) { writeToLogFile(mResult.getLog(), file); @@ -343,13 +360,13 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe ih.mSecondText.setText(getResources().getString(subEntry.mType.getMsgId(), subEntry.mParameters)); } - ih.mSecondText.setTextColor(subEntry.mType.mLevel == LogLevel.DEBUG ? Color.GRAY : Color.BLACK); + ih.mSecondText.setTextColor(subEntry.mType.mLevel == LogLevel.DEBUG ? Color.GRAY : mTextColor); switch (subEntry.mType.mLevel) { case DEBUG: ih.mSecondImg.setBackgroundColor(Color.GRAY); break; - case INFO: ih.mSecondImg.setBackgroundColor(Color.BLACK); break; + case INFO: ih.mSecondImg.setBackgroundColor(mTextColor); break; case WARN: ih.mSecondImg.setBackgroundColor(getResources().getColor(R.color.android_orange_light)); break; case ERROR: ih.mSecondImg.setBackgroundColor(getResources().getColor(R.color.android_red_light)); break; - case START: ih.mSecondImg.setBackgroundColor(Color.BLACK); break; + case START: ih.mSecondImg.setBackgroundColor(mTextColor); break; case OK: ih.mSecondImg.setBackgroundColor(getResources().getColor(R.color.android_green_light)); break; case CANCELLED: ih.mSecondImg.setBackgroundColor(getResources().getColor(R.color.android_red_light)); break; } @@ -374,13 +391,13 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe entry.mParameters)); } convertView.setPadding((entry.mIndent) * dipFactor, 0, 0, 0); - ih.mText.setTextColor(entry.mType.mLevel == LogLevel.DEBUG ? Color.GRAY : Color.BLACK); + ih.mText.setTextColor(entry.mType.mLevel == LogLevel.DEBUG ? Color.GRAY : mTextColor); switch (entry.mType.mLevel) { case DEBUG: ih.mImg.setBackgroundColor(Color.GRAY); break; - case INFO: ih.mImg.setBackgroundColor(Color.BLACK); break; + case INFO: ih.mImg.setBackgroundColor(mTextColor); break; case WARN: ih.mImg.setBackgroundColor(getResources().getColor(R.color.android_orange_light)); break; case ERROR: ih.mImg.setBackgroundColor(getResources().getColor(R.color.android_red_light)); break; - case START: ih.mImg.setBackgroundColor(Color.BLACK); break; + case START: ih.mImg.setBackgroundColor(mTextColor); break; case OK: ih.mImg.setBackgroundColor(getResources().getColor(R.color.android_green_light)); break; case CANCELLED: ih.mImg.setBackgroundColor(getResources().getColor(R.color.android_red_light)); break; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java index f571ba1e6..6f5d98afd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -23,8 +23,8 @@ import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentManager.OnBackStackChangedListener; import android.support.v4.app.FragmentTransaction; -import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.View; import android.widget.AdapterView; @@ -33,23 +33,31 @@ import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; import com.mikepenz.iconics.typeface.FontAwesome; import com.mikepenz.materialdrawer.Drawer; +import com.mikepenz.materialdrawer.DrawerBuilder; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.remote.ui.AppsListFragment; +import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Preferences; -public class MainActivity extends AppCompatActivity implements FabContainer { +public class MainActivity extends BaseNfcActivity implements FabContainer, OnBackStackChangedListener { - public Drawer.Result result; + static final int ID_KEYS = 1; + static final int ID_ENCRYPT_DECRYPT = 2; + static final int ID_APPS = 3; + static final int ID_BACKUP = 4; + static final int ID_SETTINGS = 5; + static final int ID_HELP = 6; - private KeyListFragment mKeyListFragment ; - private AppsListFragment mAppsListFragment; - private EncryptDecryptOverviewFragment mEncryptDecryptOverviewFragment; - private Fragment mLastUsedFragment; + // both of these are used for instrumentation testing only + public static final String EXTRA_SKIP_FIRST_TIME = "skip_first_time"; + public static final String EXTRA_INIT_FRAG = "init_frag"; + + public Drawer mDrawer; private Toolbar mToolbar; @Override @@ -57,50 +65,51 @@ public class MainActivity extends AppCompatActivity implements FabContainer { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); - //initialize FragmentLayout with KeyListFragment at first - Fragment mainFragment = new KeyListFragment(); - FragmentManager fm = getSupportFragmentManager(); - FragmentTransaction transaction = fm.beginTransaction(); - transaction.replace(R.id.main_fragment_container, mainFragment); - transaction.commit(); - mToolbar = (Toolbar) findViewById(R.id.toolbar); mToolbar.setTitle(R.string.app_name); setSupportActionBar(mToolbar); - result = new Drawer() + mDrawer = new DrawerBuilder() .withActivity(this) .withHeader(R.layout.main_drawer_header) .withToolbar(mToolbar) .addDrawerItems( - new PrimaryDrawerItem().withName(R.string.nav_keys).withIcon(CommunityMaterial.Icon.cmd_key).withIdentifier(1).withCheckable(false), - new PrimaryDrawerItem().withName(R.string.nav_encrypt_decrypt).withIcon(FontAwesome.Icon.faw_lock).withIdentifier(2).withCheckable(false), - new PrimaryDrawerItem().withName(R.string.title_api_registered_apps).withIcon(CommunityMaterial.Icon.cmd_apps).withIdentifier(3).withCheckable(false) + new PrimaryDrawerItem().withName(R.string.nav_keys).withIcon(CommunityMaterial.Icon.cmd_key) + .withIdentifier(ID_KEYS).withCheckable(false), + new PrimaryDrawerItem().withName(R.string.nav_encrypt_decrypt).withIcon(FontAwesome.Icon.faw_lock) + .withIdentifier(ID_ENCRYPT_DECRYPT).withCheckable(false), + new PrimaryDrawerItem().withName(R.string.title_api_registered_apps).withIcon(CommunityMaterial.Icon.cmd_apps) + .withIdentifier(ID_APPS).withCheckable(false), + new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore) + .withIdentifier(ID_BACKUP).withCheckable(false) ) .addStickyDrawerItems( // display and stick on bottom of drawer - new PrimaryDrawerItem().withName(R.string.menu_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(4).withCheckable(false), - new PrimaryDrawerItem().withName(R.string.menu_help).withIcon(CommunityMaterial.Icon.cmd_help_circle).withIdentifier(5).withCheckable(false) + new PrimaryDrawerItem().withName(R.string.menu_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(ID_SETTINGS).withCheckable(false), + new PrimaryDrawerItem().withName(R.string.menu_help).withIcon(CommunityMaterial.Icon.cmd_help_circle).withIdentifier(ID_HELP).withCheckable(false) ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id, IDrawerItem drawerItem) { + public boolean onItemClick(AdapterView<?> parent, View view, int position, long id, IDrawerItem drawerItem) { if (drawerItem != null) { Intent intent = null; switch(drawerItem.getIdentifier()) { - case 1: + case ID_KEYS: onKeysSelected(); break; - case 2: + case ID_ENCRYPT_DECRYPT: onEnDecryptSelected(); break; - case 3: + case ID_APPS: onAppsSelected(); break; - case 4: + case ID_BACKUP: + onBackupSelected(); + break; + case ID_SETTINGS: intent = new Intent(MainActivity.this, SettingsActivity.class); break; - case 5: + case ID_HELP: intent = new Intent(MainActivity.this, HelpActivity.class); break; } @@ -108,6 +117,8 @@ public class MainActivity extends AppCompatActivity implements FabContainer { MainActivity.this.startActivity(intent); } } + + return false; } }) .withSelectedItem(-1) @@ -116,7 +127,7 @@ public class MainActivity extends AppCompatActivity implements FabContainer { // if this is the first time show first time activity Preferences prefs = Preferences.getPreferences(this); - if (prefs.isFirstTime()) { + if (!getIntent().getBooleanExtra(EXTRA_SKIP_FIRST_TIME, false) && prefs.isFirstTime()) { Intent intent = new Intent(this, CreateKeyActivity.class); intent.putExtra(CreateKeyActivity.EXTRA_FIRST_TIME, true); startActivity(intent); @@ -124,82 +135,91 @@ public class MainActivity extends AppCompatActivity implements FabContainer { return; } + getSupportFragmentManager().addOnBackStackChangedListener(this); + + // all further initialization steps are saved as instance state + if (savedInstanceState != null) { + return; + } + Intent data = getIntent(); // If we got an EXTRA_RESULT in the intent, show the notification if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); result.createNotify(this).show(); } - } - private void clearFragments() { - mKeyListFragment = null; - mAppsListFragment = null; - mEncryptDecryptOverviewFragment = null; + // always initialize keys fragment to the bottom of the backstack + onKeysSelected(); - getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); - } + if (data != null && data.hasExtra(EXTRA_INIT_FRAG)) { + // initialize FragmentLayout with KeyListFragment at first + switch (data.getIntExtra(EXTRA_INIT_FRAG, -1)) { + case ID_ENCRYPT_DECRYPT: + onEnDecryptSelected(); + break; + case ID_APPS: + onAppsSelected(); + break; + } + } - private void setFragment(Fragment fragment) { - setFragment(fragment, true); } private void setFragment(Fragment fragment, boolean addToBackStack) { - this.mLastUsedFragment = fragment; - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); + + FragmentTransaction ft = fragmentManager.beginTransaction(); ft.replace(R.id.main_fragment_container, fragment); if (addToBackStack) { ft.addToBackStack(null); } ft.commit(); + } - private boolean onKeysSelected() { + private void onKeysSelected() { mToolbar.setTitle(R.string.app_name); - clearFragments(); - - if (mKeyListFragment == null) { - mKeyListFragment = new KeyListFragment(); - } - - setFragment(mKeyListFragment, false); - return true; + mDrawer.setSelectionByIdentifier(ID_KEYS, false); + Fragment frag = new KeyListFragment(); + setFragment(frag, false); } - private boolean onEnDecryptSelected() { + private void onEnDecryptSelected() { mToolbar.setTitle(R.string.nav_encrypt_decrypt); - clearFragments(); - if (mEncryptDecryptOverviewFragment == null) { - mEncryptDecryptOverviewFragment = new EncryptDecryptOverviewFragment(); - } - - setFragment(mEncryptDecryptOverviewFragment); - return true; + mDrawer.setSelectionByIdentifier(ID_ENCRYPT_DECRYPT, false); + Fragment frag = new EncryptDecryptOverviewFragment(); + setFragment(frag, true); } - private boolean onAppsSelected() { + private void onAppsSelected() { mToolbar.setTitle(R.string.nav_apps); - clearFragments(); - if (mAppsListFragment == null) { - mAppsListFragment = new AppsListFragment(); - } + mDrawer.setSelectionByIdentifier(ID_APPS, false); + Fragment frag = new AppsListFragment(); + setFragment(frag, true); + } - setFragment(mAppsListFragment); - return true; + private void onBackupSelected() { + mToolbar.setTitle(R.string.nav_backup); + mDrawer.setSelectionByIdentifier(ID_APPS, false); + Fragment frag = new BackupFragment(); + setFragment(frag, true); } @Override protected void onSaveInstanceState(Bundle outState) { - //add the values which need to be saved from the drawer to the bundle - outState = result.saveInstanceState(outState); + // add the values which need to be saved from the drawer to the bundle + outState = mDrawer.saveInstanceState(outState); super.onSaveInstanceState(outState); } @Override - public void onBackPressed(){ - //handle the back press :D close the drawer first and if the drawer is closed close the activity - if (result != null && result.isDrawerOpen()) { - result.closeDrawer(); + public void onBackPressed() { + // close the drawer first and if the drawer is closed do regular backstack handling + if (mDrawer != null && mDrawer.isDrawerOpen()) { + mDrawer.closeDrawer(); } else { super.onBackPressed(); } @@ -223,4 +243,32 @@ public class MainActivity extends AppCompatActivity implements FabContainer { } } + + @Override + public void onBackStackChanged() { + FragmentManager fragmentManager = getSupportFragmentManager(); + if (fragmentManager == null) { + return; + } + Fragment frag = fragmentManager.findFragmentById(R.id.main_fragment_container); + if (frag == null) { + return; + } + + // make sure the selected icon is the one shown at this point + if (frag instanceof KeyListFragment) { + mToolbar.setTitle(R.string.app_name); + mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_KEYS), false); + } else if (frag instanceof EncryptDecryptOverviewFragment) { + mToolbar.setTitle(R.string.nav_encrypt_decrypt); + mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_ENCRYPT_DECRYPT), false); + } else if (frag instanceof AppsListFragment) { + mToolbar.setTitle(R.string.nav_apps); + mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_APPS), false); + } else if (frag instanceof BackupFragment) { + mToolbar.setTitle(R.string.nav_backup); + mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_BACKUP), false); + } + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java index aa66053fa..b811b218e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java @@ -1,52 +1,120 @@ -/** - * Copyright (c) 2013-2014 Philipp Jakubeit, Signe Rüsch, Dominik Schürmann +/* + * Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2013-2014 Signe Rüsch + * Copyright (C) 2013-2014 Philipp Jakubeit + * + * 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. * - * Licensed under the Bouncy Castle License (MIT license). See LICENSE file for details. + * 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.ui; import android.content.Intent; +import android.os.AsyncTask; import android.os.Bundle; +import android.view.View; import android.view.WindowManager; +import android.widget.Button; +import android.widget.TextView; +import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.remote.CryptoInputParcelCacheService; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.OrientationUtils; +import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; /** * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant * NFC devices. - * <p/> * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf */ public class NfcOperationActivity extends BaseNfcActivity { public static final String EXTRA_REQUIRED_INPUT = "required_input"; + public static final String EXTRA_CRYPTO_INPUT = "crypto_input"; // passthrough for OpenPgpService public static final String EXTRA_SERVICE_INTENT = "data"; - public static final String RESULT_DATA = "result_data"; + public static final String RESULT_CRYPTO_INPUT = "result_data"; + + public ViewAnimator vAnimator; + public TextView vErrorText; + public Button vErrorTryAgainButton; private RequiredInputParcel mRequiredInput; private Intent mServiceIntent; + private static final byte[] BLANK_FINGERPRINT = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; + + private CryptoInputParcel mInputParcel; + + @Override + protected void initTheme() { + mThemeChanger = new ThemeChanger(this); + mThemeChanger.setThemes(R.style.Theme_Keychain_Light_Dialog, + R.style.Theme_Keychain_Dark_Dialog); + mThemeChanger.changeTheme(); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(Constants.TAG, "NfcOperationActivity.onCreate"); + // prevent annoying orientation changes while fumbling with the device + OrientationUtils.lockOrientation(this); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + mInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT); + + setTitle(R.string.nfc_text); + + vAnimator = (ViewAnimator) findViewById(R.id.view_animator); + vAnimator.setDisplayedChild(0); + vErrorText = (TextView) findViewById(R.id.nfc_activity_3_error_text); + vErrorTryAgainButton = (Button) findViewById(R.id.nfc_activity_3_error_try_again); + vErrorTryAgainButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + resumeTagHandling(); + + // obtain passphrase for this subkey + if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD) { + obtainYubiKeyPin(mRequiredInput); + } + vAnimator.setDisplayedChild(0); + } + }); + Intent intent = getIntent(); Bundle data = intent.getExtras(); @@ -54,49 +122,186 @@ public class NfcOperationActivity extends BaseNfcActivity { mServiceIntent = data.getParcelable(EXTRA_SERVICE_INTENT); // obtain passphrase for this subkey - obtainYubiKeyPin(RequiredInputParcel.createRequiredPassphrase(mRequiredInput)); + if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD) { + obtainYubiKeyPin(mRequiredInput); + } } @Override protected void initLayout() { - setContentView(R.layout.nfc_activity); + setContentView(R.layout.nfc_operation_activity); } @Override - protected void onNfcPerform() throws IOException { + public void onNfcPreExecute() { + // start with indeterminate progress + vAnimator.setDisplayedChild(1); + } - CryptoInputParcel inputParcel = new CryptoInputParcel(mRequiredInput.mSignatureTime); + @Override + protected void doNfcInBackground() throws IOException { switch (mRequiredInput.mType) { case NFC_DECRYPT: { - for (int i = 0; i < mRequiredInput.mInputHashes.length; i++) { - byte[] hash = mRequiredInput.mInputHashes[i]; - byte[] decryptedSessionKey = nfcDecryptSessionKey(hash); - inputParcel.addCryptoData(hash, decryptedSessionKey); + for (int i = 0; i < mRequiredInput.mInputData.length; i++) { + byte[] encryptedSessionKey = mRequiredInput.mInputData[i]; + byte[] decryptedSessionKey = nfcDecryptSessionKey(encryptedSessionKey); + mInputParcel.addCryptoData(encryptedSessionKey, decryptedSessionKey); } break; } case NFC_SIGN: { - for (int i = 0; i < mRequiredInput.mInputHashes.length; i++) { - byte[] hash = mRequiredInput.mInputHashes[i]; + mInputParcel.addSignatureTime(mRequiredInput.mSignatureTime); + + for (int i = 0; i < mRequiredInput.mInputData.length; i++) { + byte[] hash = mRequiredInput.mInputData[i]; int algo = mRequiredInput.mSignAlgos[i]; byte[] signedHash = nfcCalculateSignature(hash, algo); - inputParcel.addCryptoData(hash, signedHash); + mInputParcel.addCryptoData(hash, signedHash); } break; } + case NFC_MOVE_KEY_TO_CARD: { + // TODO: assume PIN and Admin PIN to be default for this operation + mPin = new Passphrase("123456"); + mAdminPin = new Passphrase("12345678"); + + ProviderHelper providerHelper = new ProviderHelper(this); + CanonicalizedSecretKeyRing secretKeyRing; + try { + secretKeyRing = providerHelper.getCanonicalizedSecretKeyRing( + KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(mRequiredInput.getMasterKeyId()) + ); + } catch (ProviderHelper.NotFoundException e) { + throw new IOException("Couldn't find subkey for key to card operation."); + } + + byte[] newPin = mRequiredInput.mInputData[0]; + byte[] newAdminPin = mRequiredInput.mInputData[1]; + + for (int i = 2; i < mRequiredInput.mInputData.length; i++) { + byte[] subkeyBytes = mRequiredInput.mInputData[i]; + ByteBuffer buf = ByteBuffer.wrap(subkeyBytes); + long subkeyId = buf.getLong(); + + CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId); + + long keyGenerationTimestampMillis = key.getCreationTime().getTime(); + long keyGenerationTimestamp = keyGenerationTimestampMillis / 1000; + byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array(); + byte[] cardSerialNumber = Arrays.copyOf(nfcGetAid(), 16); + + Passphrase passphrase; + try { + passphrase = PassphraseCacheService.getCachedPassphrase(this, + mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId()); + } catch (PassphraseCacheService.KeyNotFoundException e) { + throw new IOException("Unable to get cached passphrase!"); + } + + if (key.canSign() || key.canCertify()) { + if (shouldPutKey(key.getFingerprint(), 0)) { + nfcPutKey(0xB6, key, passphrase); + nfcPutData(0xCE, timestampBytes); + nfcPutData(0xC7, key.getFingerprint()); + } else { + throw new IOException("Key slot occupied; card must be reset to put new signature key."); + } + } else if (key.canEncrypt()) { + if (shouldPutKey(key.getFingerprint(), 1)) { + nfcPutKey(0xB8, key, passphrase); + nfcPutData(0xCF, timestampBytes); + nfcPutData(0xC8, key.getFingerprint()); + } else { + throw new IOException("Key slot occupied; card must be reset to put new decryption key."); + } + } else if (key.canAuthenticate()) { + if (shouldPutKey(key.getFingerprint(), 2)) { + nfcPutKey(0xA4, key, passphrase); + nfcPutData(0xD0, timestampBytes); + nfcPutData(0xC9, key.getFingerprint()); + } else { + throw new IOException("Key slot occupied; card must be reset to put new authentication key."); + } + } else { + throw new IOException("Inappropriate key flags for smart card key."); + } + + // TODO: Is this really used anywhere? + mInputParcel.addCryptoData(subkeyBytes, cardSerialNumber); + } + + // change PINs afterwards + nfcModifyPIN(0x81, newPin); + nfcModifyPIN(0x83, newAdminPin); + + break; + } + default: { + throw new AssertionError("Unhandled mRequiredInput.mType"); + } } + } + + @Override + protected void onNfcPostExecute() throws IOException { if (mServiceIntent != null) { - CryptoInputParcelCacheService.addCryptoInputParcel(this, mServiceIntent, inputParcel); + // if we're triggered by OpenPgpService + // save updated cryptoInputParcel in cache + CryptoInputParcelCacheService.addCryptoInputParcel(this, mServiceIntent, mInputParcel); setResult(RESULT_OK, mServiceIntent); } else { Intent result = new Intent(); - result.putExtra(NfcOperationActivity.RESULT_DATA, inputParcel); + // send back the CryptoInputParcel we received + result.putExtra(RESULT_CRYPTO_INPUT, mInputParcel); setResult(RESULT_OK, result); } - finish(); + // show finish + vAnimator.setDisplayedChild(2); + + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + // check all 200ms if YubiKey has been taken away + while (true) { + if (isNfcConnected()) { + try { + Thread.sleep(200); + } catch (InterruptedException ignored) { + } + } else { + return null; + } + } + } + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + finish(); + } + }.execute(); + } + + @Override + protected void onNfcError(String error) { + pauseTagHandling(); + + vErrorText.setText(error + "\n\n" + getString(R.string.nfc_try_again_text)); + vAnimator.setDisplayedChild(3); + } + + private boolean shouldPutKey(byte[] fingerprint, int idx) throws IOException { + byte[] cardFingerprint = nfcGetFingerprint(idx); + // Slot is empty, or contains this key already. PUT KEY operation is safe + if (Arrays.equals(cardFingerprint, BLANK_FINGERPRINT) || + Arrays.equals(cardFingerprint, fingerprint)) { + return true; + } + + // Slot already contains a different key; don't overwrite it. + return false; } @Override @@ -114,8 +319,6 @@ public class NfcOperationActivity extends BaseNfcActivity { // clear (invalid) passphrase PassphraseCacheService.clearCachedPassphrase( this, mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId()); - - obtainYubiKeyPin(RequiredInputParcel.createRequiredPassphrase(mRequiredInput)); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java new file mode 100644 index 000000000..0e70cda14 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.ui; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.FragmentActivity; +import android.view.ContextThemeWrapper; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ParcelableProxy; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; + +/** + * Simply encapsulates a dialog. If orbot is not installed, it shows an install dialog, else a + * dialog to enable orbot. + */ +public class OrbotRequiredDialogActivity extends FragmentActivity + implements OrbotHelper.DialogActions { + + public static final int MESSAGE_ORBOT_STARTED = 1; + public static final int MESSAGE_ORBOT_IGNORE = 2; + public static final int MESSAGE_DIALOG_CANCEL = 3; + + // if suppplied and true will start Orbot directly without showing dialog + public static final String EXTRA_START_ORBOT = "start_orbot"; + // used for communicating results when triggered from a service + public static final String EXTRA_MESSENGER = "messenger"; + + // to provide any previous crypto input into which proxy preference is merged + public static final String EXTRA_CRYPTO_INPUT = "extra_crypto_input"; + + public static final String RESULT_CRYPTO_INPUT = "result_crypto_input"; + + private CryptoInputParcel mCryptoInputParcel; + private Messenger mMessenger; + + private ProgressDialog mShowOrbotProgressDialog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mCryptoInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT); + if (mCryptoInputParcel == null) { + // compatibility with usages that don't use a CryptoInputParcel + mCryptoInputParcel = new CryptoInputParcel(); + } + + mMessenger = getIntent().getParcelableExtra(EXTRA_MESSENGER); + + boolean startOrbotDirect = getIntent().getBooleanExtra(EXTRA_START_ORBOT, false); + if (startOrbotDirect) { + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(this); + mShowOrbotProgressDialog = new ProgressDialog(theme); + mShowOrbotProgressDialog.setTitle(R.string.progress_starting_orbot); + mShowOrbotProgressDialog.setCancelable(false); + mShowOrbotProgressDialog.show(); + OrbotHelper.bestPossibleOrbotStart(this, this, false); + } else { + showDialog(); + } + } + + /** + * Displays an install or start orbot dialog (or silent orbot start) depending on orbot's + * presence and state + */ + public void showDialog() { + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + public void run() { + + if (OrbotHelper.putOrbotInRequiredState(OrbotRequiredDialogActivity.this, + OrbotRequiredDialogActivity.this)) { + // no action required after all + onOrbotStarted(); + } + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case OrbotHelper.START_TOR_RESULT: { + dismissOrbotProgressDialog(); + // unfortunately, this result is returned immediately and not when Orbot is started + // 10s is approximately the longest time Orbot has taken to start + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + onOrbotStarted(); // assumption that orbot was started + } + }, 10000); + } + } + } + + /** + * for when Orbot is started without showing the dialog by the EXTRA_START_ORBOT intent extra + */ + private void dismissOrbotProgressDialog() { + if (mShowOrbotProgressDialog != null) { + mShowOrbotProgressDialog.dismiss(); + } + } + + @Override + public void onOrbotStarted() { + dismissOrbotProgressDialog(); + sendMessage(MESSAGE_ORBOT_STARTED); + Intent intent = new Intent(); + // send back unmodified CryptoInputParcel for a retry + intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel); + setResult(RESULT_OK, intent); + finish(); + } + + @Override + public void onNeutralButton() { + sendMessage(MESSAGE_ORBOT_IGNORE); + Intent intent = new Intent(); + mCryptoInputParcel.addParcelableProxy(ParcelableProxy.getForNoProxy()); + intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel); + setResult(RESULT_OK, intent); + finish(); + } + + @Override + public void onCancel() { + sendMessage(MESSAGE_DIALOG_CANCEL); + finish(); + } + + private void sendMessage(int what) { + if (mMessenger != null) { + Message msg = Message.obtain(); + msg.what = what; + try { + mMessenger.send(msg); + } catch (RemoteException e) { + Log.e(Constants.TAG, "Could not deliver message", e); + } + } + } +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java index c6431bfaf..e71349880 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java @@ -17,16 +17,18 @@ package org.sufficientlysecure.keychain.ui; + import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentActivity; +import android.support.v7.app.AlertDialog; import android.text.InputType; import android.text.method.PasswordTransformationMethod; import android.view.ContextThemeWrapper; @@ -43,20 +45,21 @@ import android.widget.Toast; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; -import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.remote.CryptoInputParcelCacheService; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; @@ -64,6 +67,8 @@ import org.sufficientlysecure.keychain.util.Preferences; /** * We can not directly create a dialog on the application context. * This activity encapsulates a DialogFragment to emulate a dialog. + * NOTE: If no CryptoInputParcel is passed via EXTRA_CRYPTO_INPUT, the CryptoInputParcel is created + * internally and is NOT meant to be used by signing operations before adding a signature time */ public class PassphraseDialogActivity extends FragmentActivity { @@ -71,11 +76,13 @@ public class PassphraseDialogActivity extends FragmentActivity { public static final String EXTRA_REQUIRED_INPUT = "required_input"; public static final String EXTRA_SUBKEY_ID = "secret_key_id"; + public static final String EXTRA_CRYPTO_INPUT = "crypto_input"; // special extra for OpenPgpService public static final String EXTRA_SERVICE_INTENT = "data"; + private long mSubKeyId; - private static final int REQUEST_CODE_ENTER_PATTERN = 2; + private CryptoInputParcel mCryptoInputParcel; @Override protected void onCreate(Bundle savedInstanceState) { @@ -90,20 +97,52 @@ public class PassphraseDialogActivity extends FragmentActivity { ); } + mCryptoInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT); + + if (mCryptoInputParcel == null) { + // not all usages of PassphraseActivity are from CryptoInputOperation + // NOTE: This CryptoInputParcel cannot be used for signing operations without setting + // signature time + mCryptoInputParcel = new CryptoInputParcel(); + } + // this activity itself has no content view (see manifest) - long keyId; if (getIntent().hasExtra(EXTRA_SUBKEY_ID)) { - keyId = getIntent().getLongExtra(EXTRA_SUBKEY_ID, 0); + mSubKeyId = getIntent().getLongExtra(EXTRA_SUBKEY_ID, 0); } else { RequiredInputParcel requiredInput = getIntent().getParcelableExtra(EXTRA_REQUIRED_INPUT); switch (requiredInput.mType) { case PASSPHRASE_SYMMETRIC: { - keyId = Constants.key.symmetric; + mSubKeyId = Constants.key.symmetric; break; } case PASSPHRASE: { - keyId = requiredInput.getSubKeyId(); + + // handle empty passphrases by directly returning an empty crypto input parcel + try { + CanonicalizedSecretKeyRing pubRing = + new ProviderHelper(this).getCanonicalizedSecretKeyRing( + requiredInput.getMasterKeyId()); + // use empty passphrase for empty passphrase + if (pubRing.getSecretKey(requiredInput.getSubKeyId()).getSecretKeyType() == + SecretKeyType.PASSPHRASE_EMPTY) { + // also return passphrase back to activity + Intent returnIntent = new Intent(); + mCryptoInputParcel.mPassphrase = new Passphrase(""); + returnIntent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel); + setResult(RESULT_OK, returnIntent); + finish(); + return; + } + } catch (NotFoundException e) { + Log.e(Constants.TAG, "Key not found?!", e); + setResult(RESULT_CANCELED); + finish(); + return; + } + + mSubKeyId = requiredInput.getSubKeyId(); break; } default: { @@ -112,64 +151,35 @@ public class PassphraseDialogActivity extends FragmentActivity { } } - Intent serviceIntent = getIntent().getParcelableExtra(EXTRA_SERVICE_INTENT); - - show(this, keyId, serviceIntent); } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_CODE_ENTER_PATTERN: { - /* - * NOTE that there are 4 possible result codes!!! - */ - switch (resultCode) { - case RESULT_OK: - // The user passed - break; - case RESULT_CANCELED: - // The user cancelled the task - break; -// case LockPatternActivity.RESULT_FAILED: -// // The user failed to enter the pattern -// break; -// case LockPatternActivity.RESULT_FORGOT_PATTERN: -// // The user forgot the pattern and invoked your recovery Activity. -// break; - } + protected void onResumeFragments() { + super.onResumeFragments(); - /* - * In any case, there's always a key EXTRA_RETRY_COUNT, which holds - * the number of tries that the user did. - */ -// int retryCount = data.getIntExtra( -// LockPatternActivity.EXTRA_RETRY_COUNT, 0); + /* Show passphrase dialog to cache a new passphrase the user enters for using it later for + * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks + * for a symmetric passphrase + */ - break; - } - } + Intent serviceIntent = getIntent().getParcelableExtra(EXTRA_SERVICE_INTENT); + + PassphraseDialogFragment frag = new PassphraseDialogFragment(); + Bundle args = new Bundle(); + args.putLong(EXTRA_SUBKEY_ID, mSubKeyId); + args.putParcelable(EXTRA_SERVICE_INTENT, serviceIntent); + frag.setArguments(args); + frag.show(getSupportFragmentManager(), "passphraseDialog"); } - /** - * Shows passphrase dialog to cache a new passphrase the user enters for using it later for - * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks - * for a symmetric passphrase - */ - public static void show(final FragmentActivity context, final long keyId, final Intent serviceIntent) { - DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { - public void run() { - // do NOT check if the key even needs a passphrase. that's not our job here. - PassphraseDialogFragment frag = new PassphraseDialogFragment(); - Bundle args = new Bundle(); - args.putLong(EXTRA_SUBKEY_ID, keyId); - args.putParcelable(EXTRA_SERVICE_INTENT, serviceIntent); - - frag.setArguments(args); - - frag.show(context.getSupportFragmentManager(), "passphraseDialog"); - } - }); + @Override + protected void onPause() { + super.onPause(); + + DialogFragment dialog = (DialogFragment) getSupportFragmentManager().findFragmentByTag("passphraseDialog"); + if (dialog != null) { + dialog.dismiss(); + } } public static class PassphraseDialogFragment extends DialogFragment implements TextView.OnEditorActionListener { @@ -183,17 +193,12 @@ public class PassphraseDialogActivity extends FragmentActivity { private Intent mServiceIntent; - /** - * Creates dialog - */ + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Activity activity = getActivity(); - // if the dialog is displayed from the application class, design is missing - // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay - ContextThemeWrapper theme = new ContextThemeWrapper(activity, - R.style.Theme_AppCompat_Light_Dialog); + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); mSubKeyId = getArguments().getLong(EXTRA_SUBKEY_ID); mServiceIntent = getArguments().getParcelable(EXTRA_SERVICE_INTENT); @@ -246,12 +251,7 @@ public class PassphraseDialogActivity extends FragmentActivity { userId = null; } - /* Get key type for message */ - // find a master key id for our key - long masterKeyId = new ProviderHelper(activity).getMasterKeyId(mSubKeyId); - CachedPublicKeyRing keyRing = new ProviderHelper(activity).getCachedPublicKeyRing(masterKeyId); - // get the type of key (from the database) - keyType = keyRing.getSecretKeyType(mSubKeyId); + keyType = mSecretRing.getSecretKey(mSubKeyId).getSecretKeyType(); switch (keyType) { case PASSPHRASE: message = getString(R.string.passphrase_for, userId); @@ -284,51 +284,42 @@ public class PassphraseDialogActivity extends FragmentActivity { mPassphraseText.setText(message); - if (keyType == CanonicalizedSecretKey.SecretKeyType.PATTERN) { - // start pattern dialog and show progress circle here... -// Intent patternActivity = new Intent(getActivity(), LockPatternActivity.class); -// patternActivity.putExtra(LockPatternActivity.EXTRA_PATTERN, "123"); -// startActivityForResult(patternActivity, REQUEST_CODE_ENTER_PATTERN); - mInput.setVisibility(View.INVISIBLE); - mProgress.setVisibility(View.VISIBLE); - } else { - // Hack to open keyboard. - // This is the only method that I found to work across all Android versions - // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/ - // Notes: * onCreateView can't be used because we want to add buttons to the dialog - // * opening in onActivityCreated does not work on Android 4.4 - mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - mPassphraseEditText.post(new Runnable() { - @Override - public void run() { - if (getActivity() == null || mPassphraseEditText == null) { - return; - } - InputMethodManager imm = (InputMethodManager) getActivity() - .getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT); + // Hack to open keyboard. + // This is the only method that I found to work across all Android versions + // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/ + // Notes: * onCreateView can't be used because we want to add buttons to the dialog + // * opening in onActivityCreated does not work on Android 4.4 + mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + mPassphraseEditText.post(new Runnable() { + @Override + public void run() { + if (getActivity() == null || mPassphraseEditText == null) { + return; } - }); - } - }); - mPassphraseEditText.requestFocus(); - - mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE); - mPassphraseEditText.setOnEditorActionListener(this); - - if (keyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD && Preferences.getPreferences(activity).useNumKeypadForYubiKeyPin()) { - mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } else if (keyType == CanonicalizedSecretKey.SecretKeyType.PIN) { - mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } else { - mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + InputMethodManager imm = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT); + } + }); } + }); + mPassphraseEditText.requestFocus(); + + mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE); + mPassphraseEditText.setOnEditorActionListener(this); + if ((keyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD && Preferences.getPreferences(activity).useNumKeypadForYubiKeyPin()) + || keyType == CanonicalizedSecretKey.SecretKeyType.PIN) { + mPassphraseEditText.setInputType(InputType.TYPE_CLASS_NUMBER); mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + } else { + mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } + mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + AlertDialog dialog = alert.create(); dialog.setButton(DialogInterface.BUTTON_POSITIVE, activity.getString(R.string.btn_unlock), (DialogInterface.OnClickListener) null); @@ -347,11 +338,16 @@ public class PassphraseDialogActivity extends FragmentActivity { public void onClick(View v) { final Passphrase passphrase = new Passphrase(mPassphraseEditText); + CryptoInputParcel cryptoInputParcel = + ((PassphraseDialogActivity) getActivity()).mCryptoInputParcel; + // Early breakout if we are dealing with a symmetric key if (mSecretRing == null) { - PassphraseCacheService.addCachedPassphrase(getActivity(), - Constants.key.symmetric, Constants.key.symmetric, passphrase, - getString(R.string.passp_cache_notif_pwd)); + if (cryptoInputParcel.mCachePassphrase) { + PassphraseCacheService.addCachedPassphrase(getActivity(), + Constants.key.symmetric, Constants.key.symmetric, passphrase, + getString(R.string.passp_cache_notif_pwd)); + } finishCaching(passphrase); return; @@ -404,15 +400,24 @@ public class PassphraseDialogActivity extends FragmentActivity { return; } - // cache the new passphrase - Log.d(Constants.TAG, "Everything okay! Caching entered passphrase"); + // cache the new passphrase as specified in CryptoInputParcel + Log.d(Constants.TAG, "Everything okay!"); - try { - PassphraseCacheService.addCachedPassphrase(getActivity(), - mSecretRing.getMasterKeyId(), mSubKeyId, passphrase, - mSecretRing.getPrimaryUserIdWithFallback()); - } catch (PgpKeyNotFoundException e) { - Log.e(Constants.TAG, "adding of a passphrase failed", e); + CryptoInputParcel cryptoInputParcel + = ((PassphraseDialogActivity) getActivity()).mCryptoInputParcel; + + if (cryptoInputParcel.mCachePassphrase) { + Log.d(Constants.TAG, "Caching entered passphrase"); + + try { + PassphraseCacheService.addCachedPassphrase(getActivity(), + mSecretRing.getMasterKeyId(), mSubKeyId, passphrase, + mSecretRing.getPrimaryUserIdWithFallback()); + } catch (PgpKeyNotFoundException e) { + Log.e(Constants.TAG, "adding of a passphrase failed", e); + } + } else { + Log.d(Constants.TAG, "Not caching entered passphrase!"); } finishCaching(passphrase); @@ -428,9 +433,12 @@ public class PassphraseDialogActivity extends FragmentActivity { return; } - CryptoInputParcel inputParcel = new CryptoInputParcel(null, passphrase); + CryptoInputParcel inputParcel = + ((PassphraseDialogActivity) getActivity()).mCryptoInputParcel; + inputParcel.mPassphrase = passphrase; if (mServiceIntent != null) { - CryptoInputParcelCacheService.addCryptoInputParcel(getActivity(), mServiceIntent, inputParcel); + CryptoInputParcelCacheService.addCryptoInputParcel(getActivity(), mServiceIntent, + inputParcel); getActivity().setResult(RESULT_OK, mServiceIntent); } else { // also return passphrase back to activity @@ -449,20 +457,16 @@ public class PassphraseDialogActivity extends FragmentActivity { // note we need no synchronization here, this variable is only accessed in the ui thread mIsCancelled = true; + + getActivity().setResult(RESULT_CANCELED); + getActivity().finish(); } @Override public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); - if (getActivity() == null) { - return; - } - hideKeyboard(); - - getActivity().setResult(RESULT_CANCELED); - getActivity().finish(); } private void hideKeyboard() { @@ -476,11 +480,9 @@ public class PassphraseDialogActivity extends FragmentActivity { inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } - /** - * Associate the "done" button on the soft keyboard with the okay button in the view - */ @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Associate the "done" button on the soft keyboard with the okay button in the view if (EditorInfo.IME_ACTION_DONE == actionId) { AlertDialog dialog = ((AlertDialog) getDialog()); Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java deleted file mode 100644 index 2e838535d..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java +++ /dev/null @@ -1,575 +0,0 @@ -/* - * 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.ui; - -import android.annotation.TargetApi; -import android.app.AlertDialog; -import android.app.PendingIntent; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.nfc.FormatException; -import android.nfc.NdefMessage; -import android.nfc.NdefRecord; -import android.nfc.NfcAdapter; -import android.nfc.Tag; -import android.nfc.tech.Ndef; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.FragmentTransaction; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; - -import org.sufficientlysecure.keychain.R; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.List; - -@TargetApi(Build.VERSION_CODES.HONEYCOMB) -public class PassphraseWizardActivity extends FragmentActivity { -//public class PassphraseWizardActivity extends FragmentActivity implements LockPatternView.OnPatternListener { - //create or authenticate - public String selectedAction; - //for lockpattern - public static char[] pattern; - private static String passphrase = ""; - //nfc string - private static byte[] output = new byte[8]; - - public static final String CREATE_METHOD = "create"; - public static final String AUTHENTICATION = "authenticate"; - - NfcAdapter adapter; - PendingIntent pendingIntent; - IntentFilter writeTagFilters[]; - boolean writeMode; - Tag myTag; - boolean writeNFC = false; - boolean readNFC = false; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getActionBar() != null) { - getActionBar().setTitle(R.string.unlock_method); - } - - selectedAction = getIntent().getAction(); - if (savedInstanceState == null) { - SelectMethods selectMethods = new SelectMethods(); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.add(R.id.fragmentContainer, selectMethods).commit(); - } - setContentView(R.layout.passphrase_wizard); - - adapter = NfcAdapter.getDefaultAdapter(this); - if (adapter != null) { - pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, PassphraseWizardActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); - IntentFilter tagDetected = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED); - tagDetected.addCategory(Intent.CATEGORY_DEFAULT); - writeTagFilters = new IntentFilter[]{tagDetected}; - } - } - - public void noPassphrase(View view) { - passphrase = ""; - Toast.makeText(this, R.string.no_passphrase_set, Toast.LENGTH_SHORT).show(); - this.finish(); - } - - public void passphrase(View view) { - Passphrase passphrase = new Passphrase(); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.fragmentContainer, passphrase).addToBackStack(null).commit(); - } - - public void startLockpattern(View view) { - if (getActionBar() != null) { - getActionBar().setTitle(R.string.draw_lockpattern); - } -// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction); -// LockPatternFragment lpf = LockPatternFragment.newInstance("asd"); - -// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); -// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit(); - } - - public void cancel(View view) { - this.finish(); - } - - public void savePassphrase(View view) { - EditText passphrase = (EditText) findViewById(R.id.passphrase); - passphrase.setError(null); - String pw = passphrase.getText().toString(); - //check and save passphrase - if (selectedAction.equals(CREATE_METHOD)) { - EditText passphraseAgain = (EditText) findViewById(R.id.passphraseAgain); - passphraseAgain.setError(null); - String pwAgain = passphraseAgain.getText().toString(); - - if (!TextUtils.isEmpty(pw)) { - if (!TextUtils.isEmpty(pwAgain)) { - if (pw.equals(pwAgain)) { - PassphraseWizardActivity.passphrase = pw; - Toast.makeText(this, getString(R.string.passphrase_saved), Toast.LENGTH_SHORT).show(); - this.finish(); - } else { - passphrase.setError(getString(R.string.passphrase_invalid)); - passphrase.requestFocus(); - } - } else { - passphraseAgain.setError(getString(R.string.missing_passphrase)); - passphraseAgain.requestFocus(); - } - } else { - passphrase.setError(getString(R.string.missing_passphrase)); - passphrase.requestFocus(); - } - } - //check for right passphrase - if (selectedAction.equals(AUTHENTICATION)) { - if (pw.equals(PassphraseWizardActivity.passphrase)) { - Toast.makeText(this, getString(R.string.unlocked), Toast.LENGTH_SHORT).show(); - this.finish(); - } else { - passphrase.setError(getString(R.string.passphrase_invalid)); - passphrase.requestFocus(); - } - } - } - - public void NFC(View view) { - if (adapter != null) { - if (getActionBar() != null) { - getActionBar().setTitle(R.string.nfc_title); - } - NFCFragment nfc = new NFCFragment(); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.fragmentContainer, nfc).addToBackStack(null).commit(); - - //if you want to create a new method or just authenticate - if (CREATE_METHOD.equals(selectedAction)) { - writeNFC = true; - } else if (AUTHENTICATION.equals(selectedAction)) { - readNFC = true; - } - - if (!adapter.isEnabled()) { - showAlertDialog(getString(R.string.enable_nfc), true); - } - } else { - showAlertDialog(getString(R.string.no_nfc_support), false); - } - } - - @Override - protected void onNewIntent(Intent intent) { - if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) { - myTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); - - if (writeNFC && CREATE_METHOD.equals(selectedAction)) { - //write new password on NFC tag - try { - if (myTag != null) { - write(myTag); - writeNFC = false; //just write once - Toast.makeText(this, R.string.nfc_write_succesful, Toast.LENGTH_SHORT).show(); - //advance to lockpattern -// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction); -// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); -// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit(); - } - } catch (IOException | FormatException e) { - e.printStackTrace(); - } - - } else if (readNFC && AUTHENTICATION.equals(selectedAction)) { - //read pw from NFC tag - try { - if (myTag != null) { - //if tag detected, read tag - String pwtag = read(myTag); - if (output != null && pwtag.equals(output.toString())) { - - //passwort matches, go to next view - Toast.makeText(this, R.string.passphrases_match + "!", Toast.LENGTH_SHORT).show(); - -// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction); -// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); -// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit(); - readNFC = false; //just once - } else { - //passwort doesnt match - TextView nfc = (TextView) findViewById(R.id.nfcText); - nfc.setText(R.string.nfc_wrong_tag); - } - } - } catch (IOException | FormatException e) { - e.printStackTrace(); - } - } - } - } - - private void write(Tag tag) throws IOException, FormatException { - //generate new random key and write them on the tag - SecureRandom sr = new SecureRandom(); - sr.nextBytes(output); - NdefRecord[] records = {createRecord(output.toString())}; - NdefMessage message = new NdefMessage(records); - Ndef ndef = Ndef.get(tag); - ndef.connect(); - ndef.writeNdefMessage(message); - ndef.close(); - } - - private String read(Tag tag) throws IOException, FormatException { - //read string from tag - String password = null; - Ndef ndef = Ndef.get(tag); - ndef.connect(); - NdefMessage ndefMessage = ndef.getCachedNdefMessage(); - - NdefRecord[] records = ndefMessage.getRecords(); - for (NdefRecord ndefRecord : records) { - if (ndefRecord.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) { - try { - password = readText(ndefRecord); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - } - } - ndef.close(); - return password; - } - - private String readText(NdefRecord record) throws UnsupportedEncodingException { - //low-level method for reading nfc - byte[] payload = record.getPayload(); - String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16"; - int languageCodeLength = payload[0] & 0063; - return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding); - } - - private NdefRecord createRecord(String text) throws UnsupportedEncodingException { - //low-level method for writing nfc - String lang = "en"; - byte[] textBytes = text.getBytes(); - byte[] langBytes = lang.getBytes("US-ASCII"); - int langLength = langBytes.length; - int textLength = textBytes.length; - byte[] payload = new byte[1 + langLength + textLength]; - - // set status byte (see NDEF spec for actual bits) - payload[0] = (byte) langLength; - // copy langbytes and textbytes into payload - System.arraycopy(langBytes, 0, payload, 1, langLength); - System.arraycopy(textBytes, 0, payload, 1 + langLength, textLength); - return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], payload); - } - - public void showAlertDialog(String message, boolean nfc) { - //This method shows an AlertDialog - AlertDialog.Builder alert = new AlertDialog.Builder(this); - alert.setTitle("Information").setMessage(message).setPositiveButton("Ok", - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - } - } - ); - if (nfc) { - - alert.setNeutralButton(R.string.nfc_settings, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialogInterface, int i) { - startActivity(new Intent(Settings.ACTION_NFC_SETTINGS)); - } - } - ); - } - alert.show(); - } - - @Override - public void onPause() { - //pause this app and free nfc intent - super.onPause(); - if (adapter != null) { - WriteModeOff(); - } - } - - @Override - public void onResume() { - //resume this app and get nfc intent - super.onResume(); - if (adapter != null) { - WriteModeOn(); - } - } - - private void WriteModeOn() { - //enable nfc for this view - writeMode = true; - adapter.enableForegroundDispatch(this, pendingIntent, writeTagFilters, null); - } - - private void WriteModeOff() { - //disable nfc for this view - writeMode = false; - adapter.disableForegroundDispatch(this); - } - - public static class SelectMethods extends Fragment { -// private OnFragmentInteractionListener mListener; - - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - */ - public SelectMethods() { - - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public void onResume() { - super.onResume(); - if (getActivity().getActionBar() != null) { - getActivity().getActionBar().setTitle(R.string.unlock_method); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.passphrase_wizard_fragment_select_methods, container, false); - } - -// @Override -// public void onAttach(Activity activity) { -// super.onAttach(activity); -// try { -// mListener = (OnFragmentInteractionListener) activity; -// } catch (ClassCastException e) { -// throw new ClassCastException(activity.toString() -// + " must implement OnFragmentInteractionListener"); -// } -// } -// -// @Override -// public void onDetach() { -// super.onDetach(); -// mListener = null; -// } - - /** - * This interface must be implemented by activities that contain this - * fragment to allow an interaction in this fragment to be communicated - * to the activity and potentially other fragments contained in that - * activity. - * <p/> - * See the Android Training lesson <a href= - * "http://developer.android.com/training/basics/fragments/communicating.html" - * >Communicating with Other Fragments</a> for more information. - */ -// public static interface OnFragmentInteractionListener { -// public void onFragmentInteraction(Uri uri); -// } - - } - - - // /** -// * A simple {@link android.support.v4.app.Fragment} subclass. -// * Activities that contain this fragment must implement the -// * {@link com.haibison.android.lockpattern.Passphrase.OnFragmentInteractionListener} interface -// * to handle interaction events. -// */ - public static class Passphrase extends Fragment { - -// private OnFragmentInteractionListener mListener; - - public Passphrase() { - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflate the layout for this fragment - View view = inflater.inflate(R.layout.passphrase_wizard_fragment_passphrase, container, false); - EditText passphraseAgain = (EditText) view.findViewById(R.id.passphraseAgain); - TextView passphraseText = (TextView) view.findViewById(R.id.passphraseText); - TextView passphraseTextAgain = (TextView) view.findViewById(R.id.passphraseTextAgain); - String selectedAction = getActivity().getIntent().getAction(); - if (selectedAction.equals(AUTHENTICATION)) { - passphraseAgain.setVisibility(View.GONE); - passphraseTextAgain.setVisibility(View.GONE); - passphraseText.setText(R.string.enter_passphrase); -// getActivity().getActionBar().setTitle(R.string.enter_passphrase); - } else if (selectedAction.equals(CREATE_METHOD)) { - passphraseAgain.setVisibility(View.VISIBLE); - passphraseTextAgain.setVisibility(View.VISIBLE); - passphraseText.setText(R.string.passphrase); -// getActivity().getActionBar().setTitle(R.string.set_passphrase); - } - return view; - } - -// @Override -// public void onAttach(Activity activity) { -// super.onAttach(activity); -// try { -// mListener = (OnFragmentInteractionListener) activity; -// } catch (ClassCastException e) { -// throw new ClassCastException(activity.toString() -// + " must implement OnFragmentInteractionListener"); -// } -// } -// -// @Override -// public void onDetach() { -// super.onDetach(); -// mListener = null; -// } - -// /** -// * This interface must be implemented by activities that contain this -// * fragment to allow an interaction in this fragment to be communicated -// * to the activity and potentially other fragments contained in that -// * activity. -// * <p/> -// * See the Android Training lesson <a href= -// * "http://developer.android.com/training/basics/fragments/communicating.html" -// * >Communicating with Other Fragments</a> for more information. -// */ -// public interface OnFragmentInteractionListener { -// public void onFragmentInteraction(Uri uri); -// } - } - - - /** - * A simple {@link android.support.v4.app.Fragment} subclass. - * Activities that contain this fragment must implement the - * interface - * to handle interaction events. - * Use the method to - * create an instance of this fragment. - */ - public static class NFCFragment extends Fragment { - // TODO: Rename parameter arguments, choose names that match - // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER - private static final String ARG_PARAM1 = "param1"; - private static final String ARG_PARAM2 = "param2"; - - // TODO: Rename and change types of parameters - private String mParam1; - private String mParam2; - -// private OnFragmentInteractionListener mListener; - - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment SelectMethods. - */ - // TODO: Rename and change types and number of parameters - public static NFCFragment newInstance(String param1, String param2) { - NFCFragment fragment = new NFCFragment(); - Bundle args = new Bundle(); - args.putString(ARG_PARAM1, param1); - args.putString(ARG_PARAM2, param2); - fragment.setArguments(args); - return fragment; - } - - public NFCFragment() { - // Required empty public constructor - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getArguments() != null) { - mParam1 = getArguments().getString(ARG_PARAM1); - mParam2 = getArguments().getString(ARG_PARAM2); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.passphrase_wizard_fragment_nfc, container, false); - } - -// // TODO: Rename method, update argument and hook method into UI event -// public void onButtonPressed(Uri uri) { -// if (mListener != null) { -// mListener.onFragmentInteraction(uri); -// } -// } - -// @Override -// public void onAttach(Activity activity) { -// super.onAttach(activity); -// try { -// mListener = (OnFragmentInteractionListener) activity; -// } catch (ClassCastException e) { -// throw new ClassCastException(activity.toString() -// + " must implement OnFragmentInteractionListener"); -// } -// } - - -// @Override -// public void onDetach() { -// super.onDetach(); -// mListener = null; -// } - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeViewActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeViewActivity.java index d4858ee5d..e54852f1b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeViewActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeViewActivity.java @@ -85,11 +85,12 @@ public class QrCodeViewActivity extends BaseActivity { ActivityCompat.finishAfterTransition(QrCodeViewActivity.this); } - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob); - String qrCodeContent = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; - + Uri uri = new Uri.Builder() + .scheme(Constants.FINGERPRINT_SCHEME) + .opaquePart(KeyFormattingUtils.convertFingerprintToHex(blob)) + .build(); // create a minimal size qr code, we can keep this in ram no problem - final Bitmap qrCode = QrCodeUtils.getQRCodeBitmap(qrCodeContent, 0); + final Bitmap qrCode = QrCodeUtils.getQRCodeBitmap(uri, 0); mQrCode.getViewTreeObserver().addOnGlobalLayoutListener( new OnGlobalLayoutListener() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/RetryUploadDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/RetryUploadDialogActivity.java new file mode 100644 index 000000000..2a00e8b70 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/RetryUploadDialogActivity.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.ui; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.view.ContextThemeWrapper; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; + +public class RetryUploadDialogActivity extends FragmentActivity { + + public static final String EXTRA_CRYPTO_INPUT = "extra_crypto_input"; + + public static final String RESULT_CRYPTO_INPUT = "result_crypto_input"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + UploadRetryDialogFragment.newInstance().show(getSupportFragmentManager(), + "uploadRetryDialog"); + } + + public static class UploadRetryDialogFragment extends DialogFragment { + public static UploadRetryDialogFragment newInstance() { + return new UploadRetryDialogFragment(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(getActivity()); + + CustomAlertDialogBuilder dialogBuilder = new CustomAlertDialogBuilder(theme); + dialogBuilder.setTitle(R.string.retry_up_dialog_title); + dialogBuilder.setMessage(R.string.retry_up_dialog_message); + + dialogBuilder.setNegativeButton(R.string.retry_up_dialog_btn_cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + getActivity().setResult(RESULT_CANCELED); + getActivity().finish(); + } + }); + + dialogBuilder.setPositiveButton(R.string.retry_up_dialog_btn_reupload, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(); + intent.putExtra(RESULT_CRYPTO_INPUT, getActivity() + .getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT)); + getActivity().setResult(RESULT_OK, intent); + getActivity().finish(); + } + }); + + return dialogBuilder.show(); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java index aa3c36d11..534dbfd05 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SafeSlingerActivity.java @@ -18,15 +18,11 @@ package org.sufficientlysecure.keychain.ui; import android.annotation.TargetApi; -import android.app.ProgressDialog; import android.content.Intent; import android.graphics.PorterDuff; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; -import android.support.v4.app.FragmentActivity; import android.view.View; import android.widget.ImageView; import android.widget.NumberPicker; @@ -38,10 +34,10 @@ import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseActivity; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableFileCache; @@ -53,7 +49,8 @@ import edu.cmu.cylab.starslinger.exchange.ExchangeActivity; import edu.cmu.cylab.starslinger.exchange.ExchangeConfig; @TargetApi(Build.VERSION_CODES.HONEYCOMB) -public class SafeSlingerActivity extends BaseActivity { +public class SafeSlingerActivity extends BaseActivity + implements CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> { private static final int REQUEST_CODE_SAFE_SLINGER = 211; @@ -62,6 +59,12 @@ public class SafeSlingerActivity extends BaseActivity { private long mMasterKeyId; private int mSelectedNumber = 2; + // for CryptoOperationHelper + private ArrayList<ParcelableKeyRing> mKeyList; + private String mKeyserver; + private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mOperationHelper; + + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -79,7 +82,7 @@ public class SafeSlingerActivity extends BaseActivity { }); ImageView buttonIcon = (ImageView) findViewById(R.id.safe_slinger_button_image); - buttonIcon.setColorFilter(getResources().getColor(R.color.tertiary_text_light), + buttonIcon.setColorFilter(FormattingUtils.getColorFromAttr(this, R.attr.colorTertiaryText), PorterDuff.Mode.SRC_IN); View button = findViewById(R.id.safe_slinger_button); @@ -117,69 +120,17 @@ public class SafeSlingerActivity extends BaseActivity { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mOperationHelper != null) { + mOperationHelper.handleActivityResult(requestCode, resultCode, data); + } + if (requestCode == REQUEST_CODE_SAFE_SLINGER) { if (resultCode == ExchangeActivity.RESULT_EXCHANGE_CANCELED) { return; } - final FragmentActivity activity = SafeSlingerActivity.this; - - // Message is received after importing is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - activity, - getString(R.string.progress_importing), - ProgressDialog.STYLE_HORIZONTAL, - true, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - if (returnData == null) { - return; - } - final ImportKeyResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null) { - Log.e(Constants.TAG, "result == null"); - return; - } - - if (!result.success()) { -// result.createNotify(activity).show(); - // only return if no success... - Intent data = new Intent(); - data.putExtras(returnData); - setResult(RESULT_OK, data); - finish(); - return; - } - -// if (mExchangeMasterKeyId == null) { -// return; -// } - - Intent certifyIntent = new Intent(activity, CertifyKeyActivity.class); - certifyIntent.putExtra(CertifyKeyActivity.EXTRA_RESULT, result); - certifyIntent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, result.getImportedMasterKeyIds()); - certifyIntent.putExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, mMasterKeyId); - startActivityForResult(certifyIntent, 0); - -// mExchangeMasterKeyId = null; - } - } - }; - Log.d(Constants.TAG, "importKeys started"); - // Send all information needed to service to import key in other thread - Intent intent = new Intent(activity, KeychainIntentService.class); - - intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); - // instead of giving the entries by Intent extra, cache them into a // file to prevent Java Binder problems on heavy imports // read FileImportCache for more info. @@ -190,25 +141,18 @@ public class SafeSlingerActivity extends BaseActivity { // We parcel this iteratively into a file - anything we can // display here, we should be able to import. ParcelableFileCache<ParcelableKeyRing> cache = - new ParcelableFileCache<>(activity, "key_import.pcl"); + new ParcelableFileCache<>(this, "key_import.pcl"); cache.writeCache(it.size(), it.iterator()); - // fill values for this action - Bundle bundle = new Bundle(); - intent.putExtra(KeychainIntentService.EXTRA_DATA, bundle); - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - saveHandler.showProgressDialog(activity); + mOperationHelper = + new CryptoOperationHelper(1, this, this, R.string.progress_importing); - // start service with intent - activity.startService(intent); + mKeyList = null; + mKeyserver = null; + mOperationHelper.cryptoOperation(); } catch (IOException e) { Log.e(Constants.TAG, "Problem writing cache file", e); - Notify.create(activity, "Problem writing cache file!", Notify.Style.ERROR).show(); + Notify.create(this, "Problem writing cache file!", Notify.Style.ERROR).show(); } } else { // give everything else down to KeyListActivity! @@ -235,4 +179,39 @@ public class SafeSlingerActivity extends BaseActivity { return list; } + // CryptoOperationHelper.Callback functions + + @Override + public ImportKeyringParcel createOperationInput() { + return new ImportKeyringParcel(mKeyList, mKeyserver); + } + + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + Intent certifyIntent = new Intent(this, CertifyKeyActivity.class); + certifyIntent.putExtra(CertifyKeyActivity.EXTRA_RESULT, result); + certifyIntent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, result.getImportedMasterKeyIds()); + certifyIntent.putExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, mMasterKeyId); + startActivityForResult(certifyIntent, 0); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + Bundle returnData = new Bundle(); + returnData.putParcelable(OperationResult.EXTRA_RESULT, result); + Intent data = new Intent(); + data.putExtras(returnData); + setResult(RESULT_OK, data); + finish(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java index 442bdf8f7..4077f1c84 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2014-2015 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> * * This program is free software: you can redistribute it and/or modify @@ -17,42 +18,59 @@ package org.sufficientlysecure.keychain.ui; -import android.annotation.TargetApi; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Activity; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.ContactsContract; import android.support.v7.widget.Toolbar; +import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; -import org.spongycastle.bcpg.CompressionAlgorithmTags; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.AppCompatPreferenceActivity; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference; +import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; import java.util.List; -public class SettingsActivity extends PreferenceActivity { +public class SettingsActivity extends AppCompatPreferenceActivity { public static final String ACTION_PREFS_CLOUD = "org.sufficientlysecure.keychain.ui.PREFS_CLOUD"; public static final String ACTION_PREFS_ADV = "org.sufficientlysecure.keychain.ui.PREFS_ADV"; + public static final String ACTION_PREFS_PROXY = "org.sufficientlysecure.keychain.ui.PREFS_PROXY"; + public static final String ACTION_PREFS_GUI = "org.sufficientlysecure.keychain.ui.PREFS_GUI"; public static final int REQUEST_CODE_KEYSERVER_PREF = 0x00007005; private PreferenceScreen mKeyServerPreference = null; private static Preferences sPreferences; + private ThemeChanger mThemeChanger; @Override protected void onCreate(Bundle savedInstanceState) { sPreferences = Preferences.getPreferences(this); + mThemeChanger = new ThemeChanger(this); + mThemeChanger.setThemes(R.style.Theme_Keychain_Light, R.style.Theme_Keychain_Dark); + mThemeChanger.changeTheme(); super.onCreate(savedInstanceState); setupToolbar(); @@ -76,14 +94,14 @@ public class SettingsActivity extends PreferenceActivity { } }); initializeSearchKeyserver( - (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) + (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) ); initializeSearchKeybase( - (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) + (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) ); } else if (action != null && action.equals(ACTION_PREFS_ADV)) { - addPreferencesFromResource(R.xml.adv_preferences); + addPreferencesFromResource(R.xml.passphrase_preferences); initializePassphraseCacheSubs( (CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS)); @@ -91,28 +109,29 @@ public class SettingsActivity extends PreferenceActivity { initializePassphraseCacheTtl( (IntegerListPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_TTL)); - int[] valueIds = new int[]{ - CompressionAlgorithmTags.UNCOMPRESSED, - CompressionAlgorithmTags.ZIP, - CompressionAlgorithmTags.ZLIB, - CompressionAlgorithmTags.BZIP2, - }; - String[] entries = new String[]{ - getString(R.string.choice_none) + " (" + getString(R.string.compression_fast) + ")", - "ZIP (" + getString(R.string.compression_fast) + ")", - "ZLIB (" + getString(R.string.compression_fast) + ")", - "BZIP2 (" + getString(R.string.compression_very_slow) + ")",}; - String[] values = new String[valueIds.length]; - for (int i = 0; i < values.length; ++i) { - values[i] = "" + valueIds[i]; - } - initializeUseDefaultYubiKeyPin( (CheckBoxPreference) findPreference(Constants.Pref.USE_DEFAULT_YUBIKEY_PIN)); initializeUseNumKeypadForYubiKeyPin( (CheckBoxPreference) findPreference(Constants.Pref.USE_NUMKEYPAD_FOR_YUBIKEY_PIN)); + } else if (action != null && action.equals(ACTION_PREFS_GUI)) { + addPreferencesFromResource(R.xml.gui_preferences); + + initializeTheme((ListPreference) findPreference(Constants.Pref.THEME)); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mThemeChanger.changeTheme()) { + Intent intent = getIntent(); + finish(); + overridePendingTransition(0, 0); + startActivity(intent); + overridePendingTransition(0, 0); } } @@ -122,13 +141,14 @@ public class SettingsActivity extends PreferenceActivity { private void setupToolbar() { ViewGroup root = (ViewGroup) findViewById(android.R.id.content); LinearLayout content = (LinearLayout) root.getChildAt(0); - LinearLayout toolbarContainer = (LinearLayout) View.inflate(this, R.layout.preference_toolbar_activity, null); + LinearLayout toolbarContainer = (LinearLayout) View.inflate(this, R.layout.preference_toolbar, null); root.removeAllViews(); toolbarContainer.addView(content); root.addView(toolbarContainer); Toolbar toolbar = (Toolbar) toolbarContainer.findViewById(R.id.toolbar); + toolbar.setTitle(R.string.title_preferences); toolbar.setNavigationIcon(getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp)); toolbar.setNavigationOnClickListener(new View.OnClickListener() { @@ -141,34 +161,13 @@ public class SettingsActivity extends PreferenceActivity { } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_CODE_KEYSERVER_PREF: { - if (resultCode == RESULT_CANCELED || data == null) { - return; - } - String servers[] = data - .getStringArrayExtra(SettingsKeyServerActivity.EXTRA_KEY_SERVERS); - sPreferences.setKeyServers(servers); - mKeyServerPreference.setSummary(keyserverSummary(this)); - break; - } - - default: { - super.onActivityResult(requestCode, resultCode, data); - break; - } - } - } - - @Override public void onBuildHeaders(List<Header> target) { super.onBuildHeaders(target); loadHeadersFromResource(R.xml.preference_headers, target); } /** - * This fragment shows the Cloud Search preferences in android 3.0+ + * This fragment shows the Cloud Search preferences */ public static class CloudSearchPrefsFragment extends PreferenceFragment { @@ -196,10 +195,10 @@ public class SettingsActivity extends PreferenceActivity { } }); initializeSearchKeyserver( - (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) + (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) ); initializeSearchKeybase( - (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) + (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) ); } @@ -207,12 +206,7 @@ public class SettingsActivity extends PreferenceActivity { public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CODE_KEYSERVER_PREF: { - if (resultCode == RESULT_CANCELED || data == null) { - return; - } - String servers[] = data - .getStringArrayExtra(SettingsKeyServerActivity.EXTRA_KEY_SERVERS); - sPreferences.setKeyServers(servers); + // update preference, in case it changed mKeyServerPreference.setSummary(keyserverSummary(getActivity())); break; } @@ -226,16 +220,16 @@ public class SettingsActivity extends PreferenceActivity { } /** - * This fragment shows the advanced preferences in android 3.0+ + * This fragment shows the PIN/password preferences */ - public static class AdvancedPrefsFragment extends PreferenceFragment { + public static class PassphrasePrefsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Load the preferences from an XML resource - addPreferencesFromResource(R.xml.adv_preferences); + addPreferencesFromResource(R.xml.passphrase_preferences); initializePassphraseCacheSubs( (CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS)); @@ -243,25 +237,6 @@ public class SettingsActivity extends PreferenceActivity { initializePassphraseCacheTtl( (IntegerListPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_TTL)); - int[] valueIds = new int[]{ - CompressionAlgorithmTags.UNCOMPRESSED, - CompressionAlgorithmTags.ZIP, - CompressionAlgorithmTags.ZLIB, - CompressionAlgorithmTags.BZIP2, - }; - - String[] entries = new String[]{ - getString(R.string.choice_none) + " (" + getString(R.string.compression_fast) + ")", - "ZIP (" + getString(R.string.compression_fast) + ")", - "ZLIB (" + getString(R.string.compression_fast) + ")", - "BZIP2 (" + getString(R.string.compression_very_slow) + ")", - }; - - String[] values = new String[valueIds.length]; - for (int i = 0; i < values.length; ++i) { - values[i] = "" + valueIds[i]; - } - initializeUseDefaultYubiKeyPin( (CheckBoxPreference) findPreference(Constants.Pref.USE_DEFAULT_YUBIKEY_PIN)); @@ -270,10 +245,346 @@ public class SettingsActivity extends PreferenceActivity { } } - @TargetApi(Build.VERSION_CODES.KITKAT) + public static class ProxyPrefsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + new Initializer(this).initialize(); + + } + + public static class Initializer { + private SwitchPreference mUseTor; + private SwitchPreference mUseNormalProxy; + private EditTextPreference mProxyHost; + private EditTextPreference mProxyPort; + private ListPreference mProxyType; + private PreferenceActivity mActivity; + private PreferenceFragment mFragment; + + public Initializer(PreferenceFragment fragment) { + mFragment = fragment; + } + + public Initializer(PreferenceActivity activity) { + mActivity = activity; + } + + public Preference automaticallyFindPreference(String key) { + if (mFragment != null) { + return mFragment.findPreference(key); + } else { + return mActivity.findPreference(key); + } + } + + public void initialize() { + // makes android's preference framework write to our file instead of default + // This allows us to use the "persistent" attribute to simplify code + if (mFragment != null) { + Preferences.setPreferenceManagerFileAndMode(mFragment.getPreferenceManager()); + // Load the preferences from an XML resource + mFragment.addPreferencesFromResource(R.xml.proxy_prefs); + } else { + Preferences.setPreferenceManagerFileAndMode(mActivity.getPreferenceManager()); + // Load the preferences from an XML resource + mActivity.addPreferencesFromResource(R.xml.proxy_prefs); + } + + mUseTor = (SwitchPreference) automaticallyFindPreference(Constants.Pref.USE_TOR_PROXY); + mUseNormalProxy = (SwitchPreference) automaticallyFindPreference(Constants.Pref.USE_NORMAL_PROXY); + mProxyHost = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_HOST); + mProxyPort = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_PORT); + mProxyType = (ListPreference) automaticallyFindPreference(Constants.Pref.PROXY_TYPE); + initializeUseTorPref(); + initializeUseNormalProxyPref(); + initializeEditTextPreferences(); + initializeProxyTypePreference(); + + if (mUseTor.isChecked()) { + disableNormalProxyPrefs(); + } + else if (mUseNormalProxy.isChecked()) { + disableUseTorPrefs(); + } else { + disableNormalProxySettings(); + } + } + + private void initializeUseTorPref() { + mUseTor.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Activity activity = mFragment != null ? mFragment.getActivity() : mActivity; + if ((Boolean) newValue) { + boolean installed = OrbotHelper.isOrbotInstalled(activity); + if (!installed) { + Log.d(Constants.TAG, "Prompting to install Tor"); + OrbotHelper.getPreferenceInstallDialogFragment().show(activity.getFragmentManager(), + "installDialog"); + // don't let the user check the box until he's installed orbot + return false; + } else { + disableNormalProxyPrefs(); + // let the enable tor box be checked + return true; + } + } else { + // we're unchecking Tor, so enable other proxy + enableNormalProxyCheckbox(); + return true; + } + } + }); + } + + private void initializeUseNormalProxyPref() { + mUseNormalProxy.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if ((Boolean) newValue) { + disableUseTorPrefs(); + enableNormalProxySettings(); + } else { + enableUseTorPrefs(); + disableNormalProxySettings(); + } + return true; + } + }); + } + + private void initializeEditTextPreferences() { + mProxyHost.setSummary(mProxyHost.getText()); + mProxyPort.setSummary(mProxyPort.getText()); + + mProxyHost.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Activity activity = mFragment != null ? mFragment.getActivity() : mActivity; + if (TextUtils.isEmpty((String) newValue)) { + Notify.create( + activity, + R.string.pref_proxy_host_err_invalid, + Notify.Style.ERROR + ).show(); + return false; + } else { + mProxyHost.setSummary((CharSequence) newValue); + return true; + } + } + }); + + mProxyPort.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Activity activity = mFragment != null ? mFragment.getActivity() : mActivity; + try { + int port = Integer.parseInt((String) newValue); + if (port < 0 || port > 65535) { + Notify.create( + activity, + R.string.pref_proxy_port_err_invalid, + Notify.Style.ERROR + ).show(); + return false; + } + // no issues, save port + mProxyPort.setSummary("" + port); + return true; + } catch (NumberFormatException e) { + Notify.create( + activity, + R.string.pref_proxy_port_err_invalid, + Notify.Style.ERROR + ).show(); + return false; + } + } + }); + } + + private void initializeProxyTypePreference() { + mProxyType.setSummary(mProxyType.getEntry()); + + mProxyType.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + CharSequence entry = mProxyType.getEntries()[mProxyType.findIndexOfValue((String) newValue)]; + mProxyType.setSummary(entry); + return true; + } + }); + } + + private void disableNormalProxyPrefs() { + mUseNormalProxy.setChecked(false); + mUseNormalProxy.setEnabled(false); + disableNormalProxySettings(); + } + + private void enableNormalProxyCheckbox() { + mUseNormalProxy.setEnabled(true); + } + + private void enableNormalProxySettings() { + mProxyHost.setEnabled(true); + mProxyPort.setEnabled(true); + mProxyType.setEnabled(true); + } + + private void disableNormalProxySettings() { + mProxyHost.setEnabled(false); + mProxyPort.setEnabled(false); + mProxyType.setEnabled(false); + } + + private void disableUseTorPrefs() { + mUseTor.setChecked(false); + mUseTor.setEnabled(false); + } + + private void enableUseTorPrefs() { + mUseTor.setEnabled(true); + } + } + } + + /** + * This fragment shows gui preferences. + */ + public static class GuiPrefsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.gui_preferences); + + initializeTheme((ListPreference) findPreference(Constants.Pref.THEME)); + } + } + + /** + * This fragment shows the keyserver/contacts sync preferences + */ + public static class SyncPrefsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.sync_preferences); + } + + @Override + public void onResume() { + super.onResume(); + // this needs to be done in onResume since the user can change sync values from Android + // settings and we need to reflect that change when the user navigates back + AccountManager manager = AccountManager.get(getActivity()); + final Account account = manager.getAccountsByType(Constants.ACCOUNT_TYPE)[0]; + // for keyserver sync + initializeSyncCheckBox( + (SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER), + account, + Constants.PROVIDER_AUTHORITY + ); + // for contacts sync + initializeSyncCheckBox( + (SwitchPreference) findPreference(Constants.Pref.SYNC_CONTACTS), + account, + ContactsContract.AUTHORITY + ); + } + + private void initializeSyncCheckBox(final SwitchPreference syncCheckBox, + final Account account, + final String authority) { + boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority); + syncCheckBox.setChecked(syncEnabled); + setSummary(syncCheckBox, authority, syncEnabled); + + syncCheckBox.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean syncEnabled = (Boolean) newValue; + if (syncEnabled) { + ContentResolver.setSyncAutomatically(account, authority, true); + } else { + // disable syncs + ContentResolver.setSyncAutomatically(account, authority, false); + // cancel any ongoing/pending syncs + ContentResolver.cancelSync(account, authority); + } + setSummary(syncCheckBox, authority, syncEnabled); + return true; + } + }); + } + + private void setSummary(SwitchPreference syncCheckBox, String authority, + boolean checked) { + switch (authority) { + case Constants.PROVIDER_AUTHORITY: { + if (checked) { + syncCheckBox.setSummary(R.string.label_sync_settings_keyserver_summary_on); + } else { + syncCheckBox.setSummary(R.string.label_sync_settings_keyserver_summary_off); + } + break; + } + case ContactsContract.AUTHORITY: { + if (checked) { + syncCheckBox.setSummary(R.string.label_sync_settings_contacts_summary_on); + } else { + syncCheckBox.setSummary(R.string.label_sync_settings_contacts_summary_off); + } + break; + } + } + } + } + + /** + * This fragment shows experimental features + */ + public static class ExperimentalPrefsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.experimental_preferences); + + initializeExperimentalEnableWordConfirm( + (SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM)); + + initializeExperimentalEnableLinkedIdentities( + (SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES)); + + initializeExperimentalEnableKeybase( + (SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_KEYBASE)); + + initializeTheme((ListPreference) findPreference(Constants.Pref.THEME)); + + } + } + protected boolean isValidFragment(String fragmentName) { - return AdvancedPrefsFragment.class.getName().equals(fragmentName) + return PassphrasePrefsFragment.class.getName().equals(fragmentName) || CloudSearchPrefsFragment.class.getName().equals(fragmentName) + || ProxyPrefsFragment.class.getName().equals(fragmentName) + || GuiPrefsFragment.class.getName().equals(fragmentName) + || SyncPrefsFragment.class.getName().equals(fragmentName) + || ExperimentalPrefsFragment.class.getName().equals(fragmentName) || super.isValidFragment(fragmentName); } @@ -302,7 +613,23 @@ public class SettingsActivity extends PreferenceActivity { }); } - private static void initializeSearchKeyserver(final CheckBoxPreference mSearchKeyserver) { + private static void initializeTheme(final ListPreference mTheme) { + mTheme.setValue(sPreferences.getTheme()); + mTheme.setSummary(mTheme.getEntry()); + mTheme.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mTheme.setValue((String) newValue); + mTheme.setSummary(mTheme.getEntry()); + sPreferences.setTheme((String) newValue); + + ((SettingsActivity) mTheme.getContext()).recreate(); + + return false; + } + }); + } + + private static void initializeSearchKeyserver(final SwitchPreference mSearchKeyserver) { Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs(); mSearchKeyserver.setChecked(prefs.searchKeyserver); mSearchKeyserver.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @@ -315,7 +642,7 @@ public class SettingsActivity extends PreferenceActivity { }); } - private static void initializeSearchKeybase(final CheckBoxPreference mSearchKeybase) { + private static void initializeSearchKeybase(final SwitchPreference mSearchKeybase) { Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs(); mSearchKeybase.setChecked(prefs.searchKeybase); mSearchKeybase.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @@ -332,7 +659,8 @@ public class SettingsActivity extends PreferenceActivity { String[] servers = sPreferences.getKeyServers(); String serverSummary = context.getResources().getQuantityString( R.plurals.n_keyservers, servers.length, servers.length); - return serverSummary + "; " + context.getString(R.string.label_preferred) + ": " + sPreferences.getPreferredKeyserver(); + return serverSummary + "; " + context.getString(R.string.label_preferred) + ": " + sPreferences + .getPreferredKeyserver(); } private static void initializeUseDefaultYubiKeyPin(final CheckBoxPreference mUseDefaultYubiKeyPin) { @@ -356,4 +684,37 @@ public class SettingsActivity extends PreferenceActivity { } }); } + + private static void initializeExperimentalEnableWordConfirm(final SwitchPreference mExperimentalEnableWordConfirm) { + mExperimentalEnableWordConfirm.setChecked(sPreferences.getExperimentalEnableWordConfirm()); + mExperimentalEnableWordConfirm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mExperimentalEnableWordConfirm.setChecked((Boolean) newValue); + sPreferences.setExperimentalEnableWordConfirm((Boolean) newValue); + return false; + } + }); + } + + private static void initializeExperimentalEnableLinkedIdentities(final SwitchPreference mExperimentalEnableLinkedIdentities) { + mExperimentalEnableLinkedIdentities.setChecked(sPreferences.getExperimentalEnableLinkedIdentities()); + mExperimentalEnableLinkedIdentities.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mExperimentalEnableLinkedIdentities.setChecked((Boolean) newValue); + sPreferences.setExperimentalEnableLinkedIdentities((Boolean) newValue); + return false; + } + }); + } + + private static void initializeExperimentalEnableKeybase(final SwitchPreference mExperimentalKeybase) { + mExperimentalKeybase.setChecked(sPreferences.getExperimentalEnableKeybase()); + mExperimentalKeybase.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mExperimentalKeybase.setChecked((Boolean) newValue); + sPreferences.setExperimentalEnableKeybase((Boolean) newValue); + return false; + } + }); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java index 8f025c769..f61ada84f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java @@ -17,89 +17,23 @@ package org.sufficientlysecure.keychain.ui; -import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.os.Messenger; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.TextView; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.base.BaseActivity; -import org.sufficientlysecure.keychain.ui.dialog.AddKeyserverDialogFragment; -import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.ui.widget.Editor; -import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener; -import org.sufficientlysecure.keychain.ui.widget.KeyServerEditor; -import java.util.Vector; - -public class SettingsKeyServerActivity extends BaseActivity implements OnClickListener, - EditorListener { +public class SettingsKeyServerActivity extends BaseActivity { public static final String EXTRA_KEY_SERVERS = "key_servers"; - private LayoutInflater mInflater; - private ViewGroup mEditors; - private View mAdd; - private View mRotate; - private TextView mTitle; - private TextView mSummary; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Inflate a "Done"/"Cancel" custom action bar view - setFullScreenDialogDoneClose(R.string.btn_save, - new View.OnClickListener() { - @Override - public void onClick(View v) { - okClicked(); - } - }, - new View.OnClickListener() { - @Override - public void onClick(View v) { - cancelClicked(); - } - }); - - mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - mTitle = (TextView) findViewById(R.id.title); - mSummary = (TextView) findViewById(R.id.summary); - mSummary.setText(getText(R.string.label_first_keyserver_is_used)); - - mTitle.setText(R.string.label_keyservers); - - mEditors = (ViewGroup) findViewById(R.id.editors); - mAdd = findViewById(R.id.add); - mAdd.setOnClickListener(this); - - mRotate = findViewById(R.id.rotate); - mRotate.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - Vector<String> servers = serverList(); - String first = servers.get(0); - if (first != null) { - servers.remove(0); - servers.add(first); - String[] dummy = {}; - makeServerList(servers.toArray(dummy)); - } - } - }); - Intent intent = getIntent(); String servers[] = intent.getStringArrayExtra(EXTRA_KEY_SERVERS); - makeServerList(servers); + loadFragment(savedInstanceState, servers); } @Override @@ -107,118 +41,22 @@ public class SettingsKeyServerActivity extends BaseActivity implements OnClickLi setContentView(R.layout.key_server_preference); } - private void makeServerList(String[] servers) { - if (servers != null) { - mEditors.removeAllViews(); - for (String serv : servers) { - KeyServerEditor view = (KeyServerEditor) mInflater.inflate( - R.layout.key_server_editor, mEditors, false); - view.setEditorListener(this); - view.setValue(serv); - mEditors.addView(view); - } + private void loadFragment(Bundle savedInstanceState, String[] keyservers) { + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; } - } - - public void onDeleted(Editor editor, boolean wasNewItem) { - // nothing to do - } - @Override - public void onEdited() { + SettingsKeyserverFragment fragment = SettingsKeyserverFragment.newInstance(keyservers); - } - - // button to add keyserver clicked - public void onClick(View v) { - Handler returnHandler = new Handler() { - @Override - public void handleMessage(Message message) { - Bundle data = message.getData(); - switch (message.what) { - case AddKeyserverDialogFragment.MESSAGE_OKAY: { - boolean verified = data.getBoolean(AddKeyserverDialogFragment.MESSAGE_VERIFIED); - if (verified) { - Notify.create(SettingsKeyServerActivity.this, - R.string.add_keyserver_verified, Notify.Style.OK).show(); - } else { - Notify.create(SettingsKeyServerActivity.this, - R.string.add_keyserver_without_verification, - Notify.Style.WARN).show(); - } - String keyserver = data.getString(AddKeyserverDialogFragment.MESSAGE_KEYSERVER); - addKeyserver(keyserver); - break; - } - case AddKeyserverDialogFragment.MESSAGE_VERIFICATION_FAILED: { - AddKeyserverDialogFragment.FailureReason failureReason = - (AddKeyserverDialogFragment.FailureReason) data.getSerializable( - AddKeyserverDialogFragment.MESSAGE_FAILURE_REASON); - switch (failureReason) { - case CONNECTION_FAILED: { - Notify.create(SettingsKeyServerActivity.this, - R.string.add_keyserver_connection_failed, - Notify.Style.ERROR).show(); - break; - } - case INVALID_URL: { - Notify.create(SettingsKeyServerActivity.this, - R.string.add_keyserver_invalid_url, - Notify.Style.ERROR).show(); - break; - } - } - break; - } - } - } - }; - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(returnHandler); - AddKeyserverDialogFragment dialogFragment = AddKeyserverDialogFragment - .newInstance(messenger, R.string.add_keyserver_dialog_title); - dialogFragment.show(getSupportFragmentManager(), "addKeyserverDialog"); - } - - public void addKeyserver(String keyserverUrl) { - KeyServerEditor view = (KeyServerEditor) mInflater.inflate(R.layout.key_server_editor, - mEditors, false); - view.setEditorListener(this); - view.setValue(keyserverUrl); - mEditors.addView(view); - } - - private void cancelClicked() { - setResult(RESULT_CANCELED, null); - finish(); - } - - private Vector<String> serverList() { - Vector<String> servers = new Vector<>(); - for (int i = 0; i < mEditors.getChildCount(); ++i) { - KeyServerEditor editor = (KeyServerEditor) mEditors.getChildAt(i); - String tmp = editor.getValue(); - if (tmp.length() > 0) { - servers.add(tmp); - } - } - return servers; - } - - private void okClicked() { - Intent data = new Intent(); - Vector<String> servers = new Vector<>(); - for (int i = 0; i < mEditors.getChildCount(); ++i) { - KeyServerEditor editor = (KeyServerEditor) mEditors.getChildAt(i); - String tmp = editor.getValue(); - if (tmp.length() > 0) { - servers.add(tmp); - } - } - String[] dummy = new String[0]; - data.putExtra(EXTRA_KEY_SERVERS, servers.toArray(dummy)); - setResult(RESULT_OK, data); - finish(); + // Add the fragment to the 'fragment_container' FrameLayout + // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + getSupportFragmentManager().beginTransaction() + .replace(R.id.keyserver_settings_fragment_container, fragment) + .commitAllowingStateLoss(); + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java new file mode 100644 index 000000000..d8edbe4f8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2012-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.ui; + +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.Fragment; +import android.support.v4.view.MotionEventCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.dialog.AddEditKeyserverDialogFragment; +import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperAdapter; +import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperViewHolder; +import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperDragCallback; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerItemClickListener; +import org.sufficientlysecure.keychain.util.Preferences; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SettingsKeyserverFragment extends Fragment implements RecyclerItemClickListener.OnItemClickListener { + + private static final String ARG_KEYSERVER_ARRAY = "arg_keyserver_array"; + private ItemTouchHelper mItemTouchHelper; + + private ArrayList<String> mKeyservers; + private KeyserverListAdapter mAdapter; + + public static SettingsKeyserverFragment newInstance(String[] keyservers) { + Bundle args = new Bundle(); + args.putStringArray(ARG_KEYSERVER_ARRAY, keyservers); + + SettingsKeyserverFragment fragment = new SettingsKeyserverFragment(); + fragment.setArguments(args); + + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { + + return inflater.inflate(R.layout.settings_keyserver_fragment, null); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + String keyservers[] = getArguments().getStringArray(ARG_KEYSERVER_ARRAY); + mKeyservers = new ArrayList<>(Arrays.asList(keyservers)); + + mAdapter = new KeyserverListAdapter(mKeyservers); + + RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.keyserver_recycler_view); + // recyclerView.setHasFixedSize(true); // the size of the first item changes + recyclerView.setAdapter(mAdapter); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + + + ItemTouchHelper.Callback callback = new ItemTouchHelperDragCallback(mAdapter); + mItemTouchHelper = new ItemTouchHelper(callback); + mItemTouchHelper.attachToRecyclerView(recyclerView); + + // for clicks + recyclerView.addOnItemTouchListener(new RecyclerItemClickListener(getActivity(), this)); + + // can't use item decoration because it doesn't move with drag and drop + // recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null)); + + // We have a menu item to show in action bar. + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + inflater.inflate(R.menu.keyserver_pref_menu, menu); + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case R.id.menu_add_keyserver: + startAddKeyserverDialog(); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + private void startAddKeyserverDialog() { + // keyserver and position have no meaning + startEditKeyserverDialog(AddEditKeyserverDialogFragment.DialogAction.ADD, null, -1); + } + + private void startEditKeyserverDialog(AddEditKeyserverDialogFragment.DialogAction action, + String keyserver, final int position) { + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + Bundle data = message.getData(); + switch (message.what) { + case AddEditKeyserverDialogFragment.MESSAGE_OKAY: { + boolean deleted = + data.getBoolean(AddEditKeyserverDialogFragment.MESSAGE_KEYSERVER_DELETED + , false); + if (deleted) { + Notify.create(getActivity(), + getActivity().getString( + R.string.keyserver_preference_deleted, mKeyservers.get(position)), + Notify.Style.OK) + .show(); + deleteKeyserver(position); + return; + } + boolean verified = + data.getBoolean(AddEditKeyserverDialogFragment.MESSAGE_VERIFIED); + if (verified) { + Notify.create(getActivity(), + R.string.add_keyserver_verified, Notify.Style.OK).show(); + } else { + Notify.create(getActivity(), + R.string.add_keyserver_without_verification, + Notify.Style.WARN).show(); + } + String keyserver = data.getString( + AddEditKeyserverDialogFragment.MESSAGE_KEYSERVER); + + AddEditKeyserverDialogFragment.DialogAction dialogAction + = (AddEditKeyserverDialogFragment.DialogAction) data.getSerializable( + AddEditKeyserverDialogFragment.MESSAGE_DIALOG_ACTION); + switch (dialogAction) { + case ADD: + addKeyserver(keyserver); + break; + case EDIT: + editKeyserver(keyserver, position); + break; + } + break; + } + case AddEditKeyserverDialogFragment.MESSAGE_VERIFICATION_FAILED: { + AddEditKeyserverDialogFragment.FailureReason failureReason = + (AddEditKeyserverDialogFragment.FailureReason) data.getSerializable( + AddEditKeyserverDialogFragment.MESSAGE_FAILURE_REASON); + switch (failureReason) { + case CONNECTION_FAILED: { + Notify.create(getActivity(), + R.string.add_keyserver_connection_failed, + Notify.Style.ERROR).show(); + break; + } + case INVALID_URL: { + Notify.create(getActivity(), + R.string.add_keyserver_invalid_url, + Notify.Style.ERROR).show(); + break; + } + } + break; + } + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + AddEditKeyserverDialogFragment dialogFragment = AddEditKeyserverDialogFragment + .newInstance(messenger, action, keyserver, position); + dialogFragment.show(getFragmentManager(), "addKeyserverDialog"); + } + + private void addKeyserver(String keyserver) { + mKeyservers.add(keyserver); + mAdapter.notifyItemInserted(mKeyservers.size() - 1); + saveKeyserverList(); + } + + private void editKeyserver(String newKeyserver, int position) { + mKeyservers.set(position, newKeyserver); + mAdapter.notifyItemChanged(position); + saveKeyserverList(); + } + + private void deleteKeyserver(int position) { + if (mKeyservers.size() == 1) { + Notify.create(getActivity(), R.string.keyserver_preference_cannot_delete_last, + Notify.Style.ERROR).show(); + return; + } + mKeyservers.remove(position); + // we use this + mAdapter.notifyItemRemoved(position); + if (position == 0 && mKeyservers.size() > 0) { + // if we deleted the first item, we need the adapter to redraw the new first item + mAdapter.notifyItemChanged(0); + } + saveKeyserverList(); + } + + private void saveKeyserverList() { + String servers[] = mKeyservers.toArray(new String[mKeyservers.size()]); + Preferences.getPreferences(getActivity()).setKeyServers(servers); + } + + @Override + public void onItemClick(View view, int position) { + startEditKeyserverDialog(AddEditKeyserverDialogFragment.DialogAction.EDIT, + mKeyservers.get(position), position); + } + + public class KeyserverListAdapter extends RecyclerView.Adapter<KeyserverListAdapter.ViewHolder> + implements ItemTouchHelperAdapter { + + private final List<String> mKeyservers; + + public KeyserverListAdapter(List<String> keyservers) { + mKeyservers = keyservers; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.settings_keyserver_item, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + holder.keyserverUrl.setText(mKeyservers.get(position)); + + // Start a drag whenever the handle view it touched + holder.dragHandleView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) { + mItemTouchHelper.startDrag(holder); + } + return false; + } + }); + + selectUnselectKeyserver(holder, position); + } + + private void selectUnselectKeyserver(ViewHolder holder, int position) { + + if (position == 0) { + holder.showAsSelectedKeyserver(); + } else { + holder.showAsUnselectedKeyserver(); + } + } + + @Override + public void onItemMove(RecyclerView.ViewHolder source, RecyclerView.ViewHolder target, + int fromPosition, int toPosition) { + Collections.swap(mKeyservers, fromPosition, toPosition); + saveKeyserverList(); + selectUnselectKeyserver((ViewHolder) target, fromPosition); + // we don't want source to change color while dragging, therefore we just set + // isSelectedKeyserver instead of selectUnselectKeyserver + ((ViewHolder) source).isSelectedKeyserver = toPosition == 0; + + notifyItemMoved(fromPosition, toPosition); + } + + @Override + public int getItemCount() { + return mKeyservers.size(); + } + + public class ViewHolder extends RecyclerView.ViewHolder implements + ItemTouchHelperViewHolder { + + public final ViewGroup outerLayout; + public final TextView selectedServerLabel; + public final TextView keyserverUrl; + public final ImageView dragHandleView; + + private boolean isSelectedKeyserver = false; + + public ViewHolder(View itemView) { + super(itemView); + outerLayout = (ViewGroup) itemView.findViewById(R.id.outer_layout); + selectedServerLabel = (TextView) itemView.findViewById( + R.id.selected_keyserver_title); + keyserverUrl = (TextView) itemView.findViewById(R.id.keyserver_tv); + dragHandleView = (ImageView) itemView.findViewById(R.id.drag_handle); + + itemView.setClickable(true); + } + + public void showAsSelectedKeyserver() { + isSelectedKeyserver = true; + selectedServerLabel.setVisibility(View.VISIBLE); + outerLayout.setBackgroundColor(getResources().getColor(R.color.android_green_dark)); + } + + public void showAsUnselectedKeyserver() { + isSelectedKeyserver = false; + selectedServerLabel.setVisibility(View.GONE); + outerLayout.setBackgroundColor(Color.WHITE); + } + + @Override + public void onItemSelected() { + selectedServerLabel.setVisibility(View.GONE); + itemView.setBackgroundColor(Color.LTGRAY); + } + + @Override + public void onItemClear() { + if (isSelectedKeyserver) { + showAsSelectedKeyserver(); + } else { + showAsUnselectedKeyserver(); + } + } + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java index 5c8e6bb5d..0415128a2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java @@ -17,40 +17,42 @@ package org.sufficientlysecure.keychain.ui; -import android.app.ProgressDialog; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; import android.support.v4.app.NavUtils; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.Spinner; -import android.widget.Toast; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.ExportResult; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseActivity; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; /** * Sends the selected public key to a keyserver */ -public class UploadKeyActivity extends BaseActivity { +public class UploadKeyActivity extends BaseActivity + implements CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> { private View mUploadButton; private Spinner mKeyServerSpinner; private Uri mDataUri; + // CryptoOperationHelper.Callback vars + private String mKeyserver; + private Uri mUnifiedKeyringUri; + private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -90,52 +92,23 @@ public class UploadKeyActivity extends BaseActivity { setContentView(R.layout.upload_key_activity); } - private void uploadKey() { - // Send all information needed to service to upload key in other thread - Intent intent = new Intent(this, KeychainIntentService.class); - - intent.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING); + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (mUploadOpHelper != null) { + mUploadOpHelper.handleActivityResult(requestCode, resultCode, data); + } + super.onActivityResult(requestCode, resultCode, data); + } - // set data uri as path to keyring + private void uploadKey() { Uri blobUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); - intent.setData(blobUri); - - // fill values for this action - Bundle data = new Bundle(); + mUnifiedKeyringUri = blobUri; String server = (String) mKeyServerSpinner.getSelectedItem(); - data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server); - - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Message is received after uploading is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - this, - getString(R.string.progress_uploading), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - - Toast.makeText(UploadKeyActivity.this, R.string.msg_crt_upload_success, - Toast.LENGTH_SHORT).show(); - finish(); - } - } - }; - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - saveHandler.showProgressDialog(this); + mKeyserver = server; - // start service with intent - startService(intent); + mUploadOpHelper = new CryptoOperationHelper(1, this, this, R.string.progress_uploading); + mUploadOpHelper.cryptoOperation(); } @Override @@ -150,4 +123,29 @@ public class UploadKeyActivity extends BaseActivity { } return super.onOptionsItemSelected(item); } + + @Override + public ExportKeyringParcel createOperationInput() { + return new ExportKeyringParcel(mKeyserver, mUnifiedKeyringUri); + } + + @Override + public void onCryptoOperationSuccess(ExportResult result) { + result.createNotify(this).show(); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(ExportResult result) { + result.createNotify(this).show(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 7ae4f1be3..fd50ed5ef 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -31,9 +31,10 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.Message; -import android.os.Messenger; import android.provider.ContactsContract; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.CollapsingToolbarLayout; +import android.support.design.widget.FloatingActionButton; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; import android.support.v4.app.LoaderManager; @@ -47,16 +48,17 @@ import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; -import com.getbase.floatingactionbutton.FloatingActionButton; + + import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.operations.results.CertifyResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.KeyRing; @@ -65,13 +67,10 @@ import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler.MessageStatus; -import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; -import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; @@ -85,37 +84,45 @@ import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.NfcHelper; import org.sufficientlysecure.keychain.util.Preferences; +import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; public class ViewKeyActivity extends BaseNfcActivity implements - LoaderManager.LoaderCallbacks<Cursor> { + LoaderManager.LoaderCallbacks<Cursor>, + CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> { public static final String EXTRA_NFC_USER_ID = "nfc_user_id"; public static final String EXTRA_NFC_AID = "nfc_aid"; public static final String EXTRA_NFC_FINGERPRINTS = "nfc_fingerprints"; static final int REQUEST_QR_FINGERPRINT = 1; - static final int REQUEST_DELETE = 2; - static final int REQUEST_EXPORT = 3; + static final int REQUEST_BACKUP = 2; + static final int REQUEST_CERTIFY = 3; + static final int REQUEST_DELETE = 4; + public static final String EXTRA_DISPLAY_RESULT = "display_result"; - ExportHelper mExportHelper; ProviderHelper mProviderHelper; protected Uri mDataUri; - private TextView mName; + // For CryptoOperationHelper.Callback + private String mKeyserver; + private ArrayList<ParcelableKeyRing> mKeyList; + private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mOperationHelper; + private TextView mStatusText; private ImageView mStatusImage; - private RelativeLayout mBigToolbar; + private AppBarLayout mAppBarLayout; + private CollapsingToolbarLayout mCollapsingToolbarLayout; private ImageButton mActionEncryptFile; private ImageButton mActionEncryptText; private ImageButton mActionNfc; private FloatingActionButton mFab; private ImageView mPhoto; + private FrameLayout mPhotoLayout; private ImageView mQrCode; private CardView mQrCodeLayout; @@ -132,6 +139,8 @@ public class ViewKeyActivity extends BaseNfcActivity implements private boolean mIsRevoked = false; private boolean mIsExpired = false; + private boolean mShowYubikeyAfterCreation = false; + private MenuItem mRefreshItem; private boolean mIsRefreshing; private Animation mRotate, mRotateSpin; @@ -139,26 +148,31 @@ public class ViewKeyActivity extends BaseNfcActivity implements private String mFingerprint; private long mMasterKeyId; + private byte[] mNfcFingerprints; + private String mNfcUserId; + private byte[] mNfcAid; + @SuppressLint("InflateParams") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mExportHelper = new ExportHelper(this); mProviderHelper = new ProviderHelper(this); + mOperationHelper = new CryptoOperationHelper<>(1, this, this, null); setTitle(null); - mName = (TextView) findViewById(R.id.view_key_name); mStatusText = (TextView) findViewById(R.id.view_key_status); mStatusImage = (ImageView) findViewById(R.id.view_key_status_image); - mBigToolbar = (RelativeLayout) findViewById(R.id.toolbar_big); + mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout); + mCollapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); mActionEncryptFile = (ImageButton) findViewById(R.id.view_key_action_encrypt_files); mActionEncryptText = (ImageButton) findViewById(R.id.view_key_action_encrypt_text); mActionNfc = (ImageButton) findViewById(R.id.view_key_action_nfc); mFab = (FloatingActionButton) findViewById(R.id.fab); mPhoto = (ImageView) findViewById(R.id.view_key_photo); + mPhotoLayout = (FrameLayout) findViewById(R.id.view_key_photo_layout); mQrCode = (ImageView) findViewById(R.id.view_key_qr_code); mQrCodeLayout = (CardView) findViewById(R.id.view_key_qr_code_layout); @@ -287,14 +301,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements .replace(R.id.view_key_fragment, frag) .commit(); - if (getIntent().hasExtra(EXTRA_NFC_AID)) { - Intent intent = getIntent(); - byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_NFC_FINGERPRINTS); - String nfcUserId = intent.getStringExtra(EXTRA_NFC_USER_ID); - byte[] nfcAid = intent.getByteArrayExtra(EXTRA_NFC_AID); - showYubiKeyFragment(nfcFingerprints, nfcUserId, nfcAid); + if (Preferences.getPreferences(this).getExperimentalEnableKeybase()) { + final ViewKeyKeybaseFragment keybaseFrag = ViewKeyKeybaseFragment.newInstance(mDataUri); + manager.beginTransaction() + .replace(R.id.view_key_keybase_fragment, keybaseFrag) + .commit(); } + // need to postpone loading of the yubikey fragment until after mMasterKeyId + // is available, but we mark here that this should be done + mShowYubikeyAfterCreation = true; + } @Override @@ -320,31 +337,11 @@ public class ViewKeyActivity extends BaseNfcActivity implements return true; } case R.id.menu_key_view_export_file: { - try { - if (PassphraseCacheService.getCachedPassphrase(this, mMasterKeyId, mMasterKeyId) != null) { - exportToFile(mDataUri, mExportHelper, mProviderHelper); - return true; - } - - startPassphraseActivity(REQUEST_EXPORT); - } catch (PassphraseCacheService.KeyNotFoundException e) { - // This happens when the master key is stripped - exportToFile(mDataUri, mExportHelper, mProviderHelper); - } + startPassphraseActivity(REQUEST_BACKUP); return true; } case R.id.menu_key_view_delete: { - try { - if (PassphraseCacheService.getCachedPassphrase(this, mMasterKeyId, mMasterKeyId) != null) { - deleteKey(); - return true; - } - - startPassphraseActivity(REQUEST_DELETE); - } catch (PassphraseCacheService.KeyNotFoundException e) { - // This happens when the master key is stripped - deleteKey(); - } + deleteKey(); return true; } case R.id.menu_key_view_advanced: { @@ -372,7 +369,11 @@ public class ViewKeyActivity extends BaseNfcActivity implements return true; } case R.id.menu_key_view_certify_fingerprint: { - certifyFingeprint(mDataUri); + certifyFingeprint(mDataUri, false); + return true; + } + case R.id.menu_key_view_certify_fingerprint_word: { + certifyFingeprint(mDataUri, true); return true; } } @@ -384,11 +385,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements MenuItem editKey = menu.findItem(R.id.menu_key_view_edit); editKey.setVisible(mIsSecret); + MenuItem exportKey = menu.findItem(R.id.menu_key_view_export_file); + exportKey.setVisible(mIsSecret); + MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity); addLinked.setVisible(mIsSecret); MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint); certifyFingerprint.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked); + MenuItem certifyFingerprintWord = menu.findItem(R.id.menu_key_view_certify_fingerprint_word); + certifyFingerprintWord.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked + && Preferences.getPreferences(this).getExperimentalEnableWordConfirm()); return true; } @@ -400,40 +407,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements startActivityForResult(scanQrCode, REQUEST_QR_FINGERPRINT); } - private void certifyFingeprint(Uri dataUri) { + private void certifyFingeprint(Uri dataUri, boolean enableWordConfirm) { Intent intent = new Intent(this, CertifyFingerprintActivity.class); intent.setData(dataUri); + intent.putExtra(CertifyFingerprintActivity.EXTRA_ENABLE_WORD_CONFIRM, enableWordConfirm); - startCertifyIntent(intent); + startActivityForResult(intent, REQUEST_CERTIFY); } private void certifyImmediate() { Intent intent = new Intent(this, CertifyKeyActivity.class); - intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] {mMasterKeyId}); + intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[]{mMasterKeyId}); - startCertifyIntent(intent); - } - - private void startCertifyIntent(Intent intent) { - // Message is received after signing is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler(this) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - Bundle data = message.getData(); - CertifyResult result = data.getParcelable(CertifyResult.EXTRA_RESULT); - - result.createNotify(ViewKeyActivity.this).show(); - } - } - }; - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - startActivityForResult(intent, 0); + startActivityForResult(intent, REQUEST_CERTIFY); } private void showQrCodeDialog() { @@ -458,95 +444,108 @@ public class ViewKeyActivity extends BaseNfcActivity implements startActivityForResult(intent, requestCode); } - private void exportToFile(Uri dataUri, ExportHelper exportHelper, ProviderHelper providerHelper) { - try { - Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri); - - HashMap<String, Object> data = providerHelper.getGenericData( - baseUri, - new String[] {KeychainContract.Keys.MASTER_KEY_ID, KeychainContract.KeyRings.HAS_SECRET}, - new int[] {ProviderHelper.FIELD_TYPE_INTEGER, ProviderHelper.FIELD_TYPE_INTEGER}); - - exportHelper.showExportKeysDialog( - new long[] {(Long) data.get(KeychainContract.KeyRings.MASTER_KEY_ID)}, - Constants.Path.APP_DIR_FILE, ((Long) data.get(KeychainContract.KeyRings.HAS_SECRET) != 0) - ); - } catch (ProviderHelper.NotFoundException e) { - Notify.create(this, R.string.error_key_not_found, Notify.Style.ERROR).show(); - Log.e(Constants.TAG, "Key not found", e); - } + private void backupToFile() { + new ExportHelper(this).showExportKeysDialog( + mMasterKeyId, new File(Constants.Path.APP_DIR, + KeyFormattingUtils.convertKeyIdToHex(mMasterKeyId) + ".sec.asc"), true); } private void deleteKey() { - // Message is received after key is deleted - Handler returnHandler = new Handler() { - @Override - public void handleMessage(Message message) { - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - setResult(RESULT_CANCELED); - finish(); - } - } - }; + Intent deleteIntent = new Intent(this, DeleteKeyDialogActivity.class); + + deleteIntent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, + new long[]{mMasterKeyId}); + deleteIntent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, mIsSecret); + if (mIsSecret) { + // for upload in case key is secret + deleteIntent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER, + Preferences.getPreferences(this).getPreferredKeyserver()); + } - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(returnHandler); - DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger, - new long[] {mMasterKeyId}); - deleteKeyDialog.show(getSupportFragmentManager(), "deleteKeyDialog"); + startActivityForResult(deleteIntent, REQUEST_DELETE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_QR_FINGERPRINT && resultCode == Activity.RESULT_OK) { + if (mOperationHelper.handleActivityResult(requestCode, resultCode, data)) { + return; + } - // If there is an EXTRA_RESULT, that's an error. Just show it. - if (data.hasExtra(OperationResult.EXTRA_RESULT)) { - OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); - result.createNotify(this).show(); + switch (requestCode) { + case REQUEST_QR_FINGERPRINT: { + + if (resultCode != Activity.RESULT_OK) { + return; + } + + // If there is an EXTRA_RESULT, that's an error. Just show it. + if (data.hasExtra(OperationResult.EXTRA_RESULT)) { + OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); + result.createNotify(this).show(); + return; + } + + String fp = data.getStringExtra(ImportKeysProxyActivity.EXTRA_FINGERPRINT); + if (fp == null) { + Notify.create(this, R.string.error_scan_fp, Notify.LENGTH_LONG, Style.ERROR).show(); + return; + } + if (mFingerprint.equalsIgnoreCase(fp)) { + certifyImmediate(); + } else { + Notify.create(this, R.string.error_scan_match, Notify.LENGTH_LONG, Style.ERROR).show(); + } return; } - String fp = data.getStringExtra(ImportKeysProxyActivity.EXTRA_FINGERPRINT); - if (fp == null) { - Notify.create(this, "Error scanning fingerprint!", - Notify.LENGTH_LONG, Notify.Style.ERROR).show(); + case REQUEST_BACKUP: { + if (resultCode != Activity.RESULT_OK) { + return; + } + + backupToFile(); return; } - if (mFingerprint.equalsIgnoreCase(fp)) { - certifyImmediate(); - } else { - Notify.create(this, "Fingerprints did not match!", - Notify.LENGTH_LONG, Notify.Style.ERROR).show(); + + case REQUEST_CERTIFY: { + if (resultCode != Activity.RESULT_OK) { + return; + } + + if (data.hasExtra(OperationResult.EXTRA_RESULT)) { + OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); + result.createNotify(this).show(); + } + return; } - return; - } + case REQUEST_DELETE: { + if (resultCode != Activity.RESULT_OK) { + return; + } - if (requestCode == REQUEST_DELETE && resultCode == Activity.RESULT_OK) { - deleteKey(); + setResult(RESULT_OK, data); + finish(); + return; + } } - if (requestCode == REQUEST_EXPORT && resultCode == Activity.RESULT_OK) { - exportToFile(mDataUri, mExportHelper, mProviderHelper); - } + super.onActivityResult(requestCode, resultCode, data); - if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { - OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); - result.createNotify(this).show(); - } else { - super.onActivityResult(requestCode, resultCode, data); - } } @Override - protected void onNfcPerform() throws IOException { + protected void doNfcInBackground() throws IOException { - final byte[] nfcFingerprints = nfcGetFingerprints(); - final String nfcUserId = nfcGetUserId(); - final byte[] nfcAid = nfcGetAid(); + mNfcFingerprints = nfcGetFingerprints(); + mNfcUserId = nfcGetUserId(); + mNfcAid = nfcGetAid(); + } + + @Override + protected void onNfcPostExecute() throws IOException { - long yubiKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(nfcFingerprints); + long yubiKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints); try { @@ -557,7 +556,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements // if the master key of that key matches this one, just show the yubikey dialog if (KeyFormattingUtils.convertFingerprintToHex(candidateFp).equals(mFingerprint)) { - showYubiKeyFragment(nfcFingerprints, nfcUserId, nfcAid); + showYubiKeyFragment(mNfcFingerprints, mNfcUserId, mNfcAid); return; } @@ -570,14 +569,13 @@ public class ViewKeyActivity extends BaseNfcActivity implements Intent intent = new Intent( ViewKeyActivity.this, ViewKeyActivity.class); intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, nfcAid); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, nfcUserId); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, nfcFingerprints); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, mNfcAid); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, mNfcUserId); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, mNfcFingerprints); startActivity(intent); finish(); } }, R.string.snack_yubikey_view).show(); - // and if it's not found, offer import } catch (PgpKeyNotFoundException e) { Notify.create(this, R.string.snack_yubi_other, Notify.LENGTH_LONG, @@ -586,28 +584,36 @@ public class ViewKeyActivity extends BaseNfcActivity implements public void onAction() { Intent intent = new Intent( ViewKeyActivity.this, CreateKeyActivity.class); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, nfcAid); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, nfcUserId); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, nfcFingerprints); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, mNfcAid); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, mNfcUserId); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, mNfcFingerprints); startActivity(intent); finish(); } }, R.string.snack_yubikey_import).show(); } - } - public void showYubiKeyFragment(byte[] nfcFingerprints, String nfcUserId, byte[] nfcAid) { - ViewKeyYubiKeyFragment frag = ViewKeyYubiKeyFragment.newInstance( - nfcFingerprints, nfcUserId, nfcAid); + public void showYubiKeyFragment( + final byte[] nfcFingerprints, final String nfcUserId, final byte[] nfcAid) { - FragmentManager manager = getSupportFragmentManager(); + new Handler().post(new Runnable() { + @Override + public void run() { + ViewKeyYubiKeyFragment frag = ViewKeyYubiKeyFragment.newInstance( + mMasterKeyId, nfcFingerprints, nfcUserId, nfcAid); + + FragmentManager manager = getSupportFragmentManager(); + + manager.popBackStack("yubikey", FragmentManager.POP_BACK_STACK_INCLUSIVE); + manager.beginTransaction() + .addToBackStack("yubikey") + .replace(R.id.view_key_fragment, frag) + // if this is called while the activity wasn't resumed, just forget it happened + .commitAllowingStateLoss(); + } + }); - manager.popBackStack("yubikey", FragmentManager.POP_BACK_STACK_INCLUSIVE); - manager.beginTransaction() - .addToBackStack("yubikey") - .replace(R.id.view_key_fragment, frag) - .commit(); } private void encrypt(Uri dataUri, boolean text) { @@ -620,7 +626,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements long keyId = new ProviderHelper(this) .getCachedPublicKeyRing(dataUri) .extractOrGetMasterKeyId(); - long[] encryptionKeyIds = new long[] {keyId}; + long[] encryptionKeyIds = new long[]{keyId}; Intent intent; if (text) { intent = new Intent(this, EncryptTextActivity.class); @@ -638,76 +644,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements } } - private void updateFromKeyserver(Uri dataUri, ProviderHelper providerHelper) - throws ProviderHelper.NotFoundException { - - mIsRefreshing = true; - mRefreshItem.setEnabled(false); - mRefreshItem.setActionView(mRefresh); - mRefresh.startAnimation(mRotate); - - byte[] blob = (byte[]) providerHelper.getGenericData( - KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri), - KeychainContract.Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob); - - ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null, null); - ArrayList<ParcelableKeyRing> entries = new ArrayList<>(); - entries.add(keyEntry); - - // Message is received after importing is done in KeychainIntentService - ServiceProgressHandler serviceHandler = new ServiceProgressHandler(this) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - - mIsRefreshing = false; - - if (returnData == null) { - finish(); - return; - } - final ImportKeyResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - result.createNotify(ViewKeyActivity.this).show(); - } - } - }; - - // fill values for this action - Bundle data = new Bundle(); - - // search config - { - Preferences prefs = Preferences.getPreferences(this); - Preferences.CloudSearchPrefs cloudPrefs = - new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver()); - data.putString(KeychainIntentService.IMPORT_KEY_SERVER, cloudPrefs.keyserver); - } - - data.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, entries); - - // Send all information needed to service to query keys in other thread - Intent intent = new Intent(this, KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(serviceHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - serviceHandler.showProgressDialog(this); - - // start service with intent - startService(intent); - - } - private void editKey(Uri dataUri) { Intent editIntent = new Intent(this, EditKeyActivity.class); editIntent.setData(KeychainContract.KeyRingData.buildSecretKeyRingUri(dataUri)); @@ -735,9 +671,12 @@ public class ViewKeyActivity extends BaseNfcActivity implements AsyncTask<Void, Void, Bitmap> loadTask = new AsyncTask<Void, Void, Bitmap>() { protected Bitmap doInBackground(Void... unused) { - String qrCodeContent = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; + Uri uri = new Uri.Builder() + .scheme(Constants.FINGERPRINT_SCHEME) + .opaquePart(fingerprint) + .build(); // render with minimal size - return QrCodeUtils.getQRCodeBitmap(qrCodeContent, 0); + return QrCodeUtils.getQRCodeBitmap(uri, 0); } protected void onPostExecute(Bitmap qrCode) { @@ -761,7 +700,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements // These are the rows that we will retrieve. - static final String[] PROJECTION = new String[] { + static final String[] PROJECTION = new String[]{ KeychainContract.KeyRings._ID, KeychainContract.KeyRings.MASTER_KEY_ID, KeychainContract.KeyRings.USER_ID, @@ -797,6 +736,25 @@ public class ViewKeyActivity extends BaseNfcActivity implements int mPreviousColor = 0; + /** + * Calculate a reasonable color for the status bar based on the given toolbar color. + * Style guides want the toolbar color to be a "700" on the Android scale and the status + * bar should be the same color at "500", this is roughly 17 / 20th of the value in each + * channel. + * http://www.google.com/design/spec/style/color.html#color-color-palette + */ + static public int getStatusBarBackgroundColor(int color) { + int r = (color >> 16) & 0xff; + int g = (color >> 8) & 0xff; + int b = color & 0xff; + + r = r * 17 / 20; + g = g * 17 / 20; + b = b * 17 / 20; + + return (0xff << 24) | (r << 16) | (g << 8) | b; + } + @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { /* TODO better error handling? May cause problems when a key is deleted, @@ -809,172 +767,181 @@ public class ViewKeyActivity extends BaseNfcActivity implements // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) switch (loader.getId()) { - case LOADER_ID_UNIFIED: { - if (!data.moveToFirst()) { - return; - } - - // get name, email, and comment from USER_ID - KeyRing.UserId mainUserId = KeyRing.splitUserId(data.getString(INDEX_USER_ID)); - if (mainUserId.name != null) { - mName.setText(mainUserId.name); - } else { - mName.setText(R.string.user_id_no_name); - } - - mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - mFingerprint = KeyFormattingUtils.convertFingerprintToHex( - data.getBlob(INDEX_FINGERPRINT)); - - mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; - mHasEncrypt = data.getInt(INDEX_HAS_ENCRYPT) != 0; - mIsRevoked = data.getInt(INDEX_IS_REVOKED) > 0; - mIsExpired = data.getInt(INDEX_IS_EXPIRED) != 0; - mIsVerified = data.getInt(INDEX_VERIFIED) > 0; - - // if the refresh animation isn't playing - if (!mRotate.hasStarted() && !mRotateSpin.hasStarted()) { - // re-create options menu based on mIsSecret, mIsVerified - supportInvalidateOptionsMenu(); - // this is done at the end of the animation otherwise - } - - AsyncTask<Long, Void, Bitmap> photoTask = - new AsyncTask<Long, Void, Bitmap>() { - protected Bitmap doInBackground(Long... mMasterKeyId) { - return ContactHelper.loadPhotoByMasterKeyId(getContentResolver(), mMasterKeyId[0], true); - } - - protected void onPostExecute(Bitmap photo) { - mPhoto.setImageBitmap(photo); - mPhoto.setVisibility(View.VISIBLE); - } - }; - - // Note: order is important - int color; - if (mIsRevoked) { - mStatusText.setText(R.string.view_key_revoked); - mStatusImage.setVisibility(View.VISIBLE); - KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText, - State.REVOKED, R.color.icons, true); - color = getResources().getColor(R.color.android_red_light); - - mActionEncryptFile.setVisibility(View.GONE); - mActionEncryptText.setVisibility(View.GONE); - mActionNfc.setVisibility(View.GONE); - mFab.setVisibility(View.GONE); - mQrCodeLayout.setVisibility(View.GONE); - } else if (mIsExpired) { - if (mIsSecret) { - mStatusText.setText(R.string.view_key_expired_secret); + if (data.moveToFirst()) { + // get name, email, and comment from USER_ID + KeyRing.UserId mainUserId = KeyRing.splitUserId(data.getString(INDEX_USER_ID)); + if (mainUserId.name != null) { + mCollapsingToolbarLayout.setTitle(mainUserId.name); } else { - mStatusText.setText(R.string.view_key_expired); - } - mStatusImage.setVisibility(View.VISIBLE); - KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText, - State.EXPIRED, R.color.icons, true); - color = getResources().getColor(R.color.android_red_light); - - mActionEncryptFile.setVisibility(View.GONE); - mActionEncryptText.setVisibility(View.GONE); - mActionNfc.setVisibility(View.GONE); - mFab.setVisibility(View.GONE); - mQrCodeLayout.setVisibility(View.GONE); - } else if (mIsSecret) { - mStatusText.setText(R.string.view_key_my_key); - mStatusImage.setVisibility(View.GONE); - color = getResources().getColor(R.color.primary); - // reload qr code only if the fingerprint changed - if (!mFingerprint.equals(mQrCodeLoaded)) { - loadQrCode(mFingerprint); - } - photoTask.execute(mMasterKeyId); - mQrCodeLayout.setVisibility(View.VISIBLE); - - // and place leftOf qr code - RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams) - mName.getLayoutParams(); - // remove right margin - nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - nameParams.setMarginEnd(0); + mCollapsingToolbarLayout.setTitle(getString(R.string.user_id_no_name)); } - nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); - mName.setLayoutParams(nameParams); - - RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams) - mStatusText.getLayoutParams(); - statusParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - statusParams.setMarginEnd(0); - } - statusParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); - mStatusText.setLayoutParams(statusParams); - mActionEncryptFile.setVisibility(View.VISIBLE); - mActionEncryptText.setVisibility(View.VISIBLE); + mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); + mFingerprint = KeyFormattingUtils.convertFingerprintToHex(data.getBlob(INDEX_FINGERPRINT)); + + // if it wasn't shown yet, display yubikey fragment + if (mShowYubikeyAfterCreation && getIntent().hasExtra(EXTRA_NFC_AID)) { + mShowYubikeyAfterCreation = false; + Intent intent = getIntent(); + byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_NFC_FINGERPRINTS); + String nfcUserId = intent.getStringExtra(EXTRA_NFC_USER_ID); + byte[] nfcAid = intent.getByteArrayExtra(EXTRA_NFC_AID); + showYubiKeyFragment(nfcFingerprints, nfcUserId, nfcAid); + } - // invokeBeam is available from API 21 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mActionNfc.setVisibility(View.VISIBLE); - } else { - mActionNfc.setVisibility(View.GONE); + mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; + mHasEncrypt = data.getInt(INDEX_HAS_ENCRYPT) != 0; + mIsRevoked = data.getInt(INDEX_IS_REVOKED) > 0; + mIsExpired = data.getInt(INDEX_IS_EXPIRED) != 0; + mIsVerified = data.getInt(INDEX_VERIFIED) > 0; + + // if the refresh animation isn't playing + if (!mRotate.hasStarted() && !mRotateSpin.hasStarted()) { + // re-create options menu based on mIsSecret, mIsVerified + supportInvalidateOptionsMenu(); + // this is done at the end of the animation otherwise } - mFab.setVisibility(View.VISIBLE); - mFab.setIconDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp)); - } else { - mActionEncryptFile.setVisibility(View.VISIBLE); - mActionEncryptText.setVisibility(View.VISIBLE); - mQrCodeLayout.setVisibility(View.GONE); - mActionNfc.setVisibility(View.GONE); - if (mIsVerified) { - mStatusText.setText(R.string.view_key_verified); + AsyncTask<Long, Void, Bitmap> photoTask = + new AsyncTask<Long, Void, Bitmap>() { + protected Bitmap doInBackground(Long... mMasterKeyId) { + return ContactHelper.loadPhotoByMasterKeyId(getContentResolver(), + mMasterKeyId[0], true); + } + + protected void onPostExecute(Bitmap photo) { + if (photo == null) { + return; + } + + mPhoto.setImageBitmap(photo); + mPhotoLayout.setVisibility(View.VISIBLE); + } + }; + + // Note: order is important + int color; + if (mIsRevoked) { + mStatusText.setText(R.string.view_key_revoked); mStatusImage.setVisibility(View.VISIBLE); KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText, - State.VERIFIED, R.color.icons, true); - color = getResources().getColor(R.color.primary); - photoTask.execute(mMasterKeyId); + State.REVOKED, R.color.icons, true); + color = getResources().getColor(R.color.key_flag_red); + mActionEncryptFile.setVisibility(View.INVISIBLE); + mActionEncryptText.setVisibility(View.INVISIBLE); + mActionNfc.setVisibility(View.INVISIBLE); mFab.setVisibility(View.GONE); - } else { - mStatusText.setText(R.string.view_key_unverified); + mQrCodeLayout.setVisibility(View.GONE); + } else if (mIsExpired) { + if (mIsSecret) { + mStatusText.setText(R.string.view_key_expired_secret); + } else { + mStatusText.setText(R.string.view_key_expired); + } mStatusImage.setVisibility(View.VISIBLE); KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText, - State.UNVERIFIED, R.color.icons, true); - color = getResources().getColor(R.color.android_orange_light); + State.EXPIRED, R.color.icons, true); + color = getResources().getColor(R.color.key_flag_red); + mActionEncryptFile.setVisibility(View.INVISIBLE); + mActionEncryptText.setVisibility(View.INVISIBLE); + mActionNfc.setVisibility(View.INVISIBLE); + mFab.setVisibility(View.GONE); + mQrCodeLayout.setVisibility(View.GONE); + } else if (mIsSecret) { + mStatusText.setText(R.string.view_key_my_key); + mStatusImage.setVisibility(View.GONE); + color = getResources().getColor(R.color.key_flag_green); + // reload qr code only if the fingerprint changed + if (!mFingerprint.equals(mQrCodeLoaded)) { + loadQrCode(mFingerprint); + } + photoTask.execute(mMasterKeyId); + mQrCodeLayout.setVisibility(View.VISIBLE); + + // and place leftOf qr code +// RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams) +// mName.getLayoutParams(); +// // remove right margin +// nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0); +// if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { +// nameParams.setMarginEnd(0); +// } +// nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); +// mName.setLayoutParams(nameParams); + + RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams) + mStatusText.getLayoutParams(); + statusParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + statusParams.setMarginEnd(0); + } + statusParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); + mStatusText.setLayoutParams(statusParams); + + mActionEncryptFile.setVisibility(View.VISIBLE); + mActionEncryptText.setVisibility(View.VISIBLE); + + // invokeBeam is available from API 21 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mActionNfc.setVisibility(View.VISIBLE); + } else { + mActionNfc.setVisibility(View.GONE); + } mFab.setVisibility(View.VISIBLE); - } - } + // noinspection deprecation (no getDrawable with theme at current minApi level 15!) + mFab.setImageDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp)); + } else { + mActionEncryptFile.setVisibility(View.VISIBLE); + mActionEncryptText.setVisibility(View.VISIBLE); + mQrCodeLayout.setVisibility(View.GONE); + mActionNfc.setVisibility(View.GONE); - if (mPreviousColor == 0 || mPreviousColor == color) { - mStatusBar.setBackgroundColor(color); - mBigToolbar.setBackgroundColor(color); - mPreviousColor = color; - } else { - ObjectAnimator colorFade1 = - ObjectAnimator.ofObject(mStatusBar, "backgroundColor", - new ArgbEvaluator(), mPreviousColor, color); - ObjectAnimator colorFade2 = - ObjectAnimator.ofObject(mBigToolbar, "backgroundColor", - new ArgbEvaluator(), mPreviousColor, color); - - colorFade1.setDuration(1200); - colorFade2.setDuration(1200); - colorFade1.start(); - colorFade2.start(); - mPreviousColor = color; - } + if (mIsVerified) { + mStatusText.setText(R.string.view_key_verified); + mStatusImage.setVisibility(View.VISIBLE); + KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText, + State.VERIFIED, R.color.icons, true); + color = getResources().getColor(R.color.key_flag_green); + photoTask.execute(mMasterKeyId); + + mFab.setVisibility(View.GONE); + } else { + mStatusText.setText(R.string.view_key_unverified); + mStatusImage.setVisibility(View.VISIBLE); + KeyFormattingUtils.setStatusImage(this, mStatusImage, mStatusText, + State.UNVERIFIED, R.color.icons, true); + color = getResources().getColor(R.color.key_flag_orange); + + mFab.setVisibility(View.VISIBLE); + } + } - //noinspection deprecation - mStatusImage.setAlpha(80); + if (mPreviousColor == 0 || mPreviousColor == color) { + mAppBarLayout.setBackgroundColor(color); + mCollapsingToolbarLayout.setContentScrimColor(color); + mCollapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color)); + mPreviousColor = color; + } else { + ObjectAnimator colorFade = + ObjectAnimator.ofObject(mAppBarLayout, "backgroundColor", + new ArgbEvaluator(), mPreviousColor, color); + mCollapsingToolbarLayout.setContentScrimColor(color); + mCollapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color)); + + colorFade.setDuration(1200); + colorFade.start(); + mPreviousColor = color; + } - break; + //noinspection deprecation + mStatusImage.setAlpha(80); + break; + } } } } @@ -984,4 +951,64 @@ public class ViewKeyActivity extends BaseNfcActivity implements } + // CryptoOperationHelper.Callback functions + + + private void updateFromKeyserver(Uri dataUri, ProviderHelper providerHelper) + throws ProviderHelper.NotFoundException { + + mIsRefreshing = true; + mRefreshItem.setEnabled(false); + mRefreshItem.setActionView(mRefresh); + mRefresh.startAnimation(mRotate); + + byte[] blob = (byte[]) providerHelper.getGenericData( + KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri), + KeychainContract.Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob); + + ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null, null); + ArrayList<ParcelableKeyRing> entries = new ArrayList<>(); + entries.add(keyEntry); + mKeyList = entries; + + // search config + { + Preferences prefs = Preferences.getPreferences(this); + Preferences.CloudSearchPrefs cloudPrefs = + new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver()); + mKeyserver = cloudPrefs.keyserver; + } + + mOperationHelper.cryptoOperation(); + } + + @Override + public ImportKeyringParcel createOperationInput() { + return new ImportKeyringParcel(mKeyList, mKeyserver); + } + + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + mIsRefreshing = false; + result.createNotify(this).show(); + } + + @Override + public void onCryptoOperationCancelled() { + mIsRefreshing = false; + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + mIsRefreshing = false; + result.createNotify(this).show(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return true; + } + } + diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java index 9e8a12c8a..edd9feec9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java @@ -53,13 +53,14 @@ public class ViewKeyAdvActivity extends BaseActivity implements protected Uri mDataUri; public static final String EXTRA_SELECTED_TAB = "selected_tab"; - public static final int TAB_MAIN = 0; - public static final int TAB_SHARE = 1; + public static final int TAB_SHARE = 0; + public static final int TAB_IDENTITIES = 1; + public static final int TAB_SUBKEYS = 2; + public static final int TAB_CERTS = 3; // view private ViewPager mViewPager; private PagerSlidingTabStrip mSlidingTabLayout; - private PagerTabStripAdapter mTabsAdapter; private static final int LOADER_ID_UNIFIED = 0; @@ -80,11 +81,8 @@ public class ViewKeyAdvActivity extends BaseActivity implements mViewPager = (ViewPager) findViewById(R.id.pager); mSlidingTabLayout = (PagerSlidingTabStrip) findViewById(R.id.sliding_tab_layout); - int switchToTab = TAB_MAIN; Intent intent = getIntent(); - if (intent.getExtras() != null && intent.getExtras().containsKey(EXTRA_SELECTED_TAB)) { - switchToTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB); - } + int switchToTab = intent.getIntExtra(EXTRA_SELECTED_TAB, TAB_SHARE); mDataUri = getIntent().getData(); if (mDataUri == null) { @@ -102,8 +100,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements } } - Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); - // Prepare the loaders. Either re-connect with an existing ones, // or start new ones. getSupportLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); @@ -120,34 +116,29 @@ public class ViewKeyAdvActivity extends BaseActivity implements } private void initTabs(Uri dataUri) { - mTabsAdapter = new PagerTabStripAdapter(this); - mViewPager.setAdapter(mTabsAdapter); + PagerTabStripAdapter adapter = new PagerTabStripAdapter(this); + mViewPager.setAdapter(adapter); Bundle shareBundle = new Bundle(); shareBundle.putParcelable(ViewKeyAdvUserIdsFragment.ARG_DATA_URI, dataUri); - mTabsAdapter.addTab(ViewKeyAdvShareFragment.class, + adapter.addTab(ViewKeyAdvShareFragment.class, shareBundle, getString(R.string.key_view_tab_share)); Bundle userIdsBundle = new Bundle(); userIdsBundle.putParcelable(ViewKeyAdvUserIdsFragment.ARG_DATA_URI, dataUri); - mTabsAdapter.addTab(ViewKeyAdvUserIdsFragment.class, + adapter.addTab(ViewKeyAdvUserIdsFragment.class, userIdsBundle, getString(R.string.section_user_ids)); Bundle keysBundle = new Bundle(); keysBundle.putParcelable(ViewKeyAdvSubkeysFragment.ARG_DATA_URI, dataUri); - mTabsAdapter.addTab(ViewKeyAdvSubkeysFragment.class, + adapter.addTab(ViewKeyAdvSubkeysFragment.class, keysBundle, getString(R.string.key_view_tab_keys)); Bundle certsBundle = new Bundle(); certsBundle.putParcelable(ViewKeyAdvCertsFragment.ARG_DATA_URI, dataUri); - mTabsAdapter.addTab(ViewKeyAdvCertsFragment.class, + adapter.addTab(ViewKeyAdvCertsFragment.class, certsBundle, getString(R.string.key_view_tab_certs)); - Bundle trustBundle = new Bundle(); - trustBundle.putParcelable(ViewKeyTrustFragment.ARG_DATA_URI, dataUri); - mTabsAdapter.addTab(ViewKeyTrustFragment.class, - trustBundle, getString(R.string.key_view_tab_keybase)); - // update layout after operations mSlidingTabLayout.setViewPager(mViewPager); } @@ -185,11 +176,8 @@ public class ViewKeyAdvActivity extends BaseActivity implements @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { - /* TODO better error handling? May cause problems when a key is deleted, - * because the notification triggers faster than the activity closes. - */ // Avoid NullPointerExceptions... - if (data.getCount() == 0) { + if (data == null || data.getCount() == 0) { return; } // Swap the new cursor in. (The framework will take care of closing the @@ -217,18 +205,18 @@ public class ViewKeyAdvActivity extends BaseActivity implements // Note: order is important int color; if (isRevoked || isExpired) { - color = getResources().getColor(R.color.android_red_light); + color = getResources().getColor(R.color.key_flag_red); } else if (isSecret) { - color = getResources().getColor(R.color.primary); + color = getResources().getColor(R.color.android_green_light); } else { if (isVerified) { - color = getResources().getColor(R.color.primary); + color = getResources().getColor(R.color.android_green_light); } else { - color = getResources().getColor(R.color.android_orange_light); + color = getResources().getColor(R.color.key_flag_orange); } } mToolbar.setBackgroundColor(color); - mStatusBar.setBackgroundColor(color); + mStatusBar.setBackgroundColor(ViewKeyActivity.getStatusBarBackgroundColor(color)); mSlidingTabLayout.setBackgroundColor(color); break; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java index fde0f62fd..4a46896bc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java @@ -17,7 +17,17 @@ package org.sufficientlysecure.keychain.ui; + +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import android.app.Activity; import android.app.ActivityOptions; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; @@ -42,72 +52,51 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; +import org.sufficientlysecure.keychain.util.ExportHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.NfcHelper; -import java.io.BufferedWriter; -import java.io.OutputStreamWriter; -import java.io.IOException; -import java.io.FileNotFoundException; - public class ViewKeyAdvShareFragment extends LoaderFragment implements LoaderManager.LoaderCallbacks<Cursor> { public static final String ARG_DATA_URI = "uri"; - private TextView mFingerprint; private ImageView mQrCode; private CardView mQrCodeLayout; - private View mFingerprintShareButton; - private View mFingerprintClipboardButton; - private View mKeyShareButton; - private View mKeyClipboardButton; - private View mKeyNfcButton; - private ImageButton mKeySafeSlingerButton; - private View mKeyUploadButton; - - ProviderHelper mProviderHelper; + private TextView mFingerprintView; + NfcHelper mNfcHelper; private static final int LOADER_ID_UNIFIED = 0; private Uri mDataUri; + private byte[] mFingerprint; + private long mMasterKeyId; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { View root = super.onCreateView(inflater, superContainer, savedInstanceState); View view = inflater.inflate(R.layout.view_key_adv_share_fragment, getContainer()); - mProviderHelper = new ProviderHelper(ViewKeyAdvShareFragment.this.getActivity()); - mNfcHelper = new NfcHelper(getActivity(), mProviderHelper); + ProviderHelper providerHelper = new ProviderHelper(ViewKeyAdvShareFragment.this.getActivity()); + mNfcHelper = new NfcHelper(getActivity(), providerHelper); - mFingerprint = (TextView) view.findViewById(R.id.view_key_fingerprint); + mFingerprintView = (TextView) view.findViewById(R.id.view_key_fingerprint); mQrCode = (ImageView) view.findViewById(R.id.view_key_qr_code); mQrCodeLayout = (CardView) view.findViewById(R.id.view_key_qr_code_layout); - mFingerprintShareButton = view.findViewById(R.id.view_key_action_fingerprint_share); - mFingerprintClipboardButton = view.findViewById(R.id.view_key_action_fingerprint_clipboard); - mKeyShareButton = view.findViewById(R.id.view_key_action_key_share); - mKeyNfcButton = view.findViewById(R.id.view_key_action_key_nfc); - mKeyClipboardButton = view.findViewById(R.id.view_key_action_key_clipboard); - mKeySafeSlingerButton = (ImageButton) view.findViewById(R.id.view_key_action_key_safeslinger); - mKeyUploadButton = view.findViewById(R.id.view_key_action_upload); - - mKeySafeSlingerButton.setColorFilter(getResources().getColor(R.color.tertiary_text_light), - PorterDuff.Mode.SRC_IN); - mQrCodeLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -115,45 +104,67 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements } }); - mFingerprintShareButton.setOnClickListener(new View.OnClickListener() { + View vFingerprintShareButton = view.findViewById(R.id.view_key_action_fingerprint_share); + View vFingerprintClipboardButton = view.findViewById(R.id.view_key_action_fingerprint_clipboard); + View vKeyShareButton = view.findViewById(R.id.view_key_action_key_share); + View vKeySafeButton = view.findViewById(R.id.view_key_action_key_export); + View vKeyNfcButton = view.findViewById(R.id.view_key_action_key_nfc); + View vKeyClipboardButton = view.findViewById(R.id.view_key_action_key_clipboard); + ImageButton vKeySafeSlingerButton = (ImageButton) view.findViewById(R.id.view_key_action_key_safeslinger); + View vKeyUploadButton = view.findViewById(R.id.view_key_action_upload); + vKeySafeSlingerButton.setColorFilter(FormattingUtils.getColorFromAttr(getActivity(), R.attr.colorTertiaryText), + PorterDuff.Mode.SRC_IN); + + vFingerprintShareButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - share(mDataUri, mProviderHelper, true, false); + share(true, false); } }); - mFingerprintClipboardButton.setOnClickListener(new View.OnClickListener() { + vFingerprintClipboardButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - share(mDataUri, mProviderHelper, true, true); + share(true, true); } }); - mKeyShareButton.setOnClickListener(new View.OnClickListener() { + vKeyShareButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - share(mDataUri, mProviderHelper, false, false); + share(false, false); } }); - mKeyClipboardButton.setOnClickListener(new View.OnClickListener() { + vKeySafeButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - share(mDataUri, mProviderHelper, false, true); + exportToFile(); } }); - - mKeyNfcButton.setOnClickListener(new View.OnClickListener() { + vKeyClipboardButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - mNfcHelper.invokeNfcBeam(); + share(false, true); } }); - mKeySafeSlingerButton.setOnClickListener(new View.OnClickListener() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + vKeyNfcButton.setVisibility(View.VISIBLE); + vKeyNfcButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mNfcHelper.invokeNfcBeam(); + } + }); + } else { + vKeyNfcButton.setVisibility(View.GONE); + } + + vKeySafeSlingerButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startSafeSlinger(mDataUri); } }); - mKeyUploadButton.setOnClickListener(new View.OnClickListener() { + vKeyUploadButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { uploadToKeyserver(); @@ -163,6 +174,11 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements return root; } + private void exportToFile() { + new ExportHelper(getActivity()).showExportKeysDialog( + mMasterKeyId, Constants.Path.APP_DIR_FILE, false); + } + private void startSafeSlinger(Uri dataUri) { long keyId = 0; try { @@ -177,97 +193,87 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements startActivityForResult(safeSlingerIntent, 0); } - private void share(Uri dataUri, ProviderHelper providerHelper, boolean fingerprintOnly, - boolean toClipboard) { + private void share(boolean fingerprintOnly, boolean toClipboard) { + Activity activity = getActivity(); + if (activity == null || mFingerprint == null) { + return; + } + ProviderHelper providerHelper = new ProviderHelper(activity); + try { String content; - byte[] fingerprintData = (byte[]) providerHelper.getGenericData( - KeyRings.buildUnifiedKeyRingUri(dataUri), - Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); if (fingerprintOnly) { - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintData); + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(mFingerprint); if (!toClipboard) { content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; } else { content = fingerprint; } } else { - Uri uri = KeychainContract.KeyRingData.buildPublicKeyRingUri(dataUri); - // get public keyring as ascii armored string - content = providerHelper.getKeyRingAsArmoredString(uri); + content = providerHelper.getKeyRingAsArmoredString( + KeychainContract.KeyRingData.buildPublicKeyRingUri(mDataUri)); } if (toClipboard) { - ClipboardReflection.copyToClipboard(getActivity(), content); - String message; - if (fingerprintOnly) { - message = getResources().getString(R.string.fingerprint_copied_to_clipboard); - } else { - message = getResources().getString(R.string.key_copied_to_clipboard); - } - Notify.create(getActivity(), message, Notify.Style.OK).show(); - } else { - // Android will fail with android.os.TransactionTooLargeException if key is too big - // see http://www.lonestarprod.com/?p=34 - if (content.length() >= 86389) { - Notify.create(getActivity(), R.string.key_too_big_for_sharing, - Notify.Style.ERROR).show(); + ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipMan == null) { + Notify.create(activity, R.string.error_clipboard_copy, Style.ERROR); return; } - // let user choose application - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, content); - sendIntent.setType("text/plain"); + ClipData clip = ClipData.newPlainText(Constants.CLIPBOARD_LABEL, content); + clipMan.setPrimaryClip(clip); - String title; - if (fingerprintOnly) { - title = getResources().getString(R.string.title_share_fingerprint_with); - } else { - title = getResources().getString(R.string.title_share_key); - } - Intent shareChooser = Intent.createChooser(sendIntent, title); - - // Bluetooth Share will convert text/plain sent via EXTRA_TEXT to HTML - // Add replacement extra to send a text/plain file instead. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - try { - String primaryUserId = UncachedKeyRing.decodeFromData(content.getBytes()). - getPublicKey().getPrimaryUserIdWithFallback(); - - TemporaryStorageProvider shareFileProv = new TemporaryStorageProvider(); - Uri contentUri = TemporaryStorageProvider.createFile(getActivity(), - primaryUserId + Constants.FILE_EXTENSION_ASC); - - BufferedWriter contentWriter = new BufferedWriter(new OutputStreamWriter( - new ParcelFileDescriptor.AutoCloseOutputStream( - shareFileProv.openFile(contentUri, "w")))); - contentWriter.write(content); - contentWriter.close(); - - // create replacement extras inside try{}: - // if file creation fails, just don't add the replacements - Bundle replacements = new Bundle(); - shareChooser.putExtra(Intent.EXTRA_REPLACEMENT_EXTRAS, replacements); - - Bundle bluetoothExtra = new Bundle(sendIntent.getExtras()); - replacements.putBundle("com.android.bluetooth", bluetoothExtra); - - bluetoothExtra.putParcelable(Intent.EXTRA_STREAM, contentUri); - } catch (FileNotFoundException e) { - Log.e(Constants.TAG, "error creating temporary Bluetooth key share file!", e); - Notify.create(getActivity(), R.string.error_bluetooth_file, Notify.Style.ERROR).show(); - } - } + Notify.create(activity, fingerprintOnly ? R.string.fingerprint_copied_to_clipboard + : R.string.key_copied_to_clipboard, Notify.Style.OK).show(); + return; + } + + // Android will fail with android.os.TransactionTooLargeException if key is too big + // see http://www.lonestarprod.com/?p=34 + if (content.length() >= 86389) { + Notify.create(activity, R.string.key_too_big_for_sharing, Notify.Style.ERROR).show(); + return; + } - startActivity(shareChooser); + // let user choose application + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, content); + sendIntent.setType("text/plain"); + + // Bluetooth Share will convert text/plain sent via EXTRA_TEXT to HTML + // Add replacement extra to send a text/plain file instead. + try { + TemporaryStorageProvider shareFileProv = new TemporaryStorageProvider(); + Uri contentUri = TemporaryStorageProvider.createFile(activity, + KeyFormattingUtils.convertFingerprintToHex(mFingerprint) + Constants.FILE_EXTENSION_ASC); + + BufferedWriter contentWriter = new BufferedWriter(new OutputStreamWriter( + new ParcelFileDescriptor.AutoCloseOutputStream( + shareFileProv.openFile(contentUri, "w")))); + contentWriter.write(content); + contentWriter.close(); + + sendIntent.putExtra(Intent.EXTRA_STREAM, contentUri); + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "error creating temporary Bluetooth key share file!", e); + // no need for a snackbar because one sharing option doesn't work + // Notify.create(getActivity(), R.string.error_temp_file, Notify.Style.ERROR).show(); } + + + String title = getString(fingerprintOnly + ? R.string.title_share_fingerprint_with : R.string.title_share_key); + Intent shareChooser = Intent.createChooser(sendIntent, title); + + startActivity(shareChooser); + } catch (PgpGeneralException | IOException e) { Log.e(Constants.TAG, "error processing key!", e); - Notify.create(getActivity(), R.string.error_key_processing, Notify.Style.ERROR).show(); + Notify.create(activity, R.string.error_key_processing, Notify.Style.ERROR).show(); } catch (ProviderHelper.NotFoundException e) { Log.e(Constants.TAG, "key not found!", e); - Notify.create(getActivity(), R.string.error_key_not_found, Notify.Style.ERROR).show(); + Notify.create(activity, R.string.error_key_not_found, Notify.Style.ERROR).show(); } } @@ -288,8 +294,8 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements } @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); if (dataUri == null) { @@ -304,8 +310,6 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements private void loadData(Uri dataUri) { mDataUri = dataUri; - Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); - // Prepare the loaders. Either re-connect with an existing ones, // or start new ones. getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); @@ -315,19 +319,10 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements } static final String[] UNIFIED_PROJECTION = new String[] { - KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.HAS_ANY_SECRET, - KeyRings.USER_ID, KeyRings.FINGERPRINT, - KeyRings.ALGORITHM, KeyRings.KEY_SIZE, KeyRings.CREATION, KeyRings.IS_EXPIRED, - + KeyRings._ID, KeyRings.FINGERPRINT }; - static final int INDEX_UNIFIED_MASTER_KEY_ID = 1; - static final int INDEX_UNIFIED_HAS_ANY_SECRET = 2; - static final int INDEX_UNIFIED_USER_ID = 3; - static final int INDEX_UNIFIED_FINGERPRINT = 4; - static final int INDEX_UNIFIED_ALGORITHM = 5; - static final int INDEX_UNIFIED_KEY_SIZE = 6; - static final int INDEX_UNIFIED_CREATION = 7; - static final int INDEX_UNIFIED_ID_EXPIRED = 8; + + static final int INDEX_UNIFIED_FINGERPRINT = 1; public Loader<Cursor> onCreateLoader(int id, Bundle args) { setContentShown(false); @@ -343,11 +338,8 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements } public void onLoadFinished(Loader<Cursor> loader, Cursor data) { - /* TODO better error handling? May cause problems when a key is deleted, - * because the notification triggers faster than the activity closes. - */ // Avoid NullPointerExceptions... - if (data.getCount() == 0) { + if (data == null || data.getCount() == 0) { return; } // Swap the new cursor in. (The framework will take care of closing the @@ -357,10 +349,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements if (data.moveToFirst()) { byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); - mFingerprint.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint)); - - loadQrCode(fingerprint); + setFingerprint(fingerprintBlob); break; } @@ -375,20 +364,26 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements * We need to make sure we are no longer using it. */ public void onLoaderReset(Loader<Cursor> loader) { + mFingerprint = null; } - /** - * Load QR Code asynchronously and with a fade in animation - * - * @param fingerprint - */ - private void loadQrCode(final String fingerprint) { + /** Load QR Code asynchronously and with a fade in animation */ + private void setFingerprint(byte[] fingerprintBlob) { + mFingerprint = fingerprintBlob; + mMasterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(fingerprintBlob); + + final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); + mFingerprintView.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint)); + AsyncTask<Void, Void, Bitmap> loadTask = new AsyncTask<Void, Void, Bitmap>() { protected Bitmap doInBackground(Void... unused) { - String qrCodeContent = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; + Uri uri = new Uri.Builder() + .scheme(Constants.FINGERPRINT_SCHEME) + .opaquePart(fingerprint) + .build(); // render with minimal size - return QrCodeUtils.getQRCodeBitmap(qrCodeContent, 0); + return QrCodeUtils.getQRCodeBitmap(uri, 0); } protected void onPostExecute(Bitmap qrCode) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java index c01a94286..89e5d741f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java @@ -378,7 +378,7 @@ public class ViewKeyFragment extends LoaderFragment implements * because the notification triggers faster than the activity closes. */ // Avoid NullPointerExceptions... - if (data.getCount() == 0) { + if (data == null || data.getCount() == 0) { return; } // Swap the new cursor in. (The framework will take care of closing the diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java index d5870d8c5..266633061 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java @@ -17,15 +17,12 @@ package org.sufficientlysecure.keychain.ui; -import android.app.ProgressDialog; import android.content.Intent; import android.database.Cursor; import android.graphics.Typeface; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; @@ -48,24 +45,26 @@ import com.textuality.keybase.lib.User; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.service.KeybaseVerificationParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ParcelableProxy; +import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; -public class ViewKeyTrustFragment extends LoaderFragment implements - LoaderManager.LoaderCallbacks<Cursor> { +public class ViewKeyKeybaseFragment extends LoaderFragment implements + LoaderManager.LoaderCallbacks<Cursor>, + CryptoOperationHelper.Callback<KeybaseVerificationParcel, KeybaseVerificationResult> { public static final String ARG_DATA_URI = "uri"; - private View mStartSearch; - private TextView mTrustReadout; private TextView mReportHeader; private TableLayout mProofListing; private LayoutInflater mInflater; @@ -77,15 +76,33 @@ public class ViewKeyTrustFragment extends LoaderFragment implements // for retrieving the key we’re working on private Uri mDataUri; + private Proof mProof; + + // for CryptoOperationHelper,Callback + private String mKeybaseProof; + private String mKeybaseFingerprint; + private CryptoOperationHelper<KeybaseVerificationParcel, KeybaseVerificationResult> + mKeybaseOpHelper; + + /** + * Creates new instance of this fragment + */ + public static ViewKeyKeybaseFragment newInstance(Uri dataUri) { + ViewKeyKeybaseFragment frag = new ViewKeyKeybaseFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_DATA_URI, dataUri); + + frag.setArguments(args); + + return frag; + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { View root = super.onCreateView(inflater, superContainer, savedInstanceState); View view = inflater.inflate(R.layout.view_key_adv_keybase_fragment, getContainer()); mInflater = inflater; - mTrustReadout = (TextView) view.findViewById(R.id.view_key_trust_readout); - mStartSearch = view.findViewById(R.id.view_key_trust_search_cloud); - mStartSearch.setEnabled(false); mReportHeader = (TextView) view.findViewById(R.id.view_key_trust_cloud_narrative); mProofListing = (TableLayout) view.findViewById(R.id.view_key_proof_list); mProofVerifyHeader = view.findViewById(R.id.view_key_proof_verify_header); @@ -148,58 +165,45 @@ public class ViewKeyTrustFragment extends LoaderFragment implements } boolean nothingSpecial = true; - StringBuilder message = new StringBuilder(); // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) if (data.moveToFirst()) { - if (data.getInt(INDEX_UNIFIED_HAS_ANY_SECRET) != 0) { - message.append(getString(R.string.key_trust_it_is_yours)).append("\n"); - nothingSpecial = false; - } else if (data.getInt(INDEX_VERIFIED) != 0) { - message.append(getString(R.string.key_trust_already_verified)).append("\n"); - nothingSpecial = false; - } + final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT); + final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp); - // If this key is revoked, don’t trust it! - if (data.getInt(INDEX_TRUST_IS_REVOKED) != 0) { - message.append(getString(R.string.key_trust_revoked)). - append(getString(R.string.key_trust_old_keys)); + startSearch(fingerprint); + } - nothingSpecial = false; - } else { - if (data.getInt(INDEX_TRUST_IS_EXPIRED) != 0) { + setContentShown(true); + } - // if expired, don’t trust it! - message.append(getString(R.string.key_trust_expired)). - append(getString(R.string.key_trust_old_keys)); + private void startSearch(final String fingerprint) { + final Preferences.ProxyPrefs proxyPrefs = + Preferences.getPreferences(getActivity()).getProxyPrefs(); - nothingSpecial = false; - } + OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() { + @Override + public void onOrbotStarted() { + new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint); } - if (nothingSpecial) { - message.append(getString(R.string.key_trust_maybe)); + @Override + public void onNeutralButton() { + new DescribeKey(ParcelableProxy.getForNoProxy()) + .execute(fingerprint); } - final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT); - final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp); - if (fingerprint != null) { + @Override + public void onCancel() { - mStartSearch.setEnabled(true); - mStartSearch.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mStartSearch.setEnabled(false); - new DescribeKey().execute(fingerprint); - } - }); } - } + }; - mTrustReadout.setText(message); - setContentShown(true); + if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) { + new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint); + } } /** @@ -223,6 +227,11 @@ public class ViewKeyTrustFragment extends LoaderFragment implements // look for evidence from keybase in the background, make tabular version of result // private class DescribeKey extends AsyncTask<String, Void, ResultPage> { + ParcelableProxy mParcelableProxy; + + public DescribeKey(ParcelableProxy parcelableProxy) { + mParcelableProxy = parcelableProxy; + } @Override protected ResultPage doInBackground(String... args) { @@ -231,7 +240,7 @@ public class ViewKeyTrustFragment extends LoaderFragment implements final ArrayList<CharSequence> proofList = new ArrayList<CharSequence>(); final Hashtable<Integer, ArrayList<Proof>> proofs = new Hashtable<Integer, ArrayList<Proof>>(); try { - User keybaseUser = User.findByFingerprint(fingerprint); + User keybaseUser = User.findByFingerprint(fingerprint, mParcelableProxy.getProxy()); for (Proof proof : keybaseUser.getProofs()) { Integer proofType = proof.getType(); appendIfOK(proofs, proofType, proof); @@ -243,8 +252,6 @@ public class ViewKeyTrustFragment extends LoaderFragment implements Proof[] proofsFor = proofs.get(proofType).toArray(x); if (proofsFor.length > 0) { SpannableStringBuilder ssb = new SpannableStringBuilder(); - ssb.append(getProofNarrative(proofType)).append(" "); - int i = 0; while (i < proofsFor.length - 1) { appendProofLinks(ssb, fingerprint, proofsFor[i]); @@ -252,7 +259,7 @@ public class ViewKeyTrustFragment extends LoaderFragment implements i++; } appendProofLinks(ssb, fingerprint, proofsFor[i]); - proofList.add(ssb); + proofList.add(formatSpannableString(ssb, getProofNarrative(proofType))); } } @@ -262,6 +269,20 @@ public class ViewKeyTrustFragment extends LoaderFragment implements return new ResultPage(getString(R.string.key_trust_results_prefix), proofList); } + private SpannableStringBuilder formatSpannableString(SpannableStringBuilder proofLinks, String proofType) { + //Formatting SpannableStringBuilder with String.format() causes the links to stop working. + //This method is to insert the links while reserving the links + + SpannableStringBuilder ssb = new SpannableStringBuilder(); + ssb.append(proofType); + if (proofType.contains("%s")) { + int i = proofType.indexOf("%s"); + ssb.replace(i, i + 2, proofLinks); + } else ssb.append(proofLinks); + + return ssb; + } + private SpannableStringBuilder appendProofLinks(SpannableStringBuilder ssb, final String fingerprint, final Proof proof) throws KeybaseException { int startAt = ssb.length(); String handle = proof.getHandle(); @@ -291,7 +312,6 @@ public class ViewKeyTrustFragment extends LoaderFragment implements result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence); } - mStartSearch.setVisibility(View.GONE); mReportHeader.setVisibility(View.VISIBLE); mProofListing.setVisibility(View.VISIBLE); mReportHeader.setText(result.mHeader); @@ -306,22 +326,35 @@ public class ViewKeyTrustFragment extends LoaderFragment implements text.setMovementMethod(LinkMovementMethod.getInstance()); mProofListing.addView(row); } - - // mSearchReport.loadDataWithBaseURL("file:///android_res/drawable/", s, "text/html", "UTF-8", null); } } private String getProofNarrative(int proofType) { int stringIndex; switch (proofType) { - case Proof.PROOF_TYPE_TWITTER: stringIndex = R.string.keybase_narrative_twitter; break; - case Proof.PROOF_TYPE_GITHUB: stringIndex = R.string.keybase_narrative_github; break; - case Proof.PROOF_TYPE_DNS: stringIndex = R.string.keybase_narrative_dns; break; - case Proof.PROOF_TYPE_WEB_SITE: stringIndex = R.string.keybase_narrative_web_site; break; - case Proof.PROOF_TYPE_HACKERNEWS: stringIndex = R.string.keybase_narrative_hackernews; break; - case Proof.PROOF_TYPE_COINBASE: stringIndex = R.string.keybase_narrative_coinbase; break; - case Proof.PROOF_TYPE_REDDIT: stringIndex = R.string.keybase_narrative_reddit; break; - default: stringIndex = R.string.keybase_narrative_unknown; + case Proof.PROOF_TYPE_TWITTER: + stringIndex = R.string.keybase_narrative_twitter; + break; + case Proof.PROOF_TYPE_GITHUB: + stringIndex = R.string.keybase_narrative_github; + break; + case Proof.PROOF_TYPE_DNS: + stringIndex = R.string.keybase_narrative_dns; + break; + case Proof.PROOF_TYPE_WEB_SITE: + stringIndex = R.string.keybase_narrative_web_site; + break; + case Proof.PROOF_TYPE_HACKERNEWS: + stringIndex = R.string.keybase_narrative_hackernews; + break; + case Proof.PROOF_TYPE_COINBASE: + stringIndex = R.string.keybase_narrative_coinbase; + break; + case Proof.PROOF_TYPE_REDDIT: + stringIndex = R.string.keybase_narrative_reddit; + break; + default: + stringIndex = R.string.keybase_narrative_unknown; } return getActivity().getString(stringIndex); } @@ -338,125 +371,150 @@ public class ViewKeyTrustFragment extends LoaderFragment implements // which proofs do we have working verifiers for? private boolean haveProofFor(int proofType) { switch (proofType) { - case Proof.PROOF_TYPE_TWITTER: return true; - case Proof.PROOF_TYPE_GITHUB: return true; - case Proof.PROOF_TYPE_DNS: return true; - case Proof.PROOF_TYPE_WEB_SITE: return true; - case Proof.PROOF_TYPE_HACKERNEWS: return true; - case Proof.PROOF_TYPE_COINBASE: return true; - case Proof.PROOF_TYPE_REDDIT: return true; - default: return false; + case Proof.PROOF_TYPE_TWITTER: + return true; + case Proof.PROOF_TYPE_GITHUB: + return true; + case Proof.PROOF_TYPE_DNS: + return true; + case Proof.PROOF_TYPE_WEB_SITE: + return true; + case Proof.PROOF_TYPE_HACKERNEWS: + return true; + case Proof.PROOF_TYPE_COINBASE: + return true; + case Proof.PROOF_TYPE_REDDIT: + return true; + default: + return false; } } private void verify(final Proof proof, final String fingerprint) { - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - Bundle data = new Bundle(); - intent.setAction(KeychainIntentService.ACTION_VERIFY_KEYBASE_PROOF); - data.putString(KeychainIntentService.KEYBASE_PROOF, proof.toString()); - data.putString(KeychainIntentService.KEYBASE_REQUIRED_FINGERPRINT, fingerprint); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + mProof = proof; + mKeybaseProof = proof.toString(); + mKeybaseFingerprint = fingerprint; mProofVerifyDetail.setVisibility(View.GONE); - // Create a new Messenger for the communication back after proof work is done - // - ServiceProgressHandler handler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_verifying_signature), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - Bundle returnData = message.getData(); - String msg = returnData.getString(ServiceProgressHandler.DATA_MESSAGE); - SpannableStringBuilder ssb = new SpannableStringBuilder(); - - if ((msg != null) && msg.equals("OK")) { - - //yay - String proofUrl = returnData.getString(ServiceProgressHandler.KEYBASE_PROOF_URL); - String presenceUrl = returnData.getString(ServiceProgressHandler.KEYBASE_PRESENCE_URL); - String presenceLabel = returnData.getString(ServiceProgressHandler.KEYBASE_PRESENCE_LABEL); - - String proofLabel; - switch (proof.getType()) { - case Proof.PROOF_TYPE_TWITTER: - proofLabel = getString(R.string.keybase_twitter_proof); - break; - case Proof.PROOF_TYPE_DNS: - proofLabel = getString(R.string.keybase_dns_proof); - break; - case Proof.PROOF_TYPE_WEB_SITE: - proofLabel = getString(R.string.keybase_web_site_proof); - break; - case Proof.PROOF_TYPE_GITHUB: - proofLabel = getString(R.string.keybase_github_proof); - break; - case Proof.PROOF_TYPE_REDDIT: - proofLabel = getString(R.string.keybase_reddit_proof); - break; - default: - proofLabel = getString(R.string.keybase_a_post); - break; - } + mKeybaseOpHelper = new CryptoOperationHelper<>(1, this, this, + R.string.progress_verifying_signature); + mKeybaseOpHelper.cryptoOperation(); + } - ssb.append(getString(R.string.keybase_proof_succeeded)); - StyleSpan bold = new StyleSpan(Typeface.BOLD); - ssb.setSpan(bold, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - ssb.append("\n\n"); - int length = ssb.length(); - ssb.append(proofLabel); - if (proofUrl != null) { - URLSpan postLink = new URLSpan(proofUrl); - ssb.setSpan(postLink, length, length + proofLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - if (Proof.PROOF_TYPE_DNS == proof.getType()) { - ssb.append(" ").append(getString(R.string.keybase_for_the_domain)).append(" "); - } else { - ssb.append(" ").append(getString(R.string.keybase_fetched_from)).append(" "); - } - length = ssb.length(); - URLSpan presenceLink = new URLSpan(presenceUrl); - ssb.append(presenceLabel); - ssb.setSpan(presenceLink, length, length + presenceLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - if (Proof.PROOF_TYPE_REDDIT == proof.getType()) { - ssb.append(", "). - append(getString(R.string.keybase_reddit_attribution)). - append(" “").append(proof.getHandle()).append("”, "); - } - ssb.append(" ").append(getString(R.string.keybase_contained_signature)); - } else { - // verification failed! - msg = returnData.getString(ServiceProgressHandler.DATA_ERROR); - ssb.append(getString(R.string.keybase_proof_failure)); - if (msg == null) { - msg = getString(R.string.keybase_unknown_proof_failure); - } - StyleSpan bold = new StyleSpan(Typeface.BOLD); - ssb.setSpan(bold, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - ssb.append("\n\n").append(msg); - } - mProofVerifyHeader.setVisibility(View.VISIBLE); - mProofVerifyDetail.setVisibility(View.VISIBLE); - mProofVerifyDetail.setMovementMethod(LinkMovementMethod.getInstance()); - mProofVerifyDetail.setText(ssb); - } - } - }; + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (mKeybaseOpHelper != null) { + mKeybaseOpHelper.handleActivityResult(requestCode, resultCode, data); + } + } - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(handler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + // CryptoOperationHelper.Callback methods + @Override + public KeybaseVerificationParcel createOperationInput() { + return new KeybaseVerificationParcel(mKeybaseProof, mKeybaseFingerprint); + } - // show progress dialog - handler.showProgressDialog(getActivity()); + @Override + public void onCryptoOperationSuccess(KeybaseVerificationResult result) { + + result.createNotify(getActivity()).show(); + + String proofUrl = result.mProofUrl; + String presenceUrl = result.mPresenceUrl; + String presenceLabel = result.mPresenceLabel; + + Proof proof = mProof; // TODO: should ideally be contained in result + + String proofLabel; + switch (proof.getType()) { + case Proof.PROOF_TYPE_TWITTER: + proofLabel = getString(R.string.keybase_twitter_proof); + break; + case Proof.PROOF_TYPE_DNS: + proofLabel = getString(R.string.keybase_dns_proof); + break; + case Proof.PROOF_TYPE_WEB_SITE: + proofLabel = getString(R.string.keybase_web_site_proof); + break; + case Proof.PROOF_TYPE_GITHUB: + proofLabel = getString(R.string.keybase_github_proof); + break; + case Proof.PROOF_TYPE_REDDIT: + proofLabel = getString(R.string.keybase_reddit_proof); + break; + default: + proofLabel = getString(R.string.keybase_a_post); + break; + } + + SpannableStringBuilder ssb = new SpannableStringBuilder(); + + ssb.append(getString(R.string.keybase_proof_succeeded)); + StyleSpan bold = new StyleSpan(Typeface.BOLD); + ssb.setSpan(bold, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append("\n\n"); + int length = ssb.length(); + ssb.append(proofLabel); + if (proofUrl != null) { + URLSpan postLink = new URLSpan(proofUrl); + ssb.setSpan(postLink, length, length + proofLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (Proof.PROOF_TYPE_DNS == proof.getType()) { + ssb.append(" ").append(getString(R.string.keybase_for_the_domain)).append(" "); + } else { + ssb.append(" ").append(getString(R.string.keybase_fetched_from)).append(" "); + } + length = ssb.length(); + URLSpan presenceLink = new URLSpan(presenceUrl); + ssb.append(presenceLabel); + ssb.setSpan(presenceLink, length, length + presenceLabel.length(), Spanned + .SPAN_EXCLUSIVE_EXCLUSIVE); + if (Proof.PROOF_TYPE_REDDIT == proof.getType()) { + ssb.append(", "). + append(getString(R.string.keybase_reddit_attribution)). + append(" “").append(proof.getHandle()).append("”, "); + } + ssb.append(" ").append(getString(R.string.keybase_contained_signature)); + + displaySpannableResult(ssb); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(KeybaseVerificationResult result) { + + result.createNotify(getActivity()).show(); + + SpannableStringBuilder ssb = new SpannableStringBuilder(); + + ssb.append(getString(R.string.keybase_proof_failure)); + String msg = getString(result.getLog().getLast().mType.mMsgId); + if (msg == null) { + msg = getString(R.string.keybase_unknown_proof_failure); + } + StyleSpan bold = new StyleSpan(Typeface.BOLD); + ssb.setSpan(bold, 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ssb.append("\n\n").append(msg); + + displaySpannableResult(ssb); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } - // start service with intent - getActivity().startService(intent); + private void displaySpannableResult(SpannableStringBuilder ssb) { + mProofVerifyHeader.setVisibility(View.VISIBLE); + mProofVerifyDetail.setVisibility(View.VISIBLE); + mProofVerifyDetail.setMovementMethod(LinkMovementMethod.getInstance()); + mProofVerifyDetail.setText(ssb); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyYubiKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyYubiKeyFragment.java index 812874456..f980f297b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyYubiKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyYubiKeyFragment.java @@ -18,15 +18,12 @@ package org.sufficientlysecure.keychain.ui; + import java.nio.ByteBuffer; import java.util.Arrays; -import android.content.Intent; import android.database.Cursor; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; -import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; @@ -39,31 +36,38 @@ import android.widget.TextView; import org.spongycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; +import org.sufficientlysecure.keychain.service.PromoteKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -public class ViewKeyYubiKeyFragment extends Fragment + +public class ViewKeyYubiKeyFragment + extends QueueingCryptoOperationFragment<PromoteKeyringParcel, PromoteKeyResult> implements LoaderCallbacks<Cursor> { + public static final String ARG_MASTER_KEY_ID = "master_key_id"; public static final String ARG_FINGERPRINT = "fingerprint"; public static final String ARG_USER_ID = "user_id"; public static final String ARG_CARD_AID = "aid"; + private byte[][] mFingerprints; private String mUserId; private byte[] mCardAid; private long mMasterKeyId; + private long[] mSubKeyIds; + private Button vButton; private TextView vStatus; - public static ViewKeyYubiKeyFragment newInstance(byte[] fingerprints, String userId, byte[] aid) { + public static ViewKeyYubiKeyFragment newInstance(long masterKeyId, + byte[] fingerprints, String userId, byte[] aid) { ViewKeyYubiKeyFragment frag = new ViewKeyYubiKeyFragment(); Bundle args = new Bundle(); + args.putLong(ARG_MASTER_KEY_ID, masterKeyId); args.putByteArray(ARG_FINGERPRINT, fingerprints); args.putString(ARG_USER_ID, userId); args.putByteArray(ARG_CARD_AID, aid); @@ -72,13 +76,17 @@ public class ViewKeyYubiKeyFragment extends Fragment return frag; } + public ViewKeyYubiKeyFragment() { + super(null); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle args = getArguments(); ByteBuffer buf = ByteBuffer.wrap(args.getByteArray(ARG_FINGERPRINT)); - mFingerprints = new byte[buf.remaining()/40][]; + mFingerprints = new byte[buf.remaining()/20][]; for (int i = 0; i < mFingerprints.length; i++) { mFingerprints[i] = new byte[20]; buf.get(mFingerprints[i]); @@ -86,7 +94,7 @@ public class ViewKeyYubiKeyFragment extends Fragment mUserId = args.getString(ARG_USER_ID); mCardAid = args.getByteArray(ARG_CARD_AID); - mMasterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mFingerprints[0]); + mMasterKeyId = args.getLong(ARG_MASTER_KEY_ID); getLoaderManager().initLoader(0, null, this); @@ -105,7 +113,7 @@ public class ViewKeyYubiKeyFragment extends Fragment if (!mUserId.isEmpty()) { vUserId.setText(getString(R.string.yubikey_key_holder, mUserId)); } else { - vUserId.setText(getString(R.string.yubikey_key_holder_unset)); + vUserId.setText(getString(R.string.yubikey_key_holder_not_set)); } vButton = (Button) view.findViewById(R.id.button_bind); @@ -122,44 +130,15 @@ public class ViewKeyYubiKeyFragment extends Fragment } public void promoteToSecretKey() { + long[] subKeyIds = new long[mFingerprints.length]; + for (int i = 0; i < subKeyIds.length; i++) { + subKeyIds[i] = KeyFormattingUtils.getKeyIdFromFingerprint(mFingerprints[i]); + } - ServiceProgressHandler saveHandler = new ServiceProgressHandler(getActivity()) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - - PromoteKeyResult result = - returnData.getParcelable(DecryptVerifyResult.EXTRA_RESULT); - - result.createNotify(getActivity()).show(); - } - - } - }; - - // Send all information needed to service to decrypt in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - - // fill values for this action - - intent.setAction(KeychainIntentService.ACTION_PROMOTE_KEYRING); - - Bundle data = new Bundle(); - data.putLong(KeychainIntentService.PROMOTE_MASTER_KEY_ID, mMasterKeyId); - data.putByteArray(KeychainIntentService.PROMOTE_CARD_AID, mCardAid); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // start service with intent - getActivity().startService(intent); + // mMasterKeyId and mCardAid are already set + mSubKeyIds = subKeyIds; + cryptoOperation(); } public static final String[] PROJECTION = new String[]{ @@ -169,8 +148,8 @@ public class ViewKeyYubiKeyFragment extends Fragment Keys.HAS_SECRET, Keys.FINGERPRINT }; - private static final int INDEX_KEY_ID = 1; - private static final int INDEX_RANK = 2; + // private static final int INDEX_KEY_ID = 1; + // private static final int INDEX_RANK = 2; private static final int INDEX_HAS_SECRET = 3; private static final int INDEX_FINGERPRINT = 4; @@ -216,7 +195,7 @@ public class ViewKeyYubiKeyFragment extends Fragment } - public Integer naiveIndexOf(byte[][] haystack, byte[] needle) { + static private Integer naiveIndexOf(byte[][] haystack, byte[] needle) { for (int i = 0; i < haystack.length; i++) { if (Arrays.equals(needle, haystack[i])) { return i; @@ -229,4 +208,15 @@ public class ViewKeyYubiKeyFragment extends Fragment public void onLoaderReset(Loader<Cursor> loader) { } + + @Override + public PromoteKeyringParcel createOperationInput() { + return new PromoteKeyringParcel(mMasterKeyId, mCardAid, mSubKeyIds); + } + + @Override + public void onQueuedOperationSuccess(PromoteKeyResult result) { + result.createNotify(getActivity()).show(); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java index db88de676..0be7e8f76 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java @@ -33,6 +33,7 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; +import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.Highlighter; @@ -92,8 +93,8 @@ public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> { } /** This method returns a list of all selected entries, with public keys sorted - * before secret keys, see ImportExportOperation for specifics. - * @see org.sufficientlysecure.keychain.operations.ImportExportOperation + * before secret keys, see ImportOperation for specifics. + * @see ImportOperation */ public ArrayList<ImportKeysListEntry> getSelectedEntries() { ArrayList<ImportKeysListEntry> result = new ArrayList<>(); @@ -176,9 +177,9 @@ public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> { } if (entry.isRevoked()) { - KeyFormattingUtils.setStatusImage(getContext(), holder.status, null, State.REVOKED, R.color.bg_gray); + KeyFormattingUtils.setStatusImage(getContext(), holder.status, null, State.REVOKED, R.color.key_flag_gray); } else if (entry.isExpired()) { - KeyFormattingUtils.setStatusImage(getContext(), holder.status, null, State.EXPIRED, R.color.bg_gray); + KeyFormattingUtils.setStatusImage(getContext(), holder.status, null, State.EXPIRED, R.color.key_flag_gray); } if (entry.isRevoked() || entry.isExpired()) { @@ -187,9 +188,9 @@ public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> { // no more space for algorithm display holder.algorithm.setVisibility(View.GONE); - holder.mainUserId.setTextColor(getContext().getResources().getColor(R.color.bg_gray)); - holder.mainUserIdRest.setTextColor(getContext().getResources().getColor(R.color.bg_gray)); - holder.keyId.setTextColor(getContext().getResources().getColor(R.color.bg_gray)); + holder.mainUserId.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); + holder.mainUserIdRest.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); + holder.keyId.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); } else { holder.status.setVisibility(View.GONE); holder.algorithm.setVisibility(View.VISIBLE); @@ -197,11 +198,11 @@ public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> { if (entry.isSecretKey()) { holder.mainUserId.setTextColor(Color.RED); } else { - holder.mainUserId.setTextColor(Color.BLACK); + holder.mainUserId.setTextColor(FormattingUtils.getColorFromAttr(mActivity, R.attr.colorText)); } - holder.mainUserIdRest.setTextColor(Color.BLACK); - holder.keyId.setTextColor(Color.BLACK); + holder.mainUserIdRest.setTextColor(FormattingUtils.getColorFromAttr(mActivity, R.attr.colorText)); + holder.keyId.setTextColor(FormattingUtils.getColorFromAttr(mActivity, R.attr.colorText)); } if (entry.getUserIds().size() == 1) { @@ -241,9 +242,9 @@ public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> { uidView.setPadding(0, 0, FormattingUtils.dpToPx(getContext(), 8), 0); if (entry.isRevoked() || entry.isExpired()) { - uidView.setTextColor(getContext().getResources().getColor(R.color.bg_gray)); + uidView.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); } else { - uidView.setTextColor(getContext().getResources().getColor(R.color.black)); + uidView.setTextColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorText)); } holder.userIdsList.addView(uidView); @@ -257,9 +258,9 @@ public class ImportKeysAdapter extends ArrayAdapter<ImportKeysListEntry> { emailView.setText(highlighter.highlight(email)); if (entry.isRevoked() || entry.isExpired()) { - emailView.setTextColor(getContext().getResources().getColor(R.color.bg_gray)); + emailView.setTextColor(getContext().getResources().getColor(R.color.key_flag_gray)); } else { - emailView.setTextColor(getContext().getResources().getColor(R.color.black)); + emailView.setTextColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorText)); } holder.userIdsList.addView(emailView); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListCloudLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListCloudLoader.java index 4781864dd..e77c92923 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListCloudLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListCloudLoader.java @@ -18,6 +18,7 @@ package org.sufficientlysecure.keychain.ui.adapter; import android.content.Context; +import android.support.annotation.Nullable; import android.support.v4.content.AsyncTaskLoader; import org.sufficientlysecure.keychain.Constants; @@ -26,8 +27,12 @@ import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; import org.sufficientlysecure.keychain.keyimport.Keyserver; import org.sufficientlysecure.keychain.operations.results.GetKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; import java.util.ArrayList; @@ -38,15 +43,27 @@ public class ImportKeysListCloudLoader Preferences.CloudSearchPrefs mCloudPrefs; String mServerQuery; + private ParcelableProxy mParcelableProxy; private ArrayList<ImportKeysListEntry> mEntryList = new ArrayList<>(); private AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> mEntryListWrapper; - public ImportKeysListCloudLoader(Context context, String serverQuery, Preferences.CloudSearchPrefs cloudPrefs) { + /** + * Searches a keyserver as specified in cloudPrefs, using an explicit proxy if passed + * + * @param serverQuery string to search on servers for. If is a fingerprint, + * will enforce fingerprint check + * @param cloudPrefs contains keyserver to search on, whether to search on the keyserver, + * and whether to search keybase.io + * @param parcelableProxy explicit proxy to use. If null, will retrieve from preferences + */ + public ImportKeysListCloudLoader(Context context, String serverQuery, Preferences.CloudSearchPrefs cloudPrefs, + @Nullable ParcelableProxy parcelableProxy) { super(context); mContext = context; mServerQuery = serverQuery; mCloudPrefs = cloudPrefs; + mParcelableProxy = parcelableProxy; } @Override @@ -95,9 +112,32 @@ public class ImportKeysListCloudLoader * Query keyserver */ private void queryServer(boolean enforceFingerprint) { + ParcelableProxy parcelableProxy; + + if (mParcelableProxy == null) { + // no explicit proxy specified, fetch from preferences + if (OrbotHelper.isOrbotInRequiredState(mContext)) { + parcelableProxy = Preferences.getPreferences(mContext).getProxyPrefs() + .parcelableProxy; + } else { + // user needs to enable/install orbot + mEntryList.clear(); + GetKeyResult pendingResult = new GetKeyResult(null, + RequiredInputParcel.createOrbotRequiredOperation(), + new CryptoInputParcel()); + mEntryListWrapper = new AsyncTaskResultWrapper<>(mEntryList, pendingResult); + return; + } + } else { + parcelableProxy = mParcelableProxy; + } + try { - ArrayList<ImportKeysListEntry> searchResult - = CloudSearch.search(mServerQuery, mCloudPrefs); + ArrayList<ImportKeysListEntry> searchResult = CloudSearch.search( + mServerQuery, + mCloudPrefs, + parcelableProxy.getProxy() + ); mEntryList.clear(); // add result to data @@ -109,7 +149,7 @@ public class ImportKeysListCloudLoader ImportKeysListEntry uniqueEntry = searchResult.get(0); /* * set fingerprint explicitly after query - * to enforce a check when the key is imported by KeychainIntentService + * to enforce a check when the key is imported by KeychainService */ uniqueEntry.setFingerprintHex(fingerprint); uniqueEntry.setSelected(true); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java index 3dbae09b6..56d273c7c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java @@ -18,6 +18,11 @@ package org.sufficientlysecure.keychain.ui.adapter; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; import java.util.Date; import android.content.Context; @@ -27,6 +32,7 @@ import android.support.v4.widget.CursorAdapter; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; @@ -38,6 +44,7 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.ui.util.Highlighter; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; @@ -45,6 +52,7 @@ public class KeyAdapter extends CursorAdapter { protected String mQuery; protected LayoutInflater mInflater; + protected Context mContext; // These are the rows that we will retrieve. public static final String[] PROJECTION = new String[]{ @@ -56,7 +64,6 @@ public class KeyAdapter extends CursorAdapter { KeyRings.VERIFIED, KeyRings.HAS_ANY_SECRET, KeyRings.HAS_DUPLICATE_USER_ID, - KeyRings.HAS_ENCRYPT, KeyRings.FINGERPRINT, KeyRings.CREATION, }; @@ -68,13 +75,13 @@ public class KeyAdapter extends CursorAdapter { public static final int INDEX_VERIFIED = 5; public static final int INDEX_HAS_ANY_SECRET = 6; public static final int INDEX_HAS_DUPLICATE_USER_ID = 7; - public static final int INDEX_HAS_ENCRYPT = 8; - public static final int INDEX_FINGERPRINT = 9; - public static final int INDEX_CREATION = 10; + public static final int INDEX_FINGERPRINT = 8; + public static final int INDEX_CREATION = 9; public KeyAdapter(Context context, Cursor c, int flags) { super(context, c, flags); + mContext = context; mInflater = LayoutInflater.from(context); } @@ -83,6 +90,9 @@ public class KeyAdapter extends CursorAdapter { } public static class KeyItemViewHolder { + public View mView; + public View mLayoutDummy; + public View mLayoutData; public Long mMasterKeyId; public TextView mMainUserId; public TextView mMainUserIdRest; @@ -91,7 +101,12 @@ public class KeyAdapter extends CursorAdapter { public View mSlinger; public ImageButton mSlingerButton; + public KeyItem mDisplayedItem; + public KeyItemViewHolder(View view) { + mView = view; + mLayoutData = view.findViewById(R.id.key_list_item_data); + mLayoutDummy = view.findViewById(R.id.key_list_item_dummy); mMainUserId = (TextView) view.findViewById(R.id.key_list_item_name); mMainUserIdRest = (TextView) view.findViewById(R.id.key_list_item_email); mStatus = (ImageView) view.findViewById(R.id.key_list_item_status_icon); @@ -100,11 +115,15 @@ public class KeyAdapter extends CursorAdapter { mCreationDate = (TextView) view.findViewById(R.id.key_list_item_creation); } - public void setData(Context context, Cursor cursor, Highlighter highlighter) { + public void setData(Context context, KeyItem item, Highlighter highlighter, boolean enabled) { + + mLayoutData.setVisibility(View.VISIBLE); + mLayoutDummy.setVisibility(View.GONE); + + mDisplayedItem = item; { // set name and stuff, common to both key types - String userId = cursor.getString(INDEX_USER_ID); - KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId); + KeyRing.UserId userIdSplit = item.mUserId; if (userIdSplit.name != null) { mMainUserId.setText(highlighter.highlight(userIdSplit.name)); } else { @@ -118,43 +137,42 @@ public class KeyAdapter extends CursorAdapter { } } + // sort of a hack: if this item isn't enabled, we make it clickable + // to intercept its click events. either way, no listener! + mView.setClickable(!enabled); + { // set edit button and status, specific by key type - long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); - boolean isSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0; - boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; - boolean isExpired = cursor.getInt(INDEX_IS_EXPIRED) != 0; - boolean isVerified = cursor.getInt(INDEX_VERIFIED) > 0; - boolean hasDuplicate = cursor.getInt(INDEX_HAS_DUPLICATE_USER_ID) != 0; + mMasterKeyId = item.mKeyId; - mMasterKeyId = masterKeyId; + int textColor; // Note: order is important! - if (isRevoked) { + if (item.mIsRevoked) { KeyFormattingUtils - .setStatusImage(context, mStatus, null, State.REVOKED, R.color.bg_gray); + .setStatusImage(context, mStatus, null, State.REVOKED, R.color.key_flag_gray); mStatus.setVisibility(View.VISIBLE); mSlinger.setVisibility(View.GONE); - mMainUserId.setTextColor(context.getResources().getColor(R.color.bg_gray)); - mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.bg_gray)); - } else if (isExpired) { - KeyFormattingUtils.setStatusImage(context, mStatus, null, State.EXPIRED, R.color.bg_gray); + textColor = context.getResources().getColor(R.color.key_flag_gray); + } else if (item.mIsExpired) { + KeyFormattingUtils.setStatusImage(context, mStatus, null, State.EXPIRED, R.color.key_flag_gray); mStatus.setVisibility(View.VISIBLE); mSlinger.setVisibility(View.GONE); - mMainUserId.setTextColor(context.getResources().getColor(R.color.bg_gray)); - mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.bg_gray)); - } else if (isSecret) { + textColor = context.getResources().getColor(R.color.key_flag_gray); + } else if (item.mIsSecret) { mStatus.setVisibility(View.GONE); if (mSlingerButton.hasOnClickListeners()) { + mSlingerButton.setColorFilter( + FormattingUtils.getColorFromAttr(context, R.attr.colorTertiaryText), + PorterDuff.Mode.SRC_IN); mSlinger.setVisibility(View.VISIBLE); } else { mSlinger.setVisibility(View.GONE); } - mMainUserId.setTextColor(context.getResources().getColor(R.color.black)); - mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.black)); + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); } else { // this is a public key - show if it's verified - if (isVerified) { + if (item.mIsVerified) { KeyFormattingUtils.setStatusImage(context, mStatus, State.VERIFIED); mStatus.setVisibility(View.VISIBLE); } else { @@ -162,19 +180,26 @@ public class KeyAdapter extends CursorAdapter { mStatus.setVisibility(View.VISIBLE); } mSlinger.setVisibility(View.GONE); - mMainUserId.setTextColor(context.getResources().getColor(R.color.black)); - mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.black)); + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); } - if (hasDuplicate) { + if (!enabled) { + textColor = context.getResources().getColor(R.color.key_flag_gray); + } + + mMainUserId.setTextColor(textColor); + mMainUserIdRest.setTextColor(textColor); + + if (item.mHasDuplicate) { String dateTime = DateUtils.formatDateTime(context, - cursor.getLong(INDEX_CREATION) * 1000, + item.mCreation.getTime(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); mCreationDate.setText(context.getString(R.string.label_key_created, - dateTime)); + dateTime)); + mCreationDate.setTextColor(textColor); mCreationDate.setVisibility(View.VISIBLE); } else { mCreationDate.setVisibility(View.GONE); @@ -184,6 +209,24 @@ public class KeyAdapter extends CursorAdapter { } + /** Shows the "you have no keys yet" dummy view, and sets an OnClickListener. */ + public void setDummy(OnClickListener listener) { + + // just reset everything to display the dummy layout + mLayoutDummy.setVisibility(View.VISIBLE); + mLayoutData.setVisibility(View.GONE); + mSlinger.setVisibility(View.GONE); + mStatus.setVisibility(View.GONE); + mView.setClickable(false); + + mLayoutDummy.setOnClickListener(listener); + + } + + } + + public boolean isEnabled(Cursor cursor) { + return true; } @Override @@ -191,16 +234,17 @@ public class KeyAdapter extends CursorAdapter { View view = mInflater.inflate(R.layout.key_list_item, parent, false); KeyItemViewHolder holder = new KeyItemViewHolder(view); view.setTag(holder); - holder.mSlingerButton.setColorFilter(context.getResources().getColor(R.color.tertiary_text_light), - PorterDuff.Mode.SRC_IN); return view; } @Override public void bindView(View view, Context context, Cursor cursor) { Highlighter highlighter = new Highlighter(context, mQuery); + KeyItem item = new KeyItem(cursor); + boolean isEnabled = isEnabled(cursor); + KeyItemViewHolder h = (KeyItemViewHolder) view.getTag(); - h.setData(context, cursor, highlighter); + h.setData(context, item, highlighter, isEnabled); } public boolean isSecretAvailable(int id) { @@ -230,14 +274,16 @@ public class KeyAdapter extends CursorAdapter { @Override public long getItemId(int position) { + Cursor cursor = getCursor(); // prevent a crash on rapid cursor changes - if (getCursor().isClosed()) { + if (cursor != null && getCursor().isClosed()) { return 0L; } return super.getItemId(position); } - public static class KeyItem { + // must be serializable for TokenCompleTextView state + public static class KeyItem implements Serializable { public final String mUserIdFull; public final KeyRing.UserId mUserId; @@ -245,6 +291,7 @@ public class KeyAdapter extends CursorAdapter { public final boolean mHasDuplicate; public final Date mCreation; public final String mFingerprint; + public final boolean mIsSecret, mIsRevoked, mIsExpired, mIsVerified; private KeyItem(Cursor cursor) { String userId = cursor.getString(INDEX_USER_ID); @@ -255,6 +302,10 @@ public class KeyAdapter extends CursorAdapter { mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000); mFingerprint = KeyFormattingUtils.convertFingerprintToHex( cursor.getBlob(INDEX_FINGERPRINT)); + mIsSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0; + mIsRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; + mIsExpired = cursor.getInt(INDEX_IS_EXPIRED) > 0; + mIsVerified = cursor.getInt(INDEX_VERIFIED) > 0; } public KeyItem(CanonicalizedPublicKeyRing ring) { @@ -267,6 +318,12 @@ public class KeyAdapter extends CursorAdapter { mCreation = key.getCreationTime(); mFingerprint = KeyFormattingUtils.convertFingerprintToHex( ring.getFingerprint()); + mIsRevoked = key.isRevoked(); + mIsExpired = key.isExpired(); + + // these two are actually "don't know"s + mIsSecret = false; + mIsVerified = false; } public String getReadableName() { @@ -279,4 +336,11 @@ public class KeyAdapter extends CursorAdapter { } + public static String[] getProjectionWith(String[] projection) { + List<String> list = new ArrayList<>(); + list.addAll(Arrays.asList(PROJECTION)); + list.addAll(Arrays.asList(projection)); + return list.toArray(new String[list.size()]); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java new file mode 100644 index 000000000..471a20411 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java @@ -0,0 +1,87 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import android.content.Context; +import android.database.Cursor; +import android.support.v7.internal.widget.AdapterViewCompat; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.CheckBox; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + + +public class KeySelectableAdapter extends KeyAdapter implements OnItemClickListener { + + HashSet<Long> mSelectedItems = new HashSet<>(); + + public KeySelectableAdapter(Context context, Cursor c, int flags, Set<Long> initialChecked) { + super(context, c, flags); + if (initialChecked != null) { + mSelectedItems.addAll(initialChecked); + } + } + + public static class KeySelectableItemViewHolder extends KeyItemViewHolder { + + public CheckBox mCheckbox; + + public KeySelectableItemViewHolder(View view) { + super(view); + mCheckbox = (CheckBox) view.findViewById(R.id.selected); + } + + public void setCheckedState(boolean checked) { + mCheckbox.setChecked(checked); + } + + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = mInflater.inflate(R.layout.key_list_selectable_item, parent, false); + KeySelectableItemViewHolder holder = new KeySelectableItemViewHolder(view); + view.setTag(holder); + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + super.bindView(view, context, cursor); + + KeySelectableItemViewHolder h = (KeySelectableItemViewHolder) view.getTag(); + h.setCheckedState(mSelectedItems.contains(h.mDisplayedItem.mKeyId)); + + } + + public void setCheckedStates(Set<Long> checked) { + mSelectedItems.clear(); + mSelectedItems.addAll(checked); + notifyDataSetChanged(); + } + + public Set<Long> getSelectedMasterKeyIds() { + return Collections.unmodifiableSet(mSelectedItems); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + Log.d(Constants.TAG, "clicked id: " + id); + long masterKeyId = getMasterKeyId(position); + if (mSelectedItems.contains(masterKeyId)) { + mSelectedItems.remove(masterKeyId); + } else { + mSelectedItems.add(masterKeyId); + } + notifyDataSetChanged(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java index 5218273a0..b91abf076 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java @@ -40,20 +40,19 @@ public class MultiUserIdsAdapter extends CursorAdapter { private LayoutInflater mInflater; private final ArrayList<Boolean> mCheckStates; - public MultiUserIdsAdapter(Context context, Cursor c, int flags) { + public MultiUserIdsAdapter(Context context, Cursor c, int flags, ArrayList<Boolean> preselectStates) { super(context, c, flags); mInflater = LayoutInflater.from(context); - mCheckStates = new ArrayList<>(); + mCheckStates = preselectStates == null ? new ArrayList<Boolean>() : preselectStates; } @Override public Cursor swapCursor(Cursor newCursor) { - mCheckStates.clear(); if (newCursor != null) { int count = newCursor.getCount(); mCheckStates.ensureCapacity(count); - // initialize to true (use case knowledge: we usually want to sign all uids) - for (int i = 0; i < count; i++) { + // initialize new fields to true (use case knowledge: we usually want to sign all uids) + for (int i = mCheckStates.size(); i < count; i++) { mCheckStates.add(true); } } @@ -151,6 +150,10 @@ public class MultiUserIdsAdapter extends CursorAdapter { } + public ArrayList<Boolean> getCheckStates() { + return mCheckStates; + } + public ArrayList<CertifyAction> getSelectedCertifyActions() { LongSparseArray<CertifyAction> actions = new LongSparseArray<>(); for (int i = 0; i < mCheckStates.size(); i++) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java index 6bbf41a88..f01f25200 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java @@ -149,11 +149,11 @@ abstract public class SelectKeyCursorAdapter extends CursorAdapter { boolean enabled; if (cursor.getInt(mIndexIsRevoked) != 0) { h.statusIcon.setVisibility(View.VISIBLE); - KeyFormattingUtils.setStatusImage(mContext, h.statusIcon, null, State.REVOKED, R.color.bg_gray); + KeyFormattingUtils.setStatusImage(mContext, h.statusIcon, null, State.REVOKED, R.color.key_flag_gray); enabled = false; } else if (cursor.getInt(mIndexIsExpiry) != 0) { h.statusIcon.setVisibility(View.VISIBLE); - KeyFormattingUtils.setStatusImage(mContext, h.statusIcon, null, State.EXPIRED, R.color.bg_gray); + KeyFormattingUtils.setStatusImage(mContext, h.statusIcon, null, State.EXPIRED, R.color.key_flag_gray); enabled = false; } else { h.statusIcon.setVisibility(View.GONE); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java index 096dea51f..24f5f04a1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java @@ -116,6 +116,21 @@ public class SubkeysAdapter extends CursorAdapter { } } + public int getAlgorithm(int position) { + mCursor.moveToPosition(position); + return mCursor.getInt(INDEX_ALGORITHM); + } + + public int getKeySize(int position) { + mCursor.moveToPosition(position); + return mCursor.getInt(INDEX_KEY_SIZE); + } + + public SecretKeyType getSecretKeyType(int position) { + mCursor.moveToPosition(position); + return SecretKeyType.fromNum(mCursor.getInt(INDEX_HAS_SECRET)); + } + @Override public Cursor swapCursor(Cursor newCursor) { hasAnySecret = false; @@ -164,13 +179,23 @@ public class SubkeysAdapter extends CursorAdapter { ? mSaveKeyringParcel.getSubkeyChange(keyId) : null; - if (change != null && change.mDummyStrip) { - algorithmStr.append(", "); - final SpannableString boldStripped = new SpannableString( - context.getString(R.string.key_stripped) - ); - boldStripped.setSpan(new StyleSpan(Typeface.BOLD), 0, boldStripped.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - algorithmStr.append(boldStripped); + if (change != null && (change.mDummyStrip || change.mMoveKeyToCard)) { + if (change.mDummyStrip) { + algorithmStr.append(", "); + final SpannableString boldStripped = new SpannableString( + context.getString(R.string.key_stripped) + ); + boldStripped.setSpan(new StyleSpan(Typeface.BOLD), 0, boldStripped.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + algorithmStr.append(boldStripped); + } + if (change.mMoveKeyToCard) { + algorithmStr.append(", "); + final SpannableString boldDivert = new SpannableString( + context.getString(R.string.key_divert) + ); + boldDivert.setSpan(new StyleSpan(Typeface.BOLD), 0, boldDivert.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + algorithmStr.append(boldDivert); + } } else { switch (SecretKeyType.fromNum(cursor.getInt(INDEX_HAS_SECRET))) { case GNU_DUMMY: @@ -259,27 +284,27 @@ public class SubkeysAdapter extends CursorAdapter { vStatus.setVisibility(View.VISIBLE); vCertifyIcon.setColorFilter( - mContext.getResources().getColor(R.color.bg_gray), + mContext.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); vSignIcon.setColorFilter( - mContext.getResources().getColor(R.color.bg_gray), + mContext.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); vEncryptIcon.setColorFilter( - mContext.getResources().getColor(R.color.bg_gray), + mContext.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); vAuthenticateIcon.setColorFilter( - mContext.getResources().getColor(R.color.bg_gray), + mContext.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); if (isRevoked) { vStatus.setImageResource(R.drawable.status_signature_revoked_cutout_24dp); vStatus.setColorFilter( - mContext.getResources().getColor(R.color.bg_gray), + mContext.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); } else if (isExpired) { vStatus.setImageResource(R.drawable.status_signature_expired_cutout_24dp); vStatus.setColorFilter( - mContext.getResources().getColor(R.color.bg_gray), + mContext.getResources().getColor(R.color.key_flag_gray), PorterDuff.Mode.SRC_IN); } } else { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java index c68c078ad..0f4312dad 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java @@ -128,7 +128,7 @@ public class UserIdsAdapter extends UserAttributesAdapter { if (isRevoked) { // set revocation icon (can this even be primary?) - KeyFormattingUtils.setStatusImage(mContext, vVerified, null, State.REVOKED, R.color.bg_gray); + KeyFormattingUtils.setStatusImage(mContext, vVerified, null, State.REVOKED, R.color.key_flag_gray); // disable revoked user ids vName.setEnabled(false); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java index 0e752881f..aa4e7d840 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java @@ -18,6 +18,7 @@ package org.sufficientlysecure.keychain.ui.base; import android.app.Activity; +import android.content.Intent; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; @@ -29,6 +30,8 @@ import android.view.ViewGroup; import android.widget.TextView; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; /** * Setups Toolbar @@ -36,15 +39,33 @@ import org.sufficientlysecure.keychain.R; public abstract class BaseActivity extends AppCompatActivity { protected Toolbar mToolbar; protected View mStatusBar; + protected ThemeChanger mThemeChanger; @Override protected void onCreate(Bundle savedInstanceState) { + initTheme(); super.onCreate(savedInstanceState); initLayout(); initToolbar(); } - protected abstract void initLayout(); + @Override + protected void onResume() { + super.onResume(); + KeyserverSyncAdapterService.cancelUpdates(this); + + if (mThemeChanger.changeTheme()) { + Intent intent = getIntent(); + finish(); + overridePendingTransition(0, 0); + startActivity(intent); + overridePendingTransition(0, 0); + } + } + + protected void initLayout() { + + } protected void initToolbar() { mToolbar = (Toolbar) findViewById(R.id.toolbar); @@ -55,6 +76,15 @@ public abstract class BaseActivity extends AppCompatActivity { mStatusBar = findViewById(R.id.status_bar); } + /** + * Override if you want a different theme! + */ + protected void initTheme() { + mThemeChanger = new ThemeChanger(this); + mThemeChanger.setThemes(R.style.Theme_Keychain_Light, R.style.Theme_Keychain_Dark); + mThemeChanger.changeTheme(); + } + protected void setActionBarIcon(int iconRes) { mToolbar.setNavigationIcon(iconRes); } @@ -85,9 +115,7 @@ public abstract class BaseActivity extends AppCompatActivity { mToolbar.setNavigationOnClickListener(cancelOnClickListener); } - /** - * Close button only - */ + /** Close button only */ protected void setFullScreenDialogClose(View.OnClickListener cancelOnClickListener, boolean white) { if (white) { setActionBarIcon(R.drawable.ic_close_white_24dp); @@ -102,6 +130,17 @@ public abstract class BaseActivity extends AppCompatActivity { setFullScreenDialogClose(cancelOnClickListener, true); } + /** Close button only, with finish-action and given return status, white. */ + protected void setFullScreenDialogClose(final int result, boolean white) { + setFullScreenDialogClose(new View.OnClickListener() { + @Override + public void onClick(View v) { + setResult(result); + finish(); + } + }, white); + } + /** * Inflate custom design with two buttons using drawables. * This does not conform to the Material Design Guidelines, but we deviate here as this is used diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java index 1d09b281f..972421abe 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java @@ -1,6 +1,8 @@ /* * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2013-2014 Signe Rüsch + * Copyright (C) 2013-2014 Philipp Jakubeit * * 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 @@ -19,7 +21,9 @@ package org.sufficientlysecure.keychain.ui.base; import java.io.IOException; +import java.math.BigInteger; import java.nio.ByteBuffer; +import java.security.interfaces.RSAPrivateCrtKey; import android.app.Activity; import android.app.PendingIntent; @@ -27,18 +31,25 @@ import android.content.Intent; import android.content.IntentFilter; import android.nfc.NfcAdapter; import android.nfc.Tag; +import android.nfc.TagLostException; import android.nfc.tech.IsoDep; +import android.os.AsyncTask; import android.os.Bundle; import android.widget.Toast; import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.util.Arrays; import org.spongycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.service.PassphraseCacheService.KeyNotFoundException; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.CreateKeyActivity; @@ -54,21 +65,135 @@ import org.sufficientlysecure.keychain.util.Preferences; public abstract class BaseNfcActivity extends BaseActivity { - public static final int REQUEST_CODE_PASSPHRASE = 1; + public static final int REQUEST_CODE_PIN = 1; + + public static final String EXTRA_TAG_HANDLING_ENABLED = "tag_handling_enabled"; protected Passphrase mPin; + protected Passphrase mAdminPin; protected boolean mPw1ValidForMultipleSignatures; protected boolean mPw1ValidatedForSignature; protected boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming? + protected boolean mPw3Validated; private NfcAdapter mNfcAdapter; private IsoDep mIsoDep; + private boolean mTagHandlingEnabled; private static final int TIMEOUT = 100000; + private byte[] mNfcFingerprints; + private String mNfcUserId; + private byte[] mNfcAid; + + /** + * Override to change UI before NFC handling (UI thread) + */ + protected void onNfcPreExecute() { + } + + /** + * Override to implement NFC operations (background thread) + */ + protected void doNfcInBackground() throws IOException { + mNfcFingerprints = nfcGetFingerprints(); + mNfcUserId = nfcGetUserId(); + mNfcAid = nfcGetAid(); + } + + /** + * Override to handle result of NFC operations (UI thread) + */ + protected void onNfcPostExecute() throws IOException { + + final long subKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints); + + try { + CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing( + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId)); + long masterKeyId = ring.getMasterKeyId(); + + Intent intent = new Intent(this, ViewKeyActivity.class); + intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, mNfcAid); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, mNfcUserId); + intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, mNfcFingerprints); + startActivity(intent); + } catch (PgpKeyNotFoundException e) { + Intent intent = new Intent(this, CreateKeyActivity.class); + intent.putExtra(CreateKeyActivity.EXTRA_NFC_AID, mNfcAid); + intent.putExtra(CreateKeyActivity.EXTRA_NFC_USER_ID, mNfcUserId); + intent.putExtra(CreateKeyActivity.EXTRA_NFC_FINGERPRINTS, mNfcFingerprints); + startActivity(intent); + } + } + + /** + * Override to use something different than Notify (UI thread) + */ + protected void onNfcError(String error) { + Notify.create(this, error, Style.WARN).show(); + } + + public void handleIntentInBackground(final Intent intent) { + // Actual NFC operations are executed in doInBackground to not block the UI thread + new AsyncTask<Void, Void, Exception>() { + @Override + protected void onPreExecute() { + super.onPreExecute(); + onNfcPreExecute(); + } + + @Override + protected Exception doInBackground(Void... params) { + try { + handleTagDiscoveredIntent(intent); + } catch (CardException e) { + return e; + } catch (IOException e) { + return e; + } + + return null; + } + + @Override + protected void onPostExecute(Exception exception) { + super.onPostExecute(exception); + + if (exception != null) { + handleNfcError(exception); + return; + } + + try { + onNfcPostExecute(); + } catch (IOException e) { + handleNfcError(e); + } + } + }.execute(); + } + + protected void pauseTagHandling() { + mTagHandlingEnabled = false; + } + + protected void resumeTagHandling() { + mTagHandlingEnabled = true; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Check whether we're recreating a previously destroyed instance + if (savedInstanceState != null) { + // Restore value of members from saved state + mTagHandlingEnabled = savedInstanceState.getBoolean(EXTRA_TAG_HANDLING_ENABLED); + } else { + mTagHandlingEnabled = true; + } + Intent intent = getIntent(); String action = intent.getAction(); if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) { @@ -77,25 +202,108 @@ public abstract class BaseNfcActivity extends BaseActivity { } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(EXTRA_TAG_HANDLING_ENABLED, mTagHandlingEnabled); + } + /** * This activity is started as a singleTop activity. * All new NFC Intents which are delivered to this activity are handled here */ @Override - public void onNewIntent(Intent intent) { - if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) { - try { - handleNdefDiscoveredIntent(intent); - } catch (IOException e) { - handleNfcError(e); - } + public void onNewIntent(final Intent intent) { + if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction()) + && mTagHandlingEnabled) { + handleIntentInBackground(intent); } } - public void handleNfcError(IOException e) { - + private void handleNfcError(Exception e) { Log.e(Constants.TAG, "nfc error", e); - Notify.create(this, getString(R.string.error_nfc, e.getMessage()), Style.WARN).show(); + + if (e instanceof TagLostException) { + onNfcError(getString(R.string.error_nfc_tag_lost)); + return; + } + + short status; + if (e instanceof CardException) { + status = ((CardException) e).getResponseCode(); + } else { + status = -1; + } + // When entering a PIN, a status of 63CX indicates X attempts remaining. + if ((status & (short)0xFFF0) == 0x63C0) { + int tries = status & 0x000F; + onNfcError(getResources().getQuantityString(R.plurals.error_pin, tries, tries)); + return; + } + + // Otherwise, all status codes are fixed values. + switch (status) { + // These errors should not occur in everyday use; if they are returned, it means we + // made a mistake sending data to the card, or the card is misbehaving. + case 0x6A80: { + onNfcError(getString(R.string.error_nfc_bad_data)); + break; + } + case 0x6883: { + onNfcError(getString(R.string.error_nfc_chaining_error)); + break; + } + case 0x6B00: { + onNfcError(getString(R.string.error_nfc_header, "P1/P2")); + break; + } + case 0x6D00: { + onNfcError(getString(R.string.error_nfc_header, "INS")); + break; + } + case 0x6E00: { + onNfcError(getString(R.string.error_nfc_header, "CLA")); + break; + } + // These error conditions are more likely to be experienced by an end user. + case 0x6285: { + onNfcError(getString(R.string.error_nfc_terminated)); + break; + } + case 0x6700: { + onNfcError(getString(R.string.error_nfc_wrong_length)); + break; + } + case 0x6982: { + onNfcError(getString(R.string.error_nfc_security_not_satisfied)); + break; + } + case 0x6983: { + onNfcError(getString(R.string.error_nfc_authentication_blocked)); + break; + } + case 0x6985: { + onNfcError(getString(R.string.error_nfc_conditions_not_satisfied)); + break; + } + // 6A88 is "Not Found" in the spec, but Yubikey also returns 6A83 for this in some cases. + case 0x6A88: + case 0x6A83: { + onNfcError(getString(R.string.error_nfc_data_not_found)); + break; + } + // 6F00 is a JavaCard proprietary status code, SW_UNKNOWN, and usually represents an + // unhandled exception on the smart card. + case 0x6F00: { + onNfcError(getString(R.string.error_nfc_unknown)); + break; + } + default: { + onNfcError(getString(R.string.error_nfc, e.getMessage())); + break; + } + } } @@ -129,16 +337,29 @@ public abstract class BaseNfcActivity extends BaseActivity { protected void obtainYubiKeyPin(RequiredInputParcel requiredInput) { + // shortcut if we only use the default yubikey pin Preferences prefs = Preferences.getPreferences(this); if (prefs.useDefaultYubiKeyPin()) { mPin = new Passphrase("123456"); return; } - Intent intent = new Intent(this, PassphraseDialogActivity.class); - intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT, - RequiredInputParcel.createRequiredPassphrase(requiredInput)); - startActivityForResult(intent, REQUEST_CODE_PASSPHRASE); + try { + Passphrase phrase = PassphraseCacheService.getCachedPassphrase(this, + requiredInput.getMasterKeyId(), requiredInput.getSubKeyId()); + if (phrase != null) { + mPin = phrase; + return; + } + + Intent intent = new Intent(this, PassphraseDialogActivity.class); + intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT, + RequiredInputParcel.createRequiredPassphrase(requiredInput)); + startActivityForResult(intent, REQUEST_CODE_PIN); + } catch (KeyNotFoundException e) { + throw new AssertionError( + "tried to find passphrase for non-existing key. this is a programming error!"); + } } @@ -149,7 +370,7 @@ public abstract class BaseNfcActivity extends BaseActivity { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { - case REQUEST_CODE_PASSPHRASE: + case REQUEST_CODE_PIN: { if (resultCode != Activity.RESULT_OK) { setResult(resultCode); finish(); @@ -158,6 +379,7 @@ public abstract class BaseNfcActivity extends BaseActivity { CryptoInputParcel input = data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT); mPin = input.getPassphrase(); break; + } default: super.onActivityResult(requestCode, resultCode, data); @@ -169,7 +391,7 @@ public abstract class BaseNfcActivity extends BaseActivity { * This method is called by onNewIntent above upon discovery of an NFC tag. * It handles initialization and login to the application, subsequently * calls either nfcCalculateSignature() or nfcDecryptSessionKey(), then - * finishes the activity with an appropiate result. + * finishes the activity with an appropriate result. * * On general communication, see also * http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-a.aspx @@ -178,7 +400,7 @@ public abstract class BaseNfcActivity extends BaseActivity { * on ISO SmartCard Systems specification. * */ - protected void handleNdefDiscoveredIntent(Intent intent) throws IOException { + protected void handleTagDiscoveredIntent(Intent intent) throws IOException { Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); @@ -200,52 +422,23 @@ public abstract class BaseNfcActivity extends BaseActivity { + "06" // Lc (number of bytes) + "D27600012401" // Data (6 bytes) + "00"; // Le - if ( ! nfcCommunicate(opening).endsWith(accepted)) { // activate connection - throw new IOException("Initialization failed!"); + String response = nfcCommunicate(opening); // activate connection + if ( ! response.endsWith(accepted) ) { + throw new CardException("Initialization failed!", parseCardStatus(response)); } byte[] pwStatusBytes = nfcGetPwStatusBytes(); mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1); mPw1ValidatedForSignature = false; mPw1ValidatedForDecrypt = false; + mPw3Validated = false; - onNfcPerform(); - - mIsoDep.close(); - mIsoDep = null; + doNfcInBackground(); } - protected void onNfcPerform() throws IOException { - - final byte[] nfcFingerprints = nfcGetFingerprints(); - final String nfcUserId = nfcGetUserId(); - final byte[] nfcAid = nfcGetAid(); - - final long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(nfcFingerprints); - - try { - CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(masterKeyId); - ring.getMasterKeyId(); - - Intent intent = new Intent( - BaseNfcActivity.this, ViewKeyActivity.class); - intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_AID, nfcAid); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_USER_ID, nfcUserId); - intent.putExtra(ViewKeyActivity.EXTRA_NFC_FINGERPRINTS, nfcFingerprints); - startActivity(intent); - finish(); - } catch (PgpKeyNotFoundException e) { - Intent intent = new Intent( - BaseNfcActivity.this, CreateKeyActivity.class); - intent.putExtra(CreateKeyActivity.EXTRA_NFC_AID, nfcAid); - intent.putExtra(CreateKeyActivity.EXTRA_NFC_USER_ID, nfcUserId); - intent.putExtra(CreateKeyActivity.EXTRA_NFC_FINGERPRINTS, nfcFingerprints); - startActivity(intent); - finish(); - } - + public boolean isNfcConnected() { + return mIsoDep.isConnected(); } /** Return the key id from application specific data stored on tag, or null @@ -415,7 +608,7 @@ public abstract class BaseNfcActivity extends BaseActivity { } if ( ! "9000".equals(status)) { - throw new IOException("Bad NFC response code: " + status); + throw new CardException("Bad NFC response code: " + status, parseCardStatus(response)); } // Make sure the signature we received is actually the expected number of bytes long! @@ -460,13 +653,21 @@ public abstract class BaseNfcActivity extends BaseActivity { return Hex.decode(decryptedSessionKey); } - /** Verifies the user's PW1 with the appropriate mode. + /** Verifies the user's PW1 or PW3 with the appropriate mode. * - * @param mode This is 0x81 for signing, 0x82 for everything else + * @param mode For PW1, this is 0x81 for signing, 0x82 for everything else. + * For PW3 (Admin PIN), mode is 0x83. */ public void nfcVerifyPIN(int mode) throws IOException { - if (mPin != null) { - byte[] pin = new String(mPin.getCharArray()).getBytes(); + if (mPin != null || mode == 0x83) { + + byte[] pin; + if (mode == 0x83) { + pin = mAdminPin.toStringUnsafe().getBytes(); + } else { + pin = mPin.toStringUnsafe().getBytes(); + } + // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time. // See specification, page 51 String accepted = "9000"; @@ -479,19 +680,231 @@ public abstract class BaseNfcActivity extends BaseActivity { + String.format("%02x", mode) // P2 + String.format("%02x", pin.length) // Lc + Hex.toHexString(pin); - if (!nfcCommunicate(login).equals(accepted)) { // login + String response = nfcCommunicate(login); // login + if (!response.equals(accepted)) { handlePinError(); - throw new IOException("Bad PIN!"); + throw new CardException("Bad PIN!", parseCardStatus(response)); } if (mode == 0x81) { mPw1ValidatedForSignature = true; } else if (mode == 0x82) { mPw1ValidatedForDecrypt = true; + } else if (mode == 0x83) { + mPw3Validated = true; } } } + /** Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for + * conformance to the card's requirements for key length. + * + * @param pw For PW1, this is 0x81. For PW3 (Admin PIN), mode is 0x83. + * @param newPin The new PW1 or PW3. + */ + public void nfcModifyPIN(int pw, byte[] newPin) throws IOException { + final int MAX_PW1_LENGTH_INDEX = 1; + final int MAX_PW3_LENGTH_INDEX = 3; + + byte[] pwStatusBytes = nfcGetPwStatusBytes(); + + if (pw == 0x81) { + if (newPin.length < 6 || newPin.length > pwStatusBytes[MAX_PW1_LENGTH_INDEX]) { + throw new IOException("Invalid PIN length"); + } + } else if (pw == 0x83) { + if (newPin.length < 8 || newPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) { + throw new IOException("Invalid PIN length"); + } + } else { + throw new IOException("Invalid PW index for modify PIN operation"); + } + + byte[] pin; + if (pw == 0x83) { + pin = mAdminPin.toStringUnsafe().getBytes(); + } else { + pin = mPin.toStringUnsafe().getBytes(); + } + + // Command APDU for CHANGE REFERENCE DATA command (page 32) + String changeReferenceDataApdu = "00" // CLA + + "24" // INS + + "00" // P1 + + String.format("%02x", pw) // P2 + + String.format("%02x", pin.length + newPin.length) // Lc + + getHex(pin) + + getHex(newPin); + String response = nfcCommunicate(changeReferenceDataApdu); // change PIN + if (!response.equals("9000")) { + handlePinError(); + throw new CardException("Failed to change PIN", parseCardStatus(response)); + } + } + + /** + * Stores a data object on the card. Automatically validates the proper PIN for the operation. + * Supported for all data objects < 255 bytes in length. Only the cardholder certificate + * (0x7F21) can exceed this length. + * + * @param dataObject The data object to be stored. + * @param data The data to store in the object + */ + public void nfcPutData(int dataObject, byte[] data) throws IOException { + if (data.length > 254) { + throw new IOException("Cannot PUT DATA with length > 254"); + } + if (dataObject == 0x0101 || dataObject == 0x0103) { + if (!mPw1ValidatedForDecrypt) { + nfcVerifyPIN(0x82); // (Verify PW1 for non-signing operations) + } + } else if (!mPw3Validated) { + nfcVerifyPIN(0x83); // (Verify PW3) + } + + String putDataApdu = "00" // CLA + + "DA" // INS + + String.format("%02x", (dataObject & 0xFF00) >> 8) // P1 + + String.format("%02x", dataObject & 0xFF) // P2 + + String.format("%02x", data.length) // Lc + + getHex(data); + + String response = nfcCommunicate(putDataApdu); // put data + if (!response.equals("9000")) { + throw new CardException("Failed to put data.", parseCardStatus(response)); + } + } + + /** + * Puts a key on the card in the given slot. + * + * @param slot The slot on the card where the key should be stored: + * 0xB6: Signature Key + * 0xB8: Decipherment Key + * 0xA4: Authentication Key + */ + public void nfcPutKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase) + throws IOException { + if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) { + throw new IOException("Invalid key slot"); + } + + RSAPrivateCrtKey crtSecretKey; + try { + secretKey.unlock(passphrase); + crtSecretKey = secretKey.getCrtSecretKey(); + } catch (PgpGeneralException e) { + throw new IOException(e.getMessage()); + } + + // Shouldn't happen; the UI should block the user from getting an incompatible key this far. + if (crtSecretKey.getModulus().bitLength() > 2048) { + throw new IOException("Key too large to export to smart card."); + } + + // Should happen only rarely; all GnuPG keys since 2006 use public exponent 65537. + if (!crtSecretKey.getPublicExponent().equals(new BigInteger("65537"))) { + throw new IOException("Invalid public exponent for smart card key."); + } + + if (!mPw3Validated) { + nfcVerifyPIN(0x83); // (Verify PW3 with mode 83) + } + + byte[] header= Hex.decode( + "4D82" + "03A2" // Extended header list 4D82, length of 930 bytes. (page 23) + + String.format("%02x", slot) + "00" // CRT to indicate targeted key, no length + + "7F48" + "15" // Private key template 0x7F48, length 21 (decimal, 0x15 hex) + + "9103" // Public modulus, length 3 + + "928180" // Prime P, length 128 + + "938180" // Prime Q, length 128 + + "948180" // Coefficient (1/q mod p), length 128 + + "958180" // Prime exponent P (d mod (p - 1)), length 128 + + "968180" // Prime exponent Q (d mod (1 - 1)), length 128 + + "97820100" // Modulus, length 256, last item in private key template + + "5F48" + "820383");// DO 5F48; 899 bytes of concatenated key data will follow + byte[] dataToSend = new byte[934]; + byte[] currentKeyObject; + int offset = 0; + + System.arraycopy(header, 0, dataToSend, offset, header.length); + offset += header.length; + currentKeyObject = crtSecretKey.getPublicExponent().toByteArray(); + System.arraycopy(currentKeyObject, 0, dataToSend, offset, 3); + offset += 3; + // NOTE: For a 2048-bit key, these lengths are fixed. However, bigint includes a leading 0 + // in the array to represent sign, so we take care to set the offset to 1 if necessary. + currentKeyObject = crtSecretKey.getPrimeP().toByteArray(); + System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); + Arrays.fill(currentKeyObject, (byte)0); + offset += 128; + currentKeyObject = crtSecretKey.getPrimeQ().toByteArray(); + System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); + Arrays.fill(currentKeyObject, (byte)0); + offset += 128; + currentKeyObject = crtSecretKey.getCrtCoefficient().toByteArray(); + System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); + Arrays.fill(currentKeyObject, (byte)0); + offset += 128; + currentKeyObject = crtSecretKey.getPrimeExponentP().toByteArray(); + System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); + Arrays.fill(currentKeyObject, (byte)0); + offset += 128; + currentKeyObject = crtSecretKey.getPrimeExponentQ().toByteArray(); + System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128); + Arrays.fill(currentKeyObject, (byte)0); + offset += 128; + currentKeyObject = crtSecretKey.getModulus().toByteArray(); + System.arraycopy(currentKeyObject, currentKeyObject.length - 256, dataToSend, offset, 256); + + String putKeyCommand = "10DB3FFF"; + String lastPutKeyCommand = "00DB3FFF"; + + // Now we're ready to communicate with the card. + offset = 0; + String response; + while(offset < dataToSend.length) { + int dataRemaining = dataToSend.length - offset; + if (dataRemaining > 254) { + response = nfcCommunicate( + putKeyCommand + "FE" + Hex.toHexString(dataToSend, offset, 254) + ); + offset += 254; + } else { + int length = dataToSend.length - offset; + response = nfcCommunicate( + lastPutKeyCommand + String.format("%02x", length) + + Hex.toHexString(dataToSend, offset, length)); + offset += length; + } + + if (!response.endsWith("9000")) { + throw new CardException("Key export to card failed", parseCardStatus(response)); + } + } + + // Clear array with secret data before we return. + Arrays.fill(dataToSend, (byte) 0); + } + + /** + * Parses out the status word from a JavaCard response string. + * + * @param response A hex string with the response from the card + * @return A short indicating the SW1/SW2, or 0 if a status could not be determined. + */ + short parseCardStatus(String response) { + if (response.length() < 4) { + return 0; // invalid input + } + + try { + return Short.parseShort(response.substring(response.length() - 4), 16); + } catch (NumberFormatException e) { + return 0; + } + } + /** * Prints a message to the screen * @@ -561,4 +974,18 @@ public abstract class BaseNfcActivity extends BaseActivity { return new String(Hex.encode(raw)); } + public class CardException extends IOException { + private short mResponseCode; + + public CardException(String detailMessage, short responseCode) { + super(detailMessage); + mResponseCode = responseCode; + } + + public short getResponseCode() { + return mResponseCode; + } + + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CachingCryptoOperationFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CachingCryptoOperationFragment.java new file mode 100644 index 000000000..321af0df5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CachingCryptoOperationFragment.java @@ -0,0 +1,59 @@ +package org.sufficientlysecure.keychain.ui.base; + + +import android.os.Bundle; +import android.os.Parcelable; + +import org.sufficientlysecure.keychain.operations.results.OperationResult; + + +public abstract class CachingCryptoOperationFragment <T extends Parcelable, S extends OperationResult> + extends QueueingCryptoOperationFragment<T, S> { + + public static final String ARG_CACHED_ACTIONS = "cached_actions"; + + private T mCachedActionsParcel; + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putParcelable(ARG_CACHED_ACTIONS, mCachedActionsParcel); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + mCachedActionsParcel = savedInstanceState.getParcelable(ARG_CACHED_ACTIONS); + } + } + + @Override + public void onQueuedOperationSuccess(S result) { + mCachedActionsParcel = null; + } + + @Override + public void onQueuedOperationError(S result) { + super.onQueuedOperationError(result); + mCachedActionsParcel = null; + } + + @Override + public abstract T createOperationInput(); + + protected T getCachedActionsParcel() { + return mCachedActionsParcel; + } + + protected void cacheActionsParcel(T cachedActionsParcel) { + mCachedActionsParcel = cachedActionsParcel; + } + + public void onCryptoOperationCancelled() { + mCachedActionsParcel = null; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java index 232a39f86..de90d48fd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java @@ -18,114 +18,116 @@ package org.sufficientlysecure.keychain.ui.base; -import android.app.Activity; + +import android.content.Context; import android.content.Intent; -import android.os.Bundle; -import android.os.Message; +import android.os.Parcelable; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; +import android.view.View; +import android.view.inputmethod.InputMethodManager; -import org.sufficientlysecure.keychain.operations.results.InputPendingResult; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; +import org.sufficientlysecure.keychain.service.KeychainService; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; -import org.sufficientlysecure.keychain.ui.NfcOperationActivity; -import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity; -/** - * All fragments executing crypto operations need to extend this class. +/** This is a base class for fragments which implement a cryptoOperation. + * + * Subclasses of this class can call the cryptoOperation method to execute an + * operation in KeychainService which takes a parcelable of type T as its input + * and returns an OperationResult of type S as a result. + * + * The input (of type T) is not given directly to the cryptoOperation method, + * but must be provided by the overriden createOperationInput method to be + * available upon request during execution of the cryptoOperation. + * + * After running cryptoOperation, one of the onCryptoOperation*() methods will + * be called, depending on the success status of the operation. The subclass + * must override at least onCryptoOperationSuccess to proceed after a + * successful operation. + * + * @see KeychainService + * */ -public abstract class CryptoOperationFragment extends Fragment { - - public static final int REQUEST_CODE_PASSPHRASE = 0x00008001; - public static final int REQUEST_CODE_NFC = 0x00008002; - - private void initiateInputActivity(RequiredInputParcel requiredInput) { - - switch (requiredInput.mType) { - case NFC_DECRYPT: - case NFC_SIGN: { - Intent intent = new Intent(getActivity(), NfcOperationActivity.class); - intent.putExtra(NfcOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput); - startActivityForResult(intent, REQUEST_CODE_NFC); - return; - } - - case PASSPHRASE: - case PASSPHRASE_SYMMETRIC: { - Intent intent = new Intent(getActivity(), PassphraseDialogActivity.class); - intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT, requiredInput); - startActivityForResult(intent, REQUEST_CODE_PASSPHRASE); - return; - } - } +public abstract class CryptoOperationFragment<T extends Parcelable, S extends OperationResult> + extends Fragment implements CryptoOperationHelper.Callback<T, S> { - throw new RuntimeException("Unhandled pending result!"); + final private CryptoOperationHelper<T, S> mOperationHelper; + + public CryptoOperationFragment() { + mOperationHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_processing); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_CANCELED) { - onCryptoOperationCancelled(); - return; - } + public CryptoOperationFragment(Integer initialProgressMsg) { + mOperationHelper = new CryptoOperationHelper<>(1, this, this, initialProgressMsg); + } - switch (requestCode) { - case REQUEST_CODE_PASSPHRASE: { - if (resultCode == Activity.RESULT_OK && data != null) { - CryptoInputParcel cryptoInput = - data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT); - cryptoOperation(cryptoInput); - return; - } - break; - } - - case REQUEST_CODE_NFC: { - if (resultCode == Activity.RESULT_OK && data != null) { - CryptoInputParcel cryptoInput = - data.getParcelableExtra(NfcOperationActivity.RESULT_DATA); - cryptoOperation(cryptoInput); - return; - } - break; - } - - default: { - super.onActivityResult(requestCode, resultCode, data); - } - } + public CryptoOperationFragment(int id, Integer initialProgressMsg) { + mOperationHelper = new CryptoOperationHelper<>(id, this, this, initialProgressMsg); } - public boolean handlePendingMessage(Message message) { + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + mOperationHelper.handleActivityResult(requestCode, resultCode, data); + } - if (message.arg1 == ServiceProgressHandler.MessageStatus.OKAY.ordinal()) { - Bundle data = message.getData(); + /** Starts execution of the cryptographic operation. + * + * During this process, the createOperationInput() method will be called, + * this input will be handed to KeychainService, where it is executed in + * the appropriate *Operation class. If the result is a PendingInputResult, + * it is handled accordingly. Otherwise, it is returned in one of the + * onCryptoOperation* callbacks. + */ + protected void cryptoOperation() { + mOperationHelper.cryptoOperation(); + } - OperationResult result = data.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null || !(result instanceof InputPendingResult)) { - return false; - } + protected void cryptoOperation(CryptoInputParcel cryptoInput) { + mOperationHelper.cryptoOperation(cryptoInput); + } - InputPendingResult pendingResult = (InputPendingResult) result; - if (pendingResult.isPending()) { - RequiredInputParcel requiredInput = pendingResult.getRequiredInputParcel(); - initiateInputActivity(requiredInput); - return true; - } - } + @Override @Nullable + /** Creates input for the crypto operation. Called internally after the + * crypto operation is started by a call to cryptoOperation(). Silently + * cancels operation if this method returns null. */ + public abstract T createOperationInput(); + /** Returns false, indicating that we did not handle progress ourselves. */ + public boolean onCryptoSetProgress(String msg, int progress, int max) { return false; } - protected void cryptoOperation() { - cryptoOperation(new CryptoInputParcel()); + public void setProgressMessageResource(int id) { + mOperationHelper.setProgressMessageResource(id); + } + + @Override + /** Called when the cryptoOperation() was successful. No default behavior + * here, this should always be implemented by a subclass! */ + abstract public void onCryptoOperationSuccess(S result); + + @Override + abstract public void onCryptoOperationError(S result); + + @Override + public void onCryptoOperationCancelled() { } - protected abstract void cryptoOperation(CryptoInputParcel cryptoInput); + public void hideKeyboard() { + if (getActivity() == null) { + return; + } + InputMethodManager inputManager = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + + // check if no view has focus + View v = getActivity().getCurrentFocus(); + if (v == null) + return; - protected void onCryptoOperationCancelled() { - // Nothing to do here, in most cases + inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java new file mode 100644 index 000000000..6d7bf4cd0 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@gmail.com> + * + * 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.ui.base; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcelable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.operations.results.InputPendingResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.service.KeychainService; +import org.sufficientlysecure.keychain.service.ServiceProgressHandler; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import org.sufficientlysecure.keychain.ui.NfcOperationActivity; +import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity; +import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity; +import org.sufficientlysecure.keychain.ui.RetryUploadDialogActivity; +import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.util.Log; + +/** + * Designed to be integrated into activities or fragments used for CryptoOperations. + * Encapsulates the execution of a crypto operation and handling of input pending cases.s + * + * @param <T> The type of input parcel sent to the operation + * @param <S> The type of result returned by the operation + */ +public class CryptoOperationHelper<T extends Parcelable, S extends OperationResult> { + + public interface Callback<T extends Parcelable, S extends OperationResult> { + T createOperationInput(); + + void onCryptoOperationSuccess(S result); + + void onCryptoOperationCancelled(); + + void onCryptoOperationError(S result); + + boolean onCryptoSetProgress(String msg, int progress, int max); + } + + // request codes from CryptoOperationHelper are created essentially + // a static property, used to identify requestCodes meant for this + // particular helper. a request code looks as follows: + // (id << 9) + (1<<8) + REQUEST_CODE_X + // that is, starting from LSB, there are 8 bits request code, 1 + // fixed bit set, then 7 bit helper-id code. the first two + // summands are stored in the mHelperId for easy operation. + private final int mHelperId; + // bitmask for helperId is everything except the least 8 bits + public static final int HELPER_ID_BITMASK = ~0xff; + + public static final int REQUEST_CODE_PASSPHRASE = 1; + public static final int REQUEST_CODE_NFC = 2; + public static final int REQUEST_CODE_ENABLE_ORBOT = 3; + public static final int REQUEST_CODE_RETRY_UPLOAD = 4; + + private Integer mProgressMessageResource; + + private FragmentActivity mActivity; + private Fragment mFragment; + private Callback<T, S> mCallback; + + private boolean mUseFragment; // short hand for mActivity == null + + /** + * If OperationHelper is being integrated into an activity + */ + public CryptoOperationHelper(int id, FragmentActivity activity, Callback<T, S> callback, + Integer progressMessageString) { + mHelperId = (id << 9) + (1<<8); + mActivity = activity; + mUseFragment = false; + mCallback = callback; + mProgressMessageResource = progressMessageString; + } + + /** + * if OperationHelper is being integrated into a fragment + */ + public CryptoOperationHelper(int id, Fragment fragment, Callback<T, S> callback, Integer progressMessageString) { + mHelperId = (id << 9) + (1<<8); + mFragment = fragment; + mUseFragment = true; + mProgressMessageResource = progressMessageString; + mCallback = callback; + } + + public void setProgressMessageResource(int id) { + mProgressMessageResource = id; + } + + private void initiateInputActivity(RequiredInputParcel requiredInput, + CryptoInputParcel cryptoInputParcel) { + + Activity activity = mUseFragment ? mFragment.getActivity() : mActivity; + + switch (requiredInput.mType) { + // always use CryptoOperationHelper.startActivityForResult! + case NFC_MOVE_KEY_TO_CARD: + case NFC_DECRYPT: + case NFC_SIGN: { + Intent intent = new Intent(activity, NfcOperationActivity.class); + intent.putExtra(NfcOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput); + intent.putExtra(NfcOperationActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel); + startActivityForResult(intent, REQUEST_CODE_NFC); + return; + } + + case PASSPHRASE: + case PASSPHRASE_SYMMETRIC: { + Intent intent = new Intent(activity, PassphraseDialogActivity.class); + intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT, requiredInput); + intent.putExtra(PassphraseDialogActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel); + startActivityForResult(intent, REQUEST_CODE_PASSPHRASE); + return; + } + + case ENABLE_ORBOT: { + Intent intent = new Intent(activity, OrbotRequiredDialogActivity.class); + intent.putExtra(OrbotRequiredDialogActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel); + startActivityForResult(intent, REQUEST_CODE_ENABLE_ORBOT); + return; + } + + case UPLOAD_FAIL_RETRY: { + Intent intent = new Intent(activity, RetryUploadDialogActivity.class); + intent.putExtra(RetryUploadDialogActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel); + startActivityForResult(intent, REQUEST_CODE_RETRY_UPLOAD); + return; + } + + default: { + throw new RuntimeException("Unhandled pending result!"); + } + } + } + + protected void startActivityForResult(Intent intent, int requestCode) { + if (mUseFragment) { + mFragment.startActivityForResult(intent, mHelperId + requestCode); + } else { + mActivity.startActivityForResult(intent, mHelperId + requestCode); + } + } + + /** + * Attempts the result of an activity started by this helper. Returns true if requestCode is + * recognized, false otherwise. + * @return true if requestCode was recognized, false otherwise + */ + public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { + Log.d(Constants.TAG, "received activity result in OperationHelper"); + + if ((requestCode & HELPER_ID_BITMASK) != mHelperId) { + // this wasn't meant for us to handle + return false; + } + Log.d(Constants.TAG, "handling activity result in OperationHelper"); + // filter out mHelperId from requestCode + requestCode ^= mHelperId; + + if (resultCode == Activity.RESULT_CANCELED) { + mCallback.onCryptoOperationCancelled(); + return true; + } + + switch (requestCode) { + case REQUEST_CODE_PASSPHRASE: { + if (resultCode == Activity.RESULT_OK && data != null) { + CryptoInputParcel cryptoInput = + data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT); + cryptoOperation(cryptoInput); + } + break; + } + + case REQUEST_CODE_NFC: { + if (resultCode == Activity.RESULT_OK && data != null) { + CryptoInputParcel cryptoInput = + data.getParcelableExtra(NfcOperationActivity.RESULT_CRYPTO_INPUT); + cryptoOperation(cryptoInput); + } + break; + } + + case REQUEST_CODE_ENABLE_ORBOT: { + if (resultCode == Activity.RESULT_OK && data != null) { + CryptoInputParcel cryptoInput = + data.getParcelableExtra( + OrbotRequiredDialogActivity.RESULT_CRYPTO_INPUT); + cryptoOperation(cryptoInput); + } + break; + } + + case REQUEST_CODE_RETRY_UPLOAD: { + if (resultCode == Activity.RESULT_OK) { + CryptoInputParcel cryptoInput = + data.getParcelableExtra( + RetryUploadDialogActivity.RESULT_CRYPTO_INPUT); + cryptoOperation(cryptoInput); + } + break; + } + } + + return true; + } + + protected void dismissProgress() { + FragmentManager fragmentManager = + mUseFragment ? mFragment.getFragmentManager() : + mActivity.getSupportFragmentManager(); + + if (fragmentManager == null) { // the fragment holding us has died + // fragmentManager was null when used with DialogFragments. (they close on click?) + return; + } + + ProgressDialogFragment progressDialogFragment = + (ProgressDialogFragment) fragmentManager.findFragmentByTag( + ServiceProgressHandler.TAG_PROGRESS_DIALOG); + + if (progressDialogFragment == null) { + return; + } + + progressDialogFragment.dismissAllowingStateLoss(); + + } + + public void cryptoOperation(final CryptoInputParcel cryptoInput) { + + FragmentActivity activity = mUseFragment ? mFragment.getActivity() : mActivity; + + T operationInput = mCallback.createOperationInput(); + if (operationInput == null) { + return; + } + + // Send all information needed to service to edit key in other thread + Intent intent = new Intent(activity, KeychainService.class); + + intent.putExtra(KeychainService.EXTRA_OPERATION_INPUT, operationInput); + intent.putExtra(KeychainService.EXTRA_CRYPTO_INPUT, cryptoInput); + + ServiceProgressHandler saveHandler = new ServiceProgressHandler(activity) { + @Override + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentServiceHandler first + super.handleMessage(message); + + if (message.arg1 == MessageStatus.OKAY.ordinal()) { + + // get returned data bundle + Bundle returnData = message.getData(); + if (returnData == null) { + return; + } + + final OperationResult result = + returnData.getParcelable(OperationResult.EXTRA_RESULT); + + onHandleResult(result); + } + } + + @Override + protected void onSetProgress(String msg, int progress, int max) { + // allow handling of progress in fragment, or delegate upwards + if (!mCallback.onCryptoSetProgress(msg, progress, max)) { + super.onSetProgress(msg, progress, max); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainService.EXTRA_MESSENGER, messenger); + + if (mProgressMessageResource != null) { + saveHandler.showProgressDialog( + activity.getString(mProgressMessageResource), + ProgressDialog.STYLE_HORIZONTAL, false); + } + + activity.startService(intent); + } + + public void cryptoOperation() { + cryptoOperation(new CryptoInputParcel()); + } + + public void onHandleResult(OperationResult result) { + Log.d(Constants.TAG, "Handling result in OperationHelper success: " + result.success()); + + if (result instanceof InputPendingResult) { + InputPendingResult pendingResult = (InputPendingResult) result; + if (pendingResult.isPending()) { + RequiredInputParcel requiredInput = pendingResult.getRequiredInputParcel(); + initiateInputActivity(requiredInput, pendingResult.mCryptoInputParcel); + return; + } + } + + dismissProgress(); + + try { + if (result.success()) { + // noinspection unchecked, because type erasure :( + mCallback.onCryptoOperationSuccess((S) result); + } else { + // noinspection unchecked, because type erasure :( + mCallback.onCryptoOperationError((S) result); + } + } catch (ClassCastException e) { + throw new AssertionError("bad return class (" + + result.getClass().getSimpleName() + "), this is a programming error!"); + } + } +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/QueueingCryptoOperationFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/QueueingCryptoOperationFragment.java new file mode 100644 index 000000000..65e0ce941 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/QueueingCryptoOperationFragment.java @@ -0,0 +1,93 @@ +package org.sufficientlysecure.keychain.ui.base; + + +import android.os.Bundle; +import android.os.Parcelable; + +import org.sufficientlysecure.keychain.operations.results.OperationResult; + + +/** CryptoOperationFragment which calls crypto operation results only while + * attached to Activity. + * + * This subclass of CryptoOperationFragment substitutes the onCryptoOperation* + * methods for onQueuedOperation* ones, which are ensured to be called while + * the fragment is attached to an Activity, possibly delaying the call until + * the Fragment is re-attached. + * + * TODO merge this functionality into CryptoOperationFragment? + * + * @see CryptoOperationFragment + */ +public abstract class QueueingCryptoOperationFragment<T extends Parcelable, S extends OperationResult> + extends CryptoOperationFragment<T,S> { + + public static final String ARG_QUEUED_RESULT = "queued_result"; + private S mQueuedResult; + + public QueueingCryptoOperationFragment() { + super(); + } + + public QueueingCryptoOperationFragment(Integer initialProgressMsg) { + super(initialProgressMsg); + } + + @Override + public void onViewStateRestored(Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + + if (mQueuedResult != null) { + try { + if (mQueuedResult.success()) { + onQueuedOperationSuccess(mQueuedResult); + } else { + onQueuedOperationError(mQueuedResult); + } + } finally { + mQueuedResult = null; + } + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putParcelable(ARG_QUEUED_RESULT, mQueuedResult); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + mQueuedResult = savedInstanceState.getParcelable(ARG_QUEUED_RESULT); + } + } + + public abstract void onQueuedOperationSuccess(S result); + + public void onQueuedOperationError(S result) { + hideKeyboard(); + result.createNotify(getActivity()).show(); + } + + @Override + final public void onCryptoOperationSuccess(S result) { + if (getActivity() == null) { + mQueuedResult = result; + return; + } + onQueuedOperationSuccess(result); + } + + @Override + final public void onCryptoOperationError(S result) { + if (getActivity() == null) { + mQueuedResult = result; + return; + } + onQueuedOperationError(result); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddKeyserverDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java index cbef5950f..47bc7dfda 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddKeyserverDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java @@ -17,8 +17,14 @@ package org.sufficientlysecure.keychain.ui.dialog; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; + import android.app.Activity; -import android.app.AlertDialog; +import android.support.v7.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; @@ -29,8 +35,8 @@ import android.os.Bundle; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; +import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; -import android.test.suitebuilder.TestSuiteBuilder; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -42,23 +48,24 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; + import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.TlsHelper; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; - -import javax.net.ssl.HttpsURLConnection; +import java.net.Proxy; -public class AddKeyserverDialogFragment extends DialogFragment implements OnEditorActionListener { - private static final String ARG_MESSENGER = "messenger"; - private static final String ARG_TITLE = "title"; +public class AddEditKeyserverDialogFragment extends DialogFragment implements OnEditorActionListener { + private static final String ARG_MESSENGER = "arg_messenger"; + private static final String ARG_ACTION = "arg_dialog_action"; + private static final String ARG_POSITION = "arg_position"; + private static final String ARG_KEYSERVER = "arg_keyserver"; public static final int MESSAGE_OKAY = 1; public static final int MESSAGE_VERIFICATION_FAILED = 2; @@ -66,50 +73,54 @@ public class AddKeyserverDialogFragment extends DialogFragment implements OnEdit public static final String MESSAGE_KEYSERVER = "new_keyserver"; public static final String MESSAGE_VERIFIED = "verified"; public static final String MESSAGE_FAILURE_REASON = "failure_reason"; + public static final String MESSAGE_KEYSERVER_DELETED = "keyserver_deleted"; + public static final String MESSAGE_DIALOG_ACTION = "message_dialog_action"; + public static final String MESSAGE_EDIT_POSITION = "keyserver_edited_position"; private Messenger mMessenger; + private DialogAction mDialogAction; + private int mPosition; + private EditText mKeyserverEditText; private CheckBox mVerifyKeyserverCheckBox; - public static enum FailureReason { + public enum DialogAction { + ADD, + EDIT + } + + public enum FailureReason { INVALID_URL, CONNECTION_FAILED } - ; - - /** - * Creates new instance of this dialog fragment - * - * @param title title of dialog - * @param messenger to communicate back after setting the passphrase - * @return - */ - public static AddKeyserverDialogFragment newInstance(Messenger messenger, int title) { - AddKeyserverDialogFragment frag = new AddKeyserverDialogFragment(); + public static AddEditKeyserverDialogFragment newInstance(Messenger messenger, + DialogAction action, + String keyserver, + int position) { + AddEditKeyserverDialogFragment frag = new AddEditKeyserverDialogFragment(); Bundle args = new Bundle(); - args.putInt(ARG_TITLE, title); args.putParcelable(ARG_MESSENGER, messenger); + args.putSerializable(ARG_ACTION, action); + args.putString(ARG_KEYSERVER, keyserver); + args.putInt(ARG_POSITION, position); frag.setArguments(args); return frag; } - /** - * Creates dialog - */ + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Activity activity = getActivity(); - int title = getArguments().getInt(ARG_TITLE); mMessenger = getArguments().getParcelable(ARG_MESSENGER); + mDialogAction = (DialogAction) getArguments().getSerializable(ARG_ACTION); + mPosition = getArguments().getInt(ARG_POSITION); CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(activity); - alert.setTitle(title); - LayoutInflater inflater = activity.getLayoutInflater(); View view = inflater.inflate(R.layout.add_keyserver_dialog, null); alert.setView(view); @@ -117,14 +128,26 @@ public class AddKeyserverDialogFragment extends DialogFragment implements OnEdit mKeyserverEditText = (EditText) view.findViewById(R.id.keyserver_url_edit_text); mVerifyKeyserverCheckBox = (CheckBox) view.findViewById(R.id.verify_keyserver_checkbox); - // we don't want dialog to be dismissed on click, thereby requiring the hack seen below - // and in onStart + switch (mDialogAction) { + case ADD: { + alert.setTitle(R.string.add_keyserver_dialog_title); + break; + } + case EDIT: { + alert.setTitle(R.string.edit_keyserver_dialog_title); + mKeyserverEditText.setText(getArguments().getString(ARG_KEYSERVER)); + break; + } + } + + // we don't want dialog to be dismissed on click for keyserver addition or edit, + // thereby requiring the hack seen below and in onStart alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { // we need to have an empty listener to prevent errors on some devices as mentioned // at http://stackoverflow.com/q/13746412/3000919 - // actual listener set in onStart + // actual listener set in onStart for adding keyservers or editing them } }); @@ -136,6 +159,23 @@ public class AddKeyserverDialogFragment extends DialogFragment implements OnEdit } }); + switch (mDialogAction) { + case EDIT: { + alert.setNeutralButton(R.string.label_keyserver_dialog_delete, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + deleteKeyserver(mPosition); + } + }); + break; + } + case ADD: { + // do nothing + break; + } + } + // Hack to open keyboard. // This is the only method that I found to work across all Android versions // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/ @@ -172,25 +212,63 @@ public class AddKeyserverDialogFragment extends DialogFragment implements OnEdit positiveButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - String keyserverUrl = mKeyserverEditText.getText().toString(); + // behaviour same for edit and add + final String keyserverUrl = mKeyserverEditText.getText().toString(); if (mVerifyKeyserverCheckBox.isChecked()) { - verifyConnection(keyserverUrl); + final Preferences.ProxyPrefs proxyPrefs = Preferences.getPreferences(getActivity()) + .getProxyPrefs(); + OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() { + @Override + public void onOrbotStarted() { + verifyConnection(keyserverUrl, + proxyPrefs.parcelableProxy.getProxy()); + } + + @Override + public void onNeutralButton() { + verifyConnection(keyserverUrl, null); + } + + @Override + public void onCancel() { + // do nothing + } + }; + + if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) { + verifyConnection(keyserverUrl, proxyPrefs.parcelableProxy.getProxy()); + } } else { dismiss(); // return unverified keyserver back to activity - addKeyserver(keyserverUrl, false); + keyserverEdited(keyserverUrl, false); } } }); } } - public void addKeyserver(String keyserver, boolean verified) { + public void keyserverEdited(String keyserver, boolean verified) { dismiss(); Bundle data = new Bundle(); + data.putSerializable(MESSAGE_DIALOG_ACTION, mDialogAction); data.putString(MESSAGE_KEYSERVER, keyserver); data.putBoolean(MESSAGE_VERIFIED, verified); + if (mDialogAction == DialogAction.EDIT) { + data.putInt(MESSAGE_EDIT_POSITION, mPosition); + } + + sendMessageToHandler(MESSAGE_OKAY, data); + } + + public void deleteKeyserver(int position) { + dismiss(); + Bundle data = new Bundle(); + data.putSerializable(MESSAGE_DIALOG_ACTION, DialogAction.EDIT); + data.putInt(MESSAGE_EDIT_POSITION, position); + data.putBoolean(MESSAGE_KEYSERVER_DELETED, true); + sendMessageToHandler(MESSAGE_OKAY, data); } @@ -201,7 +279,7 @@ public class AddKeyserverDialogFragment extends DialogFragment implements OnEdit sendMessageToHandler(MESSAGE_VERIFICATION_FAILED, data); } - public void verifyConnection(String keyserver) { + public void verifyConnection(String keyserver, final Proxy proxy) { new AsyncTask<String, Void, FailureReason>() { ProgressDialog mProgressDialog; @@ -225,19 +303,24 @@ public class AddKeyserverDialogFragment extends DialogFragment implements OnEdit String scheme = keyserverUri.getScheme(); String schemeSpecificPart = keyserverUri.getSchemeSpecificPart(); String fragment = keyserverUri.getFragment(); - if (scheme == null) throw new MalformedURLException(); - if (scheme.equalsIgnoreCase("hkps")) scheme = "https"; - else if (scheme.equalsIgnoreCase("hkp")) scheme = "http"; + if (scheme == null) { + throw new MalformedURLException(); + } + if ("hkps".equalsIgnoreCase(scheme)) { + scheme = "https"; + } else if ("hkp".equalsIgnoreCase(scheme)) { + scheme = "http"; + } URI newKeyserver = new URI(scheme, schemeSpecificPart, fragment); Log.d("Converted URL", newKeyserver.toString()); - TlsHelper.openConnection(newKeyserver.toURL()).getInputStream(); + + OkHttpClient client = HkpKeyserver.getClient(newKeyserver.toURL(), proxy); + TlsHelper.pinCertificateIfNecessary(client, newKeyserver.toURL()); + client.newCall(new Request.Builder().url(newKeyserver.toURL()).build()).execute(); } catch (TlsHelper.TlsHelperException e) { reason = FailureReason.CONNECTION_FAILED; - } catch (MalformedURLException e) { - Log.w(Constants.TAG, "Invalid keyserver URL entered by user."); - reason = FailureReason.INVALID_URL; - } catch (URISyntaxException e) { + } catch (MalformedURLException | URISyntaxException e) { Log.w(Constants.TAG, "Invalid keyserver URL entered by user."); reason = FailureReason.INVALID_URL; } catch (IOException e) { @@ -251,7 +334,7 @@ public class AddKeyserverDialogFragment extends DialogFragment implements OnEdit protected void onPostExecute(FailureReason failureReason) { mProgressDialog.dismiss(); if (failureReason == null) { - addKeyserver(mKeyserver, true); + keyserverEdited(mKeyserver, true); } else { verificationFailed(failureReason); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEmailDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEmailDialogFragment.java index 5b91b9d37..c55c75b55 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEmailDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEmailDialogFragment.java @@ -18,7 +18,7 @@ package org.sufficientlysecure.keychain.ui.dialog; import android.app.Activity; -import android.app.AlertDialog; +import android.support.v7.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java index 0b1d39fc1..b51d081e1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java @@ -18,12 +18,12 @@ package org.sufficientlysecure.keychain.ui.dialog; import android.annotation.TargetApi; -import android.app.AlertDialog; import android.app.Dialog; import android.os.Build; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentActivity; +import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddUserIdDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddUserIdDialogFragment.java index fe4ba0262..bc82feb70 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddUserIdDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddUserIdDialogFragment.java @@ -18,7 +18,7 @@ package org.sufficientlysecure.keychain.ui.dialog; import android.app.Activity; -import android.app.AlertDialog; +import android.support.v7.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AdvancedAppSettingsDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AdvancedAppSettingsDialogFragment.java index d2fa37cf7..d96879119 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AdvancedAppSettingsDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AdvancedAppSettingsDialogFragment.java @@ -27,7 +27,7 @@ import org.sufficientlysecure.keychain.R; public class AdvancedAppSettingsDialogFragment extends DialogFragment { private static final String ARG_PACKAGE_NAME = "package_name"; - private static final String ARG_SIGNATURE = "signature"; + private static final String ARG_CERTIFICATE = "certificate"; /** * Creates new instance of this fragment @@ -36,7 +36,7 @@ public class AdvancedAppSettingsDialogFragment extends DialogFragment { AdvancedAppSettingsDialogFragment frag = new AdvancedAppSettingsDialogFragment(); Bundle args = new Bundle(); args.putString(ARG_PACKAGE_NAME, packageName); - args.putString(ARG_SIGNATURE, digest); + args.putString(ARG_CERTIFICATE, digest); frag.setArguments(args); return frag; @@ -62,10 +62,10 @@ public class AdvancedAppSettingsDialogFragment extends DialogFragment { }); String packageName = getArguments().getString(ARG_PACKAGE_NAME); - String signature = getArguments().getString(ARG_SIGNATURE); + String certificate = getArguments().getString(ARG_CERTIFICATE); alert.setMessage(getString(R.string.api_settings_package_name) + ": " + packageName + "\n\n" - + getString(R.string.api_settings_package_signature) + ": " + signature); + + getString(R.string.api_settings_package_certificate) + ": " + certificate); return alert.show(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CustomAlertDialogBuilder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CustomAlertDialogBuilder.java index 794af5b15..840b95a3c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CustomAlertDialogBuilder.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CustomAlertDialogBuilder.java @@ -17,42 +17,15 @@ package org.sufficientlysecure.keychain.ui.dialog; -import android.app.AlertDialog; import android.content.Context; -import android.view.View; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.R; +import android.support.v7.app.AlertDialog; /** - * This class extends AlertDiaog.Builder, styling the header using emphasis color. - * Note that this class is a huge hack, because dialog boxes aren't easily stylable. - * Also, the dialog NEEDS to be called with show() directly, not create(), otherwise - * the order of internal operations will lead to a crash! + * Uses support lib's dialog builder. We can apply a theme here later! */ public class CustomAlertDialogBuilder extends AlertDialog.Builder { public CustomAlertDialogBuilder(Context context) { super(context); } - - @Override - public AlertDialog show() { - AlertDialog dialog = super.show(); - - int dividerId = dialog.getContext().getResources().getIdentifier("android:id/titleDivider", null, null); - View divider = dialog.findViewById(dividerId); - if (divider != null) { - divider.setBackgroundColor(dialog.getContext().getResources().getColor(R.color.header_text)); - } - - int textViewId = dialog.getContext().getResources().getIdentifier("android:id/alertTitle", null, null); - TextView tv = (TextView) dialog.findViewById(textViewId); - if (tv != null) { - tv.setTextColor(dialog.getContext().getResources().getColor(R.color.header_text)); - } - - return dialog; - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java deleted file mode 100644 index 581a96e52..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2013-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.ui.dialog; - -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; -import android.support.v4.app.DialogFragment; -import android.support.v4.app.FragmentActivity; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.KeyRing; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.util.Log; - -import java.util.HashMap; - -public class DeleteKeyDialogFragment extends DialogFragment { - private static final String ARG_MESSENGER = "messenger"; - private static final String ARG_DELETE_MASTER_KEY_IDS = "delete_master_key_ids"; - - public static final int MESSAGE_OKAY = 1; - public static final int MESSAGE_ERROR = 0; - - private TextView mMainMessage; - private View mInflateView; - - /** - * Creates new instance of this delete file dialog fragment - */ - public static DeleteKeyDialogFragment newInstance(Messenger messenger, long[] masterKeyIds) { - DeleteKeyDialogFragment frag = new DeleteKeyDialogFragment(); - Bundle args = new Bundle(); - - args.putParcelable(ARG_MESSENGER, messenger); - args.putLongArray(ARG_DELETE_MASTER_KEY_IDS, masterKeyIds); - - frag.setArguments(args); - - return frag; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final FragmentActivity activity = getActivity(); - final Messenger messenger = getArguments().getParcelable(ARG_MESSENGER); - - final long[] masterKeyIds = getArguments().getLongArray(ARG_DELETE_MASTER_KEY_IDS); - - CustomAlertDialogBuilder builder = new CustomAlertDialogBuilder(activity); - - // Setup custom View to display in AlertDialog - LayoutInflater inflater = activity.getLayoutInflater(); - mInflateView = inflater.inflate(R.layout.view_key_delete_fragment, null); - builder.setView(mInflateView); - - mMainMessage = (TextView) mInflateView.findViewById(R.id.mainMessage); - - final boolean hasSecret; - - // If only a single key has been selected - if (masterKeyIds.length == 1) { - long masterKeyId = masterKeyIds[0]; - - try { - HashMap<String, Object> data = new ProviderHelper(activity).getUnifiedData( - masterKeyId, new String[]{ - KeyRings.USER_ID, - KeyRings.HAS_ANY_SECRET - }, new int[]{ - ProviderHelper.FIELD_TYPE_STRING, - ProviderHelper.FIELD_TYPE_INTEGER - } - ); - String name; - KeyRing.UserId mainUserId = KeyRing.splitUserId((String) data.get(KeyRings.USER_ID)); - if (mainUserId.name != null) { - name = mainUserId.name; - } else { - name = getString(R.string.user_id_no_name); - } - hasSecret = ((Long) data.get(KeyRings.HAS_ANY_SECRET)) == 1; - - if (hasSecret) { - // show title only for secret key deletions, - // see http://www.google.com/design/spec/components/dialogs.html#dialogs-behavior - builder.setTitle(getString(R.string.title_delete_secret_key, name)); - mMainMessage.setText(getString(R.string.secret_key_deletion_confirmation, name)); - } else { - mMainMessage.setText(getString(R.string.public_key_deletetion_confirmation, name)); - } - } catch (ProviderHelper.NotFoundException e) { - dismiss(); - return null; - } - } else { - mMainMessage.setText(R.string.key_deletion_confirmation_multi); - hasSecret = false; - } - - builder.setPositiveButton(R.string.btn_delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - // Send all information needed to service to import key in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - - intent.setAction(KeychainIntentService.ACTION_DELETE); - - // Message is received after importing is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_deleting), - ProgressDialog.STYLE_HORIZONTAL, - true, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - @Override - public void handleMessage(Message message) { - super.handleMessage(message); - // handle messages by standard KeychainIntentServiceHandler first - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - try { - Message msg = Message.obtain(); - msg.copyFrom(message); - messenger.send(msg); - } catch (RemoteException e) { - Log.e(Constants.TAG, "messenger error", e); - } - } - } - }; - - // fill values for this action - Bundle data = new Bundle(); - data.putLongArray(KeychainIntentService.DELETE_KEY_LIST, masterKeyIds); - data.putBoolean(KeychainIntentService.DELETE_IS_SECRET, hasSecret); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - // show progress dialog - saveHandler.showProgressDialog(getActivity()); - - // start service with intent - getActivity().startService(intent); - - dismiss(); - } - }); - builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int id) { - dismiss(); - } - }); - - return builder.show(); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyDialogFragment.java index 9568312f5..b51648740 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyDialogFragment.java @@ -35,6 +35,7 @@ public class EditSubkeyDialogFragment extends DialogFragment { public static final int MESSAGE_CHANGE_EXPIRY = 1; public static final int MESSAGE_REVOKE = 2; public static final int MESSAGE_STRIP = 3; + public static final int MESSAGE_MOVE_KEY_TO_CARD = 4; private Messenger mMessenger; @@ -76,6 +77,9 @@ public class EditSubkeyDialogFragment extends DialogFragment { case 2: sendMessageToHandler(MESSAGE_STRIP, null); break; + case 3: + sendMessageToHandler(MESSAGE_MOVE_KEY_TO_CARD, null); + break; default: break; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyExpiryDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyExpiryDialogFragment.java index 37e05a61d..c9fb990a2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyExpiryDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyExpiryDialogFragment.java @@ -24,6 +24,7 @@ import android.os.Bundle; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; +import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.text.format.DateFormat; import android.view.LayoutInflater; @@ -36,6 +37,8 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.Log; import java.util.Calendar; @@ -72,17 +75,19 @@ public class EditSubkeyExpiryDialogFragment extends DialogFragment { /** * Creates dialog */ + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity activity = getActivity(); + Activity activity = getActivity(); + mMessenger = getArguments().getParcelable(ARG_MESSENGER); long creation = getArguments().getLong(ARG_CREATION); long expiry = getArguments().getLong(ARG_EXPIRY); - Calendar creationCal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - creationCal.setTime(new Date(creation * 1000)); - final Calendar expiryCal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - expiryCal.setTime(new Date(expiry * 1000)); + final Calendar creationCal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + creationCal.setTimeInMillis(creation * 1000); + Calendar expiryCal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + expiryCal.setTimeInMillis(expiry * 1000); // date picker works with default time zone, we need to convert from UTC to default timezone creationCal.setTimeZone(TimeZone.getDefault()); @@ -123,11 +128,8 @@ public class EditSubkeyExpiryDialogFragment extends DialogFragment { noExpiry.setChecked(false); expiryLayout.setVisibility(View.VISIBLE); - // convert from UTC to time zone of device - Calendar expiryCalTimeZone = (Calendar) expiryCal.clone(); - expiryCalTimeZone.setTimeZone(TimeZone.getDefault()); currentExpiry.setText(DateFormat.getDateFormat( - getActivity()).format(expiryCalTimeZone.getTime())); + getActivity()).format(expiryCal.getTime())); } // date picker works based on default time zone @@ -175,10 +177,13 @@ public class EditSubkeyExpiryDialogFragment extends DialogFragment { selectedCal.setTimeZone(TimeZone.getTimeZone("UTC")); long numDays = (selectedCal.getTimeInMillis() / 86400000) - - (expiryCal.getTimeInMillis() / 86400000); + - (creationCal.getTimeInMillis() / 86400000); if (numDays <= 0) { - Log.e(Constants.TAG, "Should not happen! Expiry num of days <= 0!"); - throw new RuntimeException(); + Activity activity = getActivity(); + if (activity != null) { + Notify.create(activity, R.string.error_expiry_past, Style.ERROR).show(); + } + return; } expiry = selectedCal.getTime().getTime() / 1000; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java index 63b6d26ac..84774ae5e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Message; import android.os.Messenger; @@ -119,14 +120,19 @@ public class FileDialogFragment extends DialogFragment { mFilename = (EditText) view.findViewById(R.id.input); mFilename.setText(mFile.getName()); mBrowse = (ImageButton) view.findViewById(R.id.btn_browse); - mBrowse.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - // only .asc or .gpg files - // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc - // or gpg types! - FileHelper.openFile(FileDialogFragment.this, Uri.fromFile(mFile), "*/*", REQUEST_CODE); - } - }); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + mBrowse.setVisibility(View.GONE); + } else { + mBrowse.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // only .asc or .gpg files + // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc + // or gpg types! + FileHelper.saveDocumentKitKat( + FileDialogFragment.this, "*/*", mFile.getName(), REQUEST_CODE); + } + }); + } mCheckBox = (CheckBox) view.findViewById(R.id.checkbox); if (checkboxText == null) { @@ -136,6 +142,7 @@ public class FileDialogFragment extends DialogFragment { mCheckBox.setEnabled(true); mCheckBox.setVisibility(View.VISIBLE); mCheckBox.setText(checkboxText); + mCheckBox.setChecked(true); } alert.setView(view); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/OrbotStartDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/OrbotStartDialogFragment.java new file mode 100644 index 000000000..b06e05c30 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/OrbotStartDialogFragment.java @@ -0,0 +1,163 @@ +/* + * 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.ui.dialog; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.widget.Button; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; + +/** + * displays a dialog asking the user to enable Tor + */ +public class OrbotStartDialogFragment extends DialogFragment { + private static final String ARG_MESSENGER = "messenger"; + private static final String ARG_TITLE = "title"; + private static final String ARG_MESSAGE = "message"; + private static final String ARG_MIDDLE_BUTTON = "middleButton"; + + private static final int ORBOT_REQUEST_CODE = 1; + + public static final int MESSAGE_MIDDLE_BUTTON = 1; + public static final int MESSAGE_DIALOG_CANCELLED = 2; // for either cancel or enable pressed + public static final int MESSAGE_ORBOT_STARTED = 3; // for either cancel or enable pressed + + public static OrbotStartDialogFragment newInstance(Messenger messenger, int title, int message, int middleButton) { + Bundle args = new Bundle(); + args.putParcelable(ARG_MESSENGER, messenger); + args.putInt(ARG_TITLE, title); + args.putInt(ARG_MESSAGE, message); + args.putInt(ARG_MIDDLE_BUTTON, middleButton); + + OrbotStartDialogFragment fragment = new OrbotStartDialogFragment(); + fragment.setArguments(args); + + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + + final Messenger messenger = getArguments().getParcelable(ARG_MESSENGER); + int title = getArguments().getInt(ARG_TITLE); + final int message = getArguments().getInt(ARG_MESSAGE); + int middleButton = getArguments().getInt(ARG_MIDDLE_BUTTON); + final Activity activity = getActivity(); + + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); + + CustomAlertDialogBuilder builder = new CustomAlertDialogBuilder(theme); + builder.setTitle(title).setMessage(message); + + builder.setNegativeButton(R.string.orbot_start_dialog_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Message msg = Message.obtain(); + msg.what = MESSAGE_DIALOG_CANCELLED; + try { + messenger.send(msg); + } catch (RemoteException e) { + Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); + } catch (NullPointerException e) { + Log.w(Constants.TAG, "Messenger is null!", e); + } + + } + }); + + builder.setNeutralButton(middleButton, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Message msg = new Message(); + msg.what = MESSAGE_MIDDLE_BUTTON; + try { + messenger.send(msg); + } catch (RemoteException e) { + Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); + } catch (NullPointerException e) { + Log.w(Constants.TAG, "Messenger is null!", e); + } + } + }); + + builder.setPositiveButton(R.string.orbot_start_dialog_start, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // actual onClick defined in onStart, this is just to make the button appear + } + }); + + return builder.show(); + } + + @Override + public void onStart() { + super.onStart(); + //super.onStart() is where dialog.show() is actually called on the underlying dialog, + // so we have to do it after this point + AlertDialog d = (AlertDialog) getDialog(); + if (d != null) { + Button positiveButton = d.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + startActivityForResult(OrbotHelper.getShowOrbotStartIntent(), + ORBOT_REQUEST_CODE); + } + }); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == ORBOT_REQUEST_CODE) { + // assume Orbot was started + final Messenger messenger = getArguments().getParcelable(ARG_MESSENGER); + + Message msg = Message.obtain(); + msg.what = MESSAGE_ORBOT_STARTED; + try { + messenger.send(msg); + } catch (RemoteException e) { + Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); + } catch (NullPointerException e) { + Log.w(Constants.TAG, "Messenger is null!", e); + } + dismiss(); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PreferenceInstallDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PreferenceInstallDialogFragment.java new file mode 100644 index 000000000..3f8bce28b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PreferenceInstallDialogFragment.java @@ -0,0 +1,75 @@ +/* + * 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.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.os.Messenger; +import android.app.DialogFragment; + +import org.sufficientlysecure.keychain.ui.util.InstallDialogFragmentHelper; + +public class PreferenceInstallDialogFragment extends DialogFragment { + + public static final int MESSAGE_MIDDLE_CLICKED = 1; + public static final int MESSAGE_DIALOG_DISMISSED = 2; + + /** + * Creates a dialog which prompts the user to install an application. Consists of two default buttons ("Install" + * and "Cancel") and an optional third button. Callbacks are provided only for the middle button, if set. + * + * @param messenger required only for callback from middle button if it has been set + * @param title + * @param message content of dialog + * @param packageToInstall package name of application to install + * @param middleButton if not null, adds a third button to the app with a call back + * @return The dialog to display + */ + public static PreferenceInstallDialogFragment newInstance(Messenger messenger, int title, int message, + String packageToInstall, int middleButton, boolean + useMiddleButton) { + PreferenceInstallDialogFragment frag = new PreferenceInstallDialogFragment(); + Bundle args = new Bundle(); + + InstallDialogFragmentHelper.wrapIntoArgs(messenger, title, message, packageToInstall, middleButton, + useMiddleButton, args); + + frag.setArguments(args); + + return frag; + } + + /** + * To create a DialogFragment with only two buttons + * + * @param title + * @param message + * @param packageToInstall + * @return + */ + public static PreferenceInstallDialogFragment newInstance(int title, int message, + String packageToInstall) { + return newInstance(null, title, message, packageToInstall, -1, false); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return InstallDialogFragmentHelper.getInstallDialogFromArgs(getArguments(), getActivity(), + MESSAGE_MIDDLE_CLICKED, MESSAGE_DIALOG_DISMISSED); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java index af9d175ff..764291dd0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java @@ -25,6 +25,7 @@ import android.content.DialogInterface.OnKeyListener; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.view.ContextThemeWrapper; import android.view.KeyEvent; @@ -32,25 +33,19 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.service.CloudImportService; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.service.KeychainService; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +/** + * meant to be used + */ public class ProgressDialogFragment extends DialogFragment { private static final String ARG_MESSAGE = "message"; private static final String ARG_STYLE = "style"; private static final String ARG_CANCELABLE = "cancelable"; private static final String ARG_SERVICE_TYPE = "service_class"; - public static enum ServiceType { - KEYCHAIN_INTENT, - CLOUD_IMPORT - } - - ServiceType mServiceType; - boolean mCanCancel = false, mPreventCancel = false, mIsCancelled = false; /** @@ -58,41 +53,35 @@ public class ProgressDialogFragment extends DialogFragment { * @param message the message to be displayed initially above the progress bar * @param style the progress bar style, as defined in ProgressDialog (horizontal or spinner) * @param cancelable should we let the user cancel this operation - * @param serviceType which Service this progress dialog is meant for * @return */ - public static ProgressDialogFragment newInstance(String message, int style, boolean cancelable, - ServiceType serviceType) { + public static ProgressDialogFragment newInstance(String message, int style, boolean cancelable) { ProgressDialogFragment frag = new ProgressDialogFragment(); Bundle args = new Bundle(); args.putString(ARG_MESSAGE, message); args.putInt(ARG_STYLE, style); args.putBoolean(ARG_CANCELABLE, cancelable); - args.putSerializable(ARG_SERVICE_TYPE, serviceType); frag.setArguments(args); return frag; } - /** Updates progress of dialog */ public void setProgress(int messageId, int progress, int max) { setProgress(getString(messageId), progress, max); } - /** Updates progress of dialog */ public void setProgress(int progress, int max) { - if (mIsCancelled) { + ProgressDialog dialog = (ProgressDialog) getDialog(); + + if (mIsCancelled || dialog == null) { return; } - ProgressDialog dialog = (ProgressDialog) getDialog(); - dialog.setProgress(progress); dialog.setMax(max); } - /** Updates progress of dialog */ public void setProgress(String message, int progress, int max) { ProgressDialog dialog = (ProgressDialog) getDialog(); @@ -105,17 +94,12 @@ public class ProgressDialogFragment extends DialogFragment { dialog.setMax(max); } - /** - * Creates dialog - */ + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Activity activity = getActivity(); - // if the progress dialog is displayed from the application class, design is missing - // hack to get holo design (which is not automatically applied due to activity's Theme.NoDisplay - ContextThemeWrapper context = new ContextThemeWrapper(activity, - R.style.Theme_AppCompat_Light); + ContextThemeWrapper context = ThemeChanger.getDialogThemeWrapper(activity); ProgressDialog dialog = new ProgressDialog(context); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); @@ -126,7 +110,6 @@ public class ProgressDialogFragment extends DialogFragment { String message = getArguments().getString(ARG_MESSAGE); int style = getArguments().getInt(ARG_STYLE); mCanCancel = getArguments().getBoolean(ARG_CANCELABLE); - mServiceType = (ServiceType) getArguments().getSerializable(ARG_SERVICE_TYPE); dialog.setMessage(message); dialog.setProgressStyle(style); @@ -165,7 +148,12 @@ public class ProgressDialogFragment extends DialogFragment { } mPreventCancel = preventCancel; - final Button negative = ((ProgressDialog) getDialog()).getButton(DialogInterface.BUTTON_NEGATIVE); + ProgressDialog dialog = (ProgressDialog) getDialog(); + if (dialog == null) { + return; + } + + final Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); negative.setEnabled(mIsCancelled && !preventCancel); } @@ -188,30 +176,24 @@ public class ProgressDialogFragment extends DialogFragment { negative.setClickable(false); negative.setTextColor(Color.GRAY); + // send a cancel message. note that this message will be handled by + // KeychainService.onStartCommand, which runs in this thread, + // not the service one, and will not queue up a command. + Intent serviceIntent = new Intent(getActivity(), KeychainService.class); + + serviceIntent.setAction(KeychainService.ACTION_CANCEL); + getActivity().startService(serviceIntent); + // Set the progress bar accordingly ProgressDialog dialog = (ProgressDialog) getDialog(); + if (dialog == null) { + return; + } + dialog.setIndeterminate(true); dialog.setMessage(getString(R.string.progress_cancelling)); - // send a cancel message. note that this message will be handled by - // KeychainIntentService.onStartCommand, which runs in this thread, - // not the service one, and will not queue up a command. - Intent serviceIntent = null; - - switch (mServiceType) { - case CLOUD_IMPORT: - serviceIntent = new Intent(getActivity(), CloudImportService.class); - break; - case KEYCHAIN_INTENT: - serviceIntent = new Intent(getActivity(), KeychainIntentService.class); - break; - default: - //should never happen, unless we forget to include a ServiceType enum case - Log.e(Constants.TAG, "Unrecognized ServiceType at ProgressDialogFragment"); - } - serviceIntent.setAction(KeychainIntentService.ACTION_CANCEL); - getActivity().startService(serviceIntent); } }); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java index 4eb253825..a990682f6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java @@ -18,7 +18,7 @@ package org.sufficientlysecure.keychain.ui.dialog; import android.app.Activity; -import android.app.AlertDialog; +import android.support.v7.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SupportInstallDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SupportInstallDialogFragment.java new file mode 100644 index 000000000..82d1be4ed --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SupportInstallDialogFragment.java @@ -0,0 +1,78 @@ +/* + * 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.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.os.Messenger; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; + +import org.sufficientlysecure.keychain.ui.util.InstallDialogFragmentHelper; + +public class SupportInstallDialogFragment extends DialogFragment { + + public static final int MESSAGE_MIDDLE_CLICKED = 1; + public static final int MESSAGE_DIALOG_DISMISSED = 2; + + /** + * Creates a dialog which prompts the user to install an application. Consists of two default buttons ("Install" + * and "Cancel") and an optional third button. Callbacks are provided only for the middle button, if set. + * + * @param messenger required only for callback from middle button if it has been set + * @param title xml resource for title of the install dialog + * @param message content of dialog + * @param packageToInstall package name of application to install + * @param middleButton if not null, adds a third button to the app with a call back + * @return The dialog to display + */ + public static SupportInstallDialogFragment newInstance(Messenger messenger, int title, int message, + String packageToInstall, int middleButton, boolean + useMiddleButton) { + SupportInstallDialogFragment frag = new SupportInstallDialogFragment(); + Bundle args = new Bundle(); + + InstallDialogFragmentHelper.wrapIntoArgs(messenger, title, message, packageToInstall, middleButton, + useMiddleButton, args); + + frag.setArguments(args); + + return frag; + } + + /** + * To create a DialogFragment with only two buttons + * + * @param title xml string resource for title of the dialog + * @param message xml string resource to display as dialog body + * @param packageToInstall name of package to install + * @return + */ + public static SupportInstallDialogFragment newInstance(int title, int message, + String packageToInstall) { + return newInstance(null, title, message, packageToInstall, -1, false); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + + return InstallDialogFragmentHelper.getInstallDialogFromArgs(getArguments(), getActivity(), + MESSAGE_MIDDLE_CLICKED, MESSAGE_DIALOG_DISMISSED); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java index 8062428e3..c54d0c948 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep1Fragment.java @@ -107,10 +107,10 @@ public class LinkedIdCreateDnsStep1Fragment extends Fragment { if (uri.length() > 0) { if (checkUri(uri)) { mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0, - R.drawable.uid_mail_ok, 0); + R.drawable.ic_stat_retyped_ok, 0); } else { mEditDns.setCompoundDrawablesWithIntrinsicBounds(0, 0, - R.drawable.uid_mail_bad, 0); + R.drawable.ic_stat_retyped_bad, 0); } } else { // remove drawable if email is empty diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java index e0e6976ee..c9eca8882 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateDnsStep2Fragment.java @@ -17,6 +17,14 @@ package org.sufficientlysecure.keychain.ui.linked; + +import java.io.FileNotFoundException; +import java.io.PrintWriter; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -26,17 +34,14 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.TextView; +import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.linked.LinkedTokenResource; import org.sufficientlysecure.keychain.linked.resources.DnsResource; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; -import java.io.FileNotFoundException; -import java.io.PrintWriter; - public class LinkedIdCreateDnsStep2Fragment extends LinkedIdCreateFinalFragment { private static final int REQUEST_CODE_OUTPUT = 0x00007007; @@ -117,7 +122,20 @@ public class LinkedIdCreateDnsStep2Fragment extends LinkedIdCreateFinalFragment } private void proofToClipboard() { - ClipboardReflection.copyToClipboard(getActivity(), mResourceString); + Activity activity = getActivity(); + if (activity == null) { + return; + } + + ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipMan == null) { + Notify.create(activity, R.string.error_clipboard_copy, Style.ERROR).show(); + return; + } + + ClipData clip = ClipData.newPlainText(Constants.CLIPBOARD_LABEL, mResourceString); + clipMan.setPrimaryClip(clip); + Notify.create(getActivity(), R.string.linked_text_clipboard, Notify.Style.OK).show(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java index eedc7cdd9..24499a467 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java @@ -1,12 +1,11 @@ package org.sufficientlysecure.keychain.ui.linked; -import android.app.ProgressDialog; -import android.content.Intent; + import android.graphics.PorterDuff; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; +import android.os.Parcelable; +import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -16,20 +15,18 @@ import android.widget.TextView; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.linked.LinkedAttribute; +import org.sufficientlysecure.keychain.linked.LinkedTokenResource; import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; -import org.sufficientlysecure.keychain.linked.LinkedTokenResource; -import org.sufficientlysecure.keychain.linked.LinkedAttribute; -import org.sufficientlysecure.keychain.service.KeychainIntentService; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment.ServiceType; import org.sufficientlysecure.keychain.ui.util.Notify; + public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragment { protected LinkedIdWizard mLinkedIdWizard; @@ -169,54 +166,31 @@ public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragmen } - protected void cryptoOperation(CryptoInputParcel cryptoInput) { - + @Override + protected void cryptoOperation() { if (mVerifiedResource == null) { Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR) .show(LinkedIdCreateFinalFragment.this); return; } - ServiceProgressHandler saveHandler = new ServiceProgressHandler( - getActivity(), - getString(R.string.progress_saving), - ProgressDialog.STYLE_HORIZONTAL, - true, ServiceType.KEYCHAIN_INTENT) { - - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - // handle pending messages - if (handlePendingMessage(message)) { - return; - } - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - - // get returned data bundle - Bundle returnData = message.getData(); - if (returnData == null) { - return; - } - final OperationResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null) { - return; - } - - // if bad -> display here! - if (!result.success()) { - result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this); - return; - } + super.cryptoOperation(); + } - getActivity().finish(); + @Override + protected void cryptoOperation(CryptoInputParcel cryptoInput) { + if (mVerifiedResource == null) { + Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR) + .show(LinkedIdCreateFinalFragment.this); + return; + } - } - } - }; + super.cryptoOperation(cryptoInput); + } + @Nullable + @Override + public Parcelable createOperationInput() { SaveKeyringParcel skp = new SaveKeyringParcel(mLinkedIdWizard.mMasterKeyId, mLinkedIdWizard.mFingerprint); @@ -225,25 +199,22 @@ public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragmen skp.mAddUserAttribute.add(ua); - // Send all information needed to service to import key in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_EDIT_KEYRING); - - // fill values for this action - Bundle data = new Bundle(); - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); - data.putParcelable(KeychainIntentService.EDIT_KEYRING_PARCEL, skp); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + return skp; + } - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + @Override + public void onCryptoOperationSuccess(OperationResult result) { + // if bad -> display here! + if (!result.success()) { + result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this); + return; + } - // show progress dialog - saveHandler.showProgressDialog(getActivity()); + getActivity().finish(); + } - // start service with intent - getActivity().startService(intent); + @Override + public void onCryptoOperationError(OperationResult result) { } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java index 7bc33c93b..8a05c35db 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java @@ -103,10 +103,10 @@ public class LinkedIdCreateHttpsStep1Fragment extends Fragment { if (uri.length() > 0) { if (checkUri(uri)) { mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, - R.drawable.uid_mail_ok, 0); + R.drawable.ic_stat_retyped_ok, 0); } else { mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, - R.drawable.uid_mail_bad, 0); + R.drawable.ic_stat_retyped_bad, 0); } } else { // remove drawable if email is empty diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java index 2af97fe36..22a201ba3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java @@ -19,7 +19,6 @@ package org.sufficientlysecure.keychain.ui.linked; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.view.LayoutInflater; @@ -135,13 +134,10 @@ public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragmen String targetName = "pgpkey.txt"; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - File targetFile = new File(Constants.Path.APP_DIR, targetName); - FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file), - getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT); - } else { - FileHelper.saveDocument(this, "text/plain", targetName, REQUEST_CODE_OUTPUT); - } + FileHelper.saveDocument(this, + targetName, Uri.fromFile(new File(Constants.Path.APP_DIR, targetName)), + "text/plain", R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, + REQUEST_CODE_OUTPUT); } private void saveFile(Uri uri) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java index 82aaa51c4..7007fa50c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java @@ -12,8 +12,8 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.Message; -import android.os.Messenger; +import android.os.Parcelable; +import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager.OnBackStackChangedListener; import android.support.v4.app.LoaderManager; @@ -31,20 +31,17 @@ import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants.key; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.CertifyResult; import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult; import org.sufficientlysecure.keychain.linked.LinkedTokenResource; import org.sufficientlysecure.keychain.linked.LinkedAttribute; import org.sufficientlysecure.keychain.linked.LinkedResource; import org.sufficientlysecure.keychain.linked.UriAttribute; +import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; @@ -58,7 +55,6 @@ import org.sufficientlysecure.keychain.ui.widget.CertListWidget; import org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner; import org.sufficientlysecure.keychain.util.Log; - public class LinkedIdViewFragment extends CryptoOperationFragment implements LoaderManager.LoaderCallbacks<Cursor>, OnBackStackChangedListener { @@ -97,6 +93,12 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements return frag; } + public LinkedIdViewFragment() { + // IMPORTANT: the id must be unique in the ViewKeyActivity CryptoOperationHelper id namespace! + // no initial progress message -> we handle progress ourselves! + super(5, null); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -499,7 +501,7 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements } // get the user's passphrase for this key (if required) - mCertifyKeyId = mViewHolder.vKeySpinner.getSelectedItemId(); + mCertifyKeyId = mViewHolder.vKeySpinner.getSelectedKeyId(); if (mCertifyKeyId == key.none || mCertifyKeyId == key.symmetric) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { SubtleAttentionSeeker.tintBackground(mViewHolder.vKeySpinnerContainer, 600).start(); @@ -509,11 +511,13 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements return; } + mViewHolder.setVerifyingState(mContext, VerifyState.CERTIFYING, false); cryptoOperation(); + } @Override - protected void onCryptoOperationCancelled() { + public void onCryptoOperationCancelled() { super.onCryptoOperationCancelled(); // go back to 'verified ok' @@ -521,68 +525,34 @@ public class LinkedIdViewFragment extends CryptoOperationFragment implements } + @Nullable @Override - protected void cryptoOperation(CryptoInputParcel cryptoInput) { - - if (mIsSecret) { - return; - } - - mViewHolder.setVerifyingState(mContext, VerifyState.CERTIFYING, false); - - Bundle data = new Bundle(); - { - - long masterKeyId = KeyFormattingUtils.convertFingerprintToKeyId(mFingerprint); - CertifyAction action = new CertifyAction(masterKeyId, null, - Collections.singletonList(mLinkedId.toUserAttribute())); - - // fill values for this action - CertifyActionsParcel parcel = new CertifyActionsParcel(mCertifyKeyId); - parcel.mCertifyActions.addAll(Collections.singletonList(action)); - data.putParcelable(KeychainIntentService.CERTIFY_PARCEL, parcel); - - data.putParcelable(KeychainIntentService.EXTRA_CRYPTO_INPUT, cryptoInput); + public Parcelable createOperationInput() { + long masterKeyId = KeyFormattingUtils.convertFingerprintToKeyId(mFingerprint); + CertifyAction action = new CertifyAction(masterKeyId, null, + Collections.singletonList(mLinkedId.toUserAttribute())); - /* if (mUploadKeyCheckbox.isChecked()) { - String keyserver = Preferences.getPreferences(getActivity()).getPreferredKeyserver(); - data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, keyserver); - } */ - } - - // Send all information needed to service to sign key in other thread - Intent intent = new Intent(getActivity(), KeychainIntentService.class); - intent.setAction(KeychainIntentService.ACTION_CERTIFY_KEYRING); - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Message is received after signing is done in KeychainIntentService - ServiceProgressHandler saveHandler = new ServiceProgressHandler(getActivity()) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); - - // handle pending messages - if (handlePendingMessage(message)) { - return; - } - - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - Bundle data = message.getData(); - CertifyResult result = data.getParcelable(CertifyResult.EXTRA_RESULT); - result.createNotify(getActivity()).show(); - // no need to do anything else, we will get a loader refresh! - } + // fill values for this action + CertifyActionsParcel parcel = new CertifyActionsParcel(mCertifyKeyId); + parcel.mCertifyActions.addAll(Collections.singletonList(action)); - } - }; + return parcel; + } - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(saveHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + @Override + public void onCryptoOperationSuccess(OperationResult result) { + result.createNotify(getActivity()).show(); + // no need to do anything else, we will get a loader refresh! + } - // start service with intent - getActivity().startService(intent); + @Override + public void onCryptoOperationError(OperationResult result) { + result.createNotify(getActivity()).show(); + } + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return true; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java new file mode 100644 index 000000000..43ccac24f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2015 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.ui.util; + +import android.content.Context; + +import org.spongycastle.util.Arrays; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.BitSet; + +public class ExperimentalWordConfirm { + + public static String getWords(Context context, byte[] fingerprintBlob) { + ArrayList<String> words = new ArrayList<>(); + + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader( + context.getAssets().open("word_confirm_list.txt"), + "UTF-8" + )); + + String line = reader.readLine(); + while (line != null) { + words.add(line); + + line = reader.readLine(); + } + } catch (IOException e) { + throw new RuntimeException("IOException", e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) { + } + } + } + + String fingerprint = ""; + + // NOTE: 160 bit SHA-1 truncated to 156 bit + byte[] fingerprintBlobTruncated = Arrays.copyOfRange(fingerprintBlob, 0, 156 / 8); + + // TODO: implement key stretching to minimize fp length? + + // BitSet bits = BitSet.valueOf(fingerprintBlob); // min API 19 and little endian! + BitSet bits = bitSetToByteArray(fingerprintBlobTruncated); + Log.d(Constants.TAG, "bits: " + bits.toString()); + + final int CHUNK_SIZE = 13; + final int LAST_CHUNK_INDEX = fingerprintBlobTruncated.length * 8 / CHUNK_SIZE; // 12 + Log.d(Constants.TAG, "LAST_CHUNK_INDEX: " + LAST_CHUNK_INDEX); + + int from = 0; + int to = CHUNK_SIZE; + for (int i = 0; i < (LAST_CHUNK_INDEX + 1); i++) { + Log.d(Constants.TAG, "from: " + from + " to: " + to); + + BitSet setIndex = bits.get(from, to); + int wordIndex = (int) bitSetToLong(setIndex); + // int wordIndex = (int) setIndex.toLongArray()[0]; // min API 19 + + fingerprint += words.get(wordIndex); + + if (i != LAST_CHUNK_INDEX) { + // line break every 3 words + if (to % (CHUNK_SIZE * 3) == 0) { + fingerprint += "\n"; + } else { + fingerprint += " "; + } + } + + from = to; + to += CHUNK_SIZE; + } + + return fingerprint; + } + + /** + * Returns a BitSet containing the values in bytes. + * BIG ENDIAN! + */ + private static BitSet bitSetToByteArray(byte[] bytes) { + int arrayLength = bytes.length * 8; + BitSet bits = new BitSet(); + + for (int i = 0; i < arrayLength; i++) { + if ((bytes[bytes.length - i / 8 - 1] & (1 << (i % 8))) > 0) { + bits.set(i); + } + } + return bits; + } + + private static long bitSetToLong(BitSet bits) { + long value = 0L; + for (int i = 0; i < bits.length(); ++i) { + value += bits.get(i) ? (1L << i) : 0L; + } + return value; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/FormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/FormattingUtils.java index eb5c3df45..902a7ec56 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/FormattingUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/FormattingUtils.java @@ -18,9 +18,11 @@ package org.sufficientlysecure.keychain.ui.util; import android.content.Context; +import android.content.res.Resources.Theme; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.StrikethroughSpan; +import android.util.TypedValue; public class FormattingUtils { @@ -32,4 +34,10 @@ public class FormattingUtils { return (int) ((px / context.getResources().getDisplayMetrics().density) + 0.5f); } + public static int getColorFromAttr(Context context, int attr) { + TypedValue typedValue = new TypedValue(); + Theme theme = context.getTheme(); + theme.resolveAttribute(attr, typedValue, true); + return typedValue.data; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java index 69338aa3e..ac34d5526 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Highlighter.java @@ -22,6 +22,7 @@ import android.text.Spannable; import android.text.style.ForegroundColorSpan; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,9 +45,12 @@ public class Highlighter { Pattern pattern = Pattern.compile("(?i)(" + mQuery.trim().replaceAll("\\s+", "|") + ")"); Matcher matcher = pattern.matcher(text); + + int colorEmphasis = FormattingUtils.getColorFromAttr(mContext, R.attr.colorEmphasis); + while (matcher.find()) { highlight.setSpan( - new ForegroundColorSpan(mContext.getResources().getColor(R.color.emphasis)), + new ForegroundColorSpan(colorEmphasis), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/InstallDialogFragmentHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/InstallDialogFragmentHelper.java new file mode 100644 index 000000000..b2213ed10 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/InstallDialogFragmentHelper.java @@ -0,0 +1,132 @@ +/* + * 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.ui.util; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v7.app.AlertDialog; +import android.view.ContextThemeWrapper; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.util.Log; + +public class InstallDialogFragmentHelper { + private static final String ARG_MESSENGER = "messenger"; + private static final String ARG_TITLE = "title"; + private static final String ARG_MESSAGE = "message"; + private static final String ARG_MIDDLE_BUTTON = "middleButton"; + private static final String ARG_INSTALL_PATH = "installPath"; + private static final String ARG_USE_MIDDLE_BUTTON = "useMiddleButton"; + + private static final String PLAY_STORE_PATH = "market://search?q=pname:"; + + public static void wrapIntoArgs(Messenger messenger, int title, int message, String packageToInstall, + int middleButton, boolean useMiddleButton, Bundle args) { + args.putParcelable(ARG_MESSENGER, messenger); + + args.putInt(ARG_TITLE, title); + args.putInt(ARG_MESSAGE, message); + args.putInt(ARG_MIDDLE_BUTTON, middleButton); + args.putString(ARG_INSTALL_PATH, PLAY_STORE_PATH + packageToInstall); + args.putBoolean(ARG_USE_MIDDLE_BUTTON, useMiddleButton); + } + + public static AlertDialog getInstallDialogFromArgs(Bundle args, final Activity activity, + final int messengerMiddleButtonClicked, + final int messengerDialogDimissed) { + final Messenger messenger = args.getParcelable(ARG_MESSENGER); + + final int title = args.getInt(ARG_TITLE); + final int message = args.getInt(ARG_MESSAGE); + final int middleButton = args.getInt(ARG_MIDDLE_BUTTON); + final String installPath = args.getString(ARG_INSTALL_PATH); + final boolean useMiddleButton = args.getBoolean(ARG_USE_MIDDLE_BUTTON); + + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); + CustomAlertDialogBuilder builder = new CustomAlertDialogBuilder(theme); + + builder.setTitle(title).setMessage(message); + + builder.setNegativeButton(R.string.orbot_install_dialog_cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Message msg = Message.obtain(); + msg.what = messengerDialogDimissed; + try { + messenger.send(msg); + } catch (RemoteException e) { + Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); + } catch (NullPointerException e) { + Log.w(Constants.TAG, "Messenger is null!", e); + } + } + }); + + builder.setPositiveButton(R.string.orbot_install_dialog_install, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri.parse(installPath); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + activity.startActivity(intent); + + Message msg = Message.obtain(); + msg.what = messengerDialogDimissed; + try { + messenger.send(msg); + } catch (RemoteException e) { + Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); + } catch (NullPointerException e) { + Log.w(Constants.TAG, "Messenger is null!", e); + } + } + } + ); + + if (useMiddleButton) { + builder.setNeutralButton(middleButton, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Message msg = Message.obtain(); + msg.what = messengerMiddleButtonClicked; + try { + messenger.send(msg); + } catch (RemoteException e) { + Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); + } catch (NullPointerException e) { + Log.w(Constants.TAG, "Messenger is null!", e); + } + } + } + ); + } + + return builder.show(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java index 3d98034d2..284c17e7a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java @@ -24,9 +24,12 @@ import android.graphics.PorterDuff; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.ForegroundColorSpan; +import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import org.openintents.openpgp.OpenPgpDecryptionResult; +import org.openintents.openpgp.OpenPgpSignatureResult; import org.spongycastle.asn1.ASN1ObjectIdentifier; import org.spongycastle.asn1.nist.NISTNamedCurves; import org.spongycastle.asn1.teletrust.TeleTrusTNamedCurves; @@ -34,6 +37,8 @@ import org.spongycastle.bcpg.PublicKeyAlgorithmTags; import org.spongycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve; import org.sufficientlysecure.keychain.util.Log; @@ -218,14 +223,11 @@ public class KeyFormattingUtils { public static String convertFingerprintToHex(byte[] fingerprint) { // NOTE: Even though v3 keys are not imported we need to support both fingerprints for // display/comparison before import - // Also better cut of unneeded parts, e.g., for fingerprints returned from YubiKeys - if (fingerprint.length < 20) { - // v3 key fingerprint with 128 bit (MD5) - return Hex.toHexString(fingerprint, 0, 16).toLowerCase(Locale.ENGLISH); - } else { - // v4 key fingerprint with 160 bit (SHA1) - return Hex.toHexString(fingerprint, 0, 20).toLowerCase(Locale.ENGLISH); + if (fingerprint.length != 16 && fingerprint.length != 20) { + throw new IllegalArgumentException("No valid v3 or v4 fingerprint!"); } + + return Hex.toHexString(fingerprint).toLowerCase(Locale.ENGLISH); } public static long getKeyIdFromFingerprint(byte[] fingerprint) { @@ -383,7 +385,6 @@ public class KeyFormattingUtils { /** * 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 @@ -401,7 +402,7 @@ public class KeyFormattingUtils { public static final int DEFAULT_COLOR = -1; - public static enum State { + public enum State { REVOKED, EXPIRED, VERIFIED, @@ -411,7 +412,8 @@ public class KeyFormattingUtils { UNVERIFIED, UNKNOWN_KEY, INVALID, - NOT_SIGNED + NOT_SIGNED, + INSECURE } public static void setStatusImage(Context context, ImageView statusIcon, State state) { @@ -427,9 +429,196 @@ public class KeyFormattingUtils { setStatusImage(context, statusIcon, statusText, state, color, false); } + public interface StatusHolder { + ImageView getEncryptionStatusIcon(); + TextView getEncryptionStatusText(); + + ImageView getSignatureStatusIcon(); + TextView getSignatureStatusText(); + + View getSignatureLayout(); + TextView getSignatureUserName(); + TextView getSignatureUserEmail(); + TextView getSignatureAction(); + + boolean hasEncrypt(); + + } + + @SuppressWarnings("deprecation") // context.getDrawable is api lvl 21, need to use deprecated + public static void setStatus(Context context, StatusHolder holder, DecryptVerifyResult result) { + + if (holder.hasEncrypt()) { + OpenPgpDecryptionResult decryptionResult = result.getDecryptionResult(); + + int encText, encIcon, encColor; + + switch (decryptionResult.getResult()) { + case OpenPgpDecryptionResult.RESULT_ENCRYPTED: { + encText = R.string.decrypt_result_encrypted; + encIcon = R.drawable.status_lock_closed_24dp; + encColor = R.color.key_flag_green; + break; + } + + case OpenPgpDecryptionResult.RESULT_INSECURE: { + encText = R.string.decrypt_result_insecure; + encIcon = R.drawable.status_signature_invalid_cutout_24dp; + encColor = R.color.key_flag_red; + break; + } + + default: + case OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED: { + encText = R.string.decrypt_result_not_encrypted; + encIcon = R.drawable.status_lock_open_24dp; + encColor = R.color.key_flag_red; + break; + } + } + + int encColorRes = context.getResources().getColor(encColor); + holder.getEncryptionStatusIcon().setColorFilter(encColorRes, PorterDuff.Mode.SRC_IN); + holder.getEncryptionStatusIcon().setImageDrawable(context.getResources().getDrawable(encIcon)); + holder.getEncryptionStatusText().setText(encText); + holder.getEncryptionStatusText().setTextColor(encColorRes); + } + + OpenPgpSignatureResult signatureResult = result.getSignatureResult(); + + int sigText, sigIcon, sigColor; + int sigActionText, sigActionIcon; + + switch (signatureResult.getResult()) { + + case OpenPgpSignatureResult.RESULT_NO_SIGNATURE: { + // no signature + + sigText = R.string.decrypt_result_no_signature; + sigIcon = R.drawable.status_signature_invalid_cutout_24dp; + sigColor = R.color.key_flag_gray; + + // won't be used, but makes compiler happy + sigActionText = 0; + sigActionIcon = 0; + break; + } + + case OpenPgpSignatureResult.RESULT_VALID_CONFIRMED: { + sigText = R.string.decrypt_result_signature_certified; + sigIcon = R.drawable.status_signature_verified_cutout_24dp; + sigColor = R.color.key_flag_green; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + case OpenPgpSignatureResult.RESULT_VALID_UNCONFIRMED: { + sigText = R.string.decrypt_result_signature_uncertified; + sigIcon = R.drawable.status_signature_unverified_cutout_24dp; + sigColor = R.color.key_flag_orange; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + case OpenPgpSignatureResult.RESULT_INVALID_KEY_REVOKED: { + sigText = R.string.decrypt_result_signature_revoked_key; + sigIcon = R.drawable.status_signature_revoked_cutout_24dp; + sigColor = R.color.key_flag_red; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + case OpenPgpSignatureResult.RESULT_INVALID_KEY_EXPIRED: { + sigText = R.string.decrypt_result_signature_expired_key; + sigIcon = R.drawable.status_signature_expired_cutout_24dp; + sigColor = R.color.key_flag_red; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + case OpenPgpSignatureResult.RESULT_KEY_MISSING: { + sigText = R.string.decrypt_result_signature_missing_key; + sigIcon = R.drawable.status_signature_unknown_cutout_24dp; + sigColor = R.color.key_flag_red; + + sigActionText = R.string.decrypt_result_action_Lookup; + sigActionIcon = R.drawable.ic_file_download_grey_24dp; + break; + } + + case OpenPgpSignatureResult.RESULT_INVALID_INSECURE: { + sigText = R.string.decrypt_result_insecure_cryptography; + sigIcon = R.drawable.status_signature_invalid_cutout_24dp; + sigColor = R.color.key_flag_red; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + default: + case OpenPgpSignatureResult.RESULT_INVALID_SIGNATURE: { + sigText = R.string.decrypt_result_invalid_signature; + sigIcon = R.drawable.status_signature_invalid_cutout_24dp; + sigColor = R.color.key_flag_red; + + sigActionText = R.string.decrypt_result_action_show; + sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + break; + } + + } + + int sigColorRes = context.getResources().getColor(sigColor); + holder.getSignatureStatusIcon().setColorFilter(sigColorRes, PorterDuff.Mode.SRC_IN); + holder.getSignatureStatusIcon().setImageDrawable(context.getResources().getDrawable(sigIcon)); + holder.getSignatureStatusText().setText(sigText); + holder.getSignatureStatusText().setTextColor(sigColorRes); + + if (signatureResult.getResult() != OpenPgpSignatureResult.RESULT_NO_SIGNATURE) { + // has a signature, thus display layouts + + holder.getSignatureLayout().setVisibility(View.VISIBLE); + + holder.getSignatureAction().setText(sigActionText); + holder.getSignatureAction().setCompoundDrawablesWithIntrinsicBounds( + 0, 0, sigActionIcon, 0); + + String userId = result.getSignatureResult().getPrimaryUserId(); + KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId); + if (userIdSplit.name != null) { + holder.getSignatureUserName().setText(userIdSplit.name); + } else { + holder.getSignatureUserName().setText(R.string.user_id_no_name); + } + if (userIdSplit.email != null) { + holder.getSignatureUserEmail().setVisibility(View.VISIBLE); + holder.getSignatureUserEmail().setText(userIdSplit.email); + } else { + holder.getSignatureUserEmail().setVisibility(View.GONE); + } + + } else { + + holder.getSignatureLayout().setVisibility(View.GONE); + + } + + + } + /** * Sets status image based on constant */ + @SuppressWarnings("deprecation") // context.getDrawable is api lvl 21 public static void setStatusImage(Context context, ImageView statusIcon, TextView statusText, State state, int color, boolean big) { switch (state) { @@ -443,7 +632,7 @@ public class KeyFormattingUtils { context.getResources().getDrawable(R.drawable.status_signature_verified_cutout_24dp)); } if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.android_green_light; + color = R.color.key_flag_green; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); @@ -456,7 +645,7 @@ public class KeyFormattingUtils { statusIcon.setImageDrawable( context.getResources().getDrawable(R.drawable.status_lock_closed_24dp)); if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.android_green_light; + color = R.color.key_flag_green; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); @@ -475,7 +664,7 @@ public class KeyFormattingUtils { context.getResources().getDrawable(R.drawable.status_signature_unverified_cutout_24dp)); } if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.android_orange_light; + color = R.color.key_flag_orange; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); @@ -488,7 +677,7 @@ public class KeyFormattingUtils { statusIcon.setImageDrawable( context.getResources().getDrawable(R.drawable.status_signature_unknown_cutout_24dp)); if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.android_red_light; + color = R.color.key_flag_red; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); @@ -507,7 +696,7 @@ public class KeyFormattingUtils { context.getResources().getDrawable(R.drawable.status_signature_revoked_cutout_24dp)); } if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.android_red_light; + color = R.color.key_flag_red; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); @@ -525,7 +714,25 @@ public class KeyFormattingUtils { context.getResources().getDrawable(R.drawable.status_signature_expired_cutout_24dp)); } if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.android_red_light; + color = R.color.key_flag_red; + } + statusIcon.setColorFilter(context.getResources().getColor(color), + PorterDuff.Mode.SRC_IN); + if (statusText != null) { + statusText.setTextColor(context.getResources().getColor(color)); + } + break; + } + case INSECURE: { + if (big) { + statusIcon.setImageDrawable( + context.getResources().getDrawable(R.drawable.status_signature_invalid_cutout_96dp)); + } else { + statusIcon.setImageDrawable( + context.getResources().getDrawable(R.drawable.status_signature_invalid_cutout_24dp)); + } + if (color == KeyFormattingUtils.DEFAULT_COLOR) { + color = R.color.key_flag_red; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); @@ -538,7 +745,7 @@ public class KeyFormattingUtils { statusIcon.setImageDrawable( context.getResources().getDrawable(R.drawable.status_lock_open_24dp)); if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.android_red_light; + color = R.color.key_flag_red; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); @@ -551,7 +758,7 @@ public class KeyFormattingUtils { statusIcon.setImageDrawable( context.getResources().getDrawable(R.drawable.status_signature_unknown_cutout_24dp)); if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.android_red_light; + color = R.color.key_flag_red; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); @@ -564,7 +771,7 @@ public class KeyFormattingUtils { statusIcon.setImageDrawable( context.getResources().getDrawable(R.drawable.status_signature_invalid_cutout_24dp)); if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.android_red_light; + color = R.color.key_flag_red; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); @@ -578,7 +785,7 @@ public class KeyFormattingUtils { statusIcon.setImageDrawable( context.getResources().getDrawable(R.drawable.status_signature_invalid_cutout_24dp)); if (color == KeyFormattingUtils.DEFAULT_COLOR) { - color = R.color.bg_gray; + color = R.color.key_flag_gray; } statusIcon.setColorFilter(context.getResources().getColor(color), PorterDuff.Mode.SRC_IN); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java index 7e07ed818..7dfd56430 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java @@ -38,33 +38,25 @@ import org.sufficientlysecure.keychain.util.FabContainer; public class Notify { public static enum Style { - OK, WARN, ERROR; + OK (R.color.android_green_light), WARN(R.color.android_orange_light), ERROR(R.color.android_red_light); - public void applyToBar(Snackbar bar) { + public final int mLineColor; - switch (this) { - case OK: - // bar.actionColorResource(R.color.android_green_light); - bar.lineColorResource(R.color.android_green_light); - break; - case WARN: - // bar.textColorResource(R.color.android_orange_light); - bar.lineColorResource(R.color.android_orange_light); - break; - case ERROR: - // bar.textColorResource(R.color.android_red_light); - bar.lineColorResource(R.color.android_red_light); - break; - } + Style(int color) { + mLineColor = color; + } + public void applyToBar(Snackbar bar) { + bar.lineColorResource(mLineColor); } } public static final int LENGTH_INDEFINITE = 0; public static final int LENGTH_LONG = 3500; + public static final int LENGTH_SHORT = 1500; public static Showable create(final Activity activity, String text, int duration, Style style, - final ActionListener actionListener, int actionResId) { + final ActionListener actionListener, Integer actionResId) { final Snackbar snackbar = Snackbar.with(activity) .type(SnackbarType.MULTI_LINE) .text(text); @@ -77,14 +69,16 @@ public class Notify { style.applyToBar(snackbar); + if (actionResId != null) { + snackbar.actionLabel(actionResId); + } if (actionListener != null) { - snackbar.actionLabel(actionResId) - .actionListener(new ActionClickListener() { - @Override - public void onActionClicked(Snackbar snackbar) { - actionListener.onAction(); - } - }); + snackbar.actionListener(new ActionClickListener() { + @Override + public void onActionClicked(Snackbar snackbar) { + actionListener.onAction(); + } + }); } if (activity instanceof FabContainer) { @@ -108,6 +102,13 @@ public class Notify { } @Override + public void show(Fragment fragment, boolean animate) { + snackbar.animation(animate); + snackbar.dismissOnActionClicked(animate); + show(fragment); + } + + @Override public void show(Fragment fragment) { if (fragment != null) { View view = fragment.getView(); @@ -134,7 +135,7 @@ public class Notify { } public static Showable create(Activity activity, String text, int duration, Style style) { - return create(activity, text, duration, style, null, -1); + return create(activity, text, duration, style, null, null); } public static Showable create(Activity activity, String text, Style style) { @@ -159,24 +160,26 @@ public class Notify { /** * Shows the notification on the bottom of the Activity. */ - public void show(); + void show(); + + void show(Fragment fragment, boolean animate); /** * Shows the notification on the bottom of the Fragment. */ - public void show(Fragment fragment); + void show(Fragment fragment); /** * Shows the notification on the given ViewGroup. * The viewGroup should be either a RelativeLayout or FrameLayout. */ - public void show(ViewGroup viewGroup); + void show(ViewGroup viewGroup); } public interface ActionListener { - public void onAction(); + void onAction(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java index 5f71abdab..a6394a3fb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/QrCodeUtils.java @@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.ui.util; import android.graphics.Bitmap; import android.graphics.Color; +import android.net.Uri; import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; @@ -33,17 +34,24 @@ import org.sufficientlysecure.keychain.KeychainApplication; import org.sufficientlysecure.keychain.util.Log; import java.util.Hashtable; +import java.util.Locale; /** * Copied from Bitcoin Wallet */ public class QrCodeUtils { + public static Bitmap getQRCodeBitmap(final Uri uri, final int size) { + // for URIs we want alphanumeric encoding to save space, thus make everything upper case! + // zxing will then select Mode.ALPHANUMERIC internally + return getQRCodeBitmap(uri.toString().toUpperCase(Locale.ENGLISH), size); + } + /** * Generate Bitmap with QR Code based on input. * @return QR Code as Bitmap */ - public static Bitmap getQRCodeBitmap(final String input, final int size) { + private static Bitmap getQRCodeBitmap(final String input, final int size) { try { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java new file mode 100644 index 000000000..375483d89 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2015 Thialfihar <thi@thialfihar.org> + * + * 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.ui.util; + +import android.app.Activity; +import android.content.Context; +import android.view.ContextThemeWrapper; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Preferences; + +public class ThemeChanger { + private Activity mContext; + private Preferences mPreferences; + private String mCurrentTheme = null; + + private int mLightResId; + private int mDarkResId; + + static public ContextThemeWrapper getDialogThemeWrapper(Context context) { + Preferences preferences = Preferences.getPreferences(context); + + // if the dialog is displayed from the application class, design is missing. + // hack to get holo design (which is not automatically applied due to activity's + // Theme.NoDisplay) + if (Constants.Pref.Theme.DARK.equals(preferences.getTheme())) { + return new ContextThemeWrapper(context, R.style.Theme_Keychain_Dark); + } else { + return new ContextThemeWrapper(context, R.style.Theme_Keychain_Light); + } + } + + public void setThemes(int lightResId, int darkResId) { + mLightResId = lightResId; + mDarkResId = darkResId; + } + + public ThemeChanger(Activity context) { + mContext = context; + mPreferences = Preferences.getPreferences(mContext); + } + + /** + * Apply the theme set in preferences if it isn't equal to mCurrentTheme + * anymore or mCurrentTheme hasn't been set yet. + * If a new theme is applied in this method, then return true, so + * the caller can re-create the activity, if need be. + */ + public boolean changeTheme() { + String newTheme = mPreferences.getTheme(); + if (mCurrentTheme != null && mCurrentTheme.equals(newTheme)) { + return false; + } + + int themeId = mLightResId; + if (Constants.Pref.Theme.DARK.equals(newTheme)) { + themeId = mDarkResId; + } + + ContextThemeWrapper w = new ContextThemeWrapper(mContext, themeId); + mContext.getTheme().setTo(w.getTheme()); + mCurrentTheme = newTheme; + + return true; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java new file mode 100644 index 000000000..95199bcd5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/DividerItemDecoration.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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.ui.util.recyclerview; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + private static final int[] ATTRS = new int[]{ + android.R.attr.listDivider + }; + + public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; + + public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; + + private Drawable mDivider; + + private int mOrientation; + + public DividerItemDecoration(Context context, int orientation) { + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + setOrientation(orientation); + } + + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { + throw new IllegalArgumentException("invalid orientation"); + } + mOrientation = orientation; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + public void drawVertical(Canvas c, RecyclerView parent) { + final int left = parent.getPaddingLeft(); + final int right = parent.getWidth() - parent.getPaddingRight(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int top = child.getBottom() + params.bottomMargin; + final int bottom = top + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + public void drawHorizontal(Canvas c, RecyclerView parent) { + final int top = parent.getPaddingTop(); + final int bottom = parent.getHeight() - parent.getPaddingBottom(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int left = child.getRight() + params.rightMargin; + final int right = left + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/ItemTouchHelperAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/ItemTouchHelperAdapter.java new file mode 100644 index 000000000..c691182bf --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/ItemTouchHelperAdapter.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 Paul Burke + * + * 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.ui.util.recyclerview; + +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; + +/** + * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}. + */ +public interface ItemTouchHelperAdapter { + + /** + * Called when an item has been dragged far enough to trigger a move. This is called every time + * an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/> + * <br/> + * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after + * adjusting the underlying data to reflect this move. + * + * @param fromPosition The start position of the moved item. + * @param toPosition Then resolved position of the moved item. + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() + */ + void onItemMove(RecyclerView.ViewHolder source, RecyclerView.ViewHolder target, + int fromPosition, int toPosition); +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/ItemTouchHelperDragCallback.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/ItemTouchHelperDragCallback.java new file mode 100644 index 000000000..0fd24581d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/ItemTouchHelperDragCallback.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2015 Paul Burke + * + * 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.ui.util.recyclerview; + +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; + +/** + * An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and + * swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/> + * </br/> + * Expects the <code>RecyclerView.Adapter</code> to listen for {@link + * ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> to implement + * {@link ItemTouchHelperViewHolder}. + */ +public class ItemTouchHelperDragCallback extends ItemTouchHelper.Callback { + + private final ItemTouchHelperAdapter mAdapter; + + public ItemTouchHelperDragCallback(ItemTouchHelperAdapter adapter) { + mAdapter = adapter; + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + // Enable drag and swipe in both directions + final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; + final int swipeFlags = 0; + return makeMovementFlags(dragFlags, swipeFlags); + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, + RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType()) { + return false; + } + + // Notify the adapter of the move + mAdapter.onItemMove(source, target, source.getAdapterPosition(), target.getAdapterPosition()); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { + // we don't support swipe + } + + @Override + public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { + if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { + // Let the view holder know that this item is being moved or dragged + ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; + itemViewHolder.onItemSelected(); + } + + super.onSelectedChanged(viewHolder, actionState); + } + + @Override + public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + // Tell the view holder it's time to restore the idle state + ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; + itemViewHolder.onItemClear(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/ItemTouchHelperViewHolder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/ItemTouchHelperViewHolder.java new file mode 100644 index 000000000..97e70d71e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/ItemTouchHelperViewHolder.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 Paul Burke + * + * 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.ui.util.recyclerview; + +import android.support.v7.widget.helper.ItemTouchHelper; + +/** + * Interface to notify an item ViewHolder of relevant callbacks from {@link + * android.support.v7.widget.helper.ItemTouchHelper.Callback}. + */ +public interface ItemTouchHelperViewHolder { + + /** + * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped. + * Implementations should update the item view to indicate it's active state. + */ + void onItemSelected(); + + + /** + * Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item + * state should be cleared. + */ + void onItemClear(); +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerItemClickListener.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerItemClickListener.java new file mode 100644 index 000000000..7efcbb30c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerItemClickListener.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2014 Jacob Tabak + * + * 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.ui.util.recyclerview; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +/** + * based on http://stackoverflow.com/a/26196831/3000919 + */ +public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener { + private OnItemClickListener mListener; + private boolean mIgnoreTouch = false; + + public interface OnItemClickListener { + void onItemClick(View view, int position); + } + + GestureDetector mGestureDetector; + + public RecyclerItemClickListener(Context context, OnItemClickListener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return true; + } + }); + } + + @Override + public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) { + if (mIgnoreTouch) { + return false; + } + View childView = view.findChildViewUnder(e.getX(), e.getY()); + if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) { + mListener.onItemClick(childView, view.getChildAdapterPosition(childView)); + return true; + } + return false; + } + + @Override + public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { + // TODO: should we move mListener.onItemClick here + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + mIgnoreTouch = disallowIntercept; + } +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java index e5b3df8c9..6a51085f3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java @@ -25,14 +25,12 @@ import android.support.annotation.StringRes; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.util.AttributeSet; -import android.widget.ImageView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainDatabase; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; +import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; public class CertifyKeySpinner extends KeySpinner { private long mHiddenMasterKeyId = Constants.key.none; @@ -61,19 +59,9 @@ public class CertifyKeySpinner extends KeySpinner { // sample only has one Loader, so we don't care about the ID. Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri(); - // These are the rows that we will retrieve. - String[] projection = new String[]{ - KeychainContract.KeyRings._ID, - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.KEY_ID, - KeychainContract.KeyRings.USER_ID, - KeychainContract.KeyRings.IS_REVOKED, - KeychainContract.KeyRings.IS_EXPIRED, + String[] projection = KeyAdapter.getProjectionWith(new String[] { KeychainContract.KeyRings.HAS_CERTIFY, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID, - KeychainContract.KeyRings.CREATION - }; + }); String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1 AND " + KeychainDatabase.Tables.KEYS + "." + KeychainContract.KeyRings.MASTER_KEY_ID @@ -84,7 +72,7 @@ public class CertifyKeySpinner extends KeySpinner { return new CursorLoader(getContext(), baseUri, projection, where, null, null); } - private int mIndexHasCertify, mIndexIsRevoked, mIndexIsExpired; + private int mIndexHasCertify; @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { @@ -92,16 +80,13 @@ public class CertifyKeySpinner extends KeySpinner { if (loader.getId() == LOADER_ID) { mIndexHasCertify = data.getColumnIndex(KeychainContract.KeyRings.HAS_CERTIFY); - mIndexIsRevoked = data.getColumnIndex(KeychainContract.KeyRings.IS_REVOKED); - mIndexIsExpired = data.getColumnIndex(KeychainContract.KeyRings.IS_EXPIRED); // If: // - no key has been pre-selected (e.g. by SageSlinger) // - there are actually keys (not just "none" entry) // Then: // - select key that is capable of certifying, but only if there is only one key capable of it - mIsSingle = false; - if (mSelectedKeyId == Constants.key.none && mAdapter.getCount() > 1) { + if (mPreSelectedKeyId == Constants.key.none && mAdapter.getCount() > 1) { // preselect if key can certify int selection = -1; while (data.moveToNext()) { @@ -127,18 +112,19 @@ public class CertifyKeySpinner extends KeySpinner { } @Override - boolean setStatus(Context context, Cursor cursor, ImageView statusView) { - if (cursor.getInt(mIndexIsRevoked) != 0) { - KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.REVOKED, R.color.bg_gray); + boolean isItemEnabled(Cursor cursor) { + // "none" entry is always enabled! + if (cursor.getPosition() == 0) { + return true; + } + + if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) { return false; } - if (cursor.getInt(mIndexIsExpired) != 0) { - KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.EXPIRED, R.color.bg_gray); + if (cursor.getInt(KeyAdapter.INDEX_IS_EXPIRED) != 0) { return false; } - // don't invalidate the "None" entry, which is also null! - if (cursor.getPosition() != 0 && cursor.isNull(mIndexHasCertify)) { - KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.UNAVAILABLE, R.color.bg_gray); + if (cursor.isNull(mIndexHasCertify)) { return false; } @@ -146,6 +132,7 @@ public class CertifyKeySpinner extends KeySpinner { return true; } + @Override public @StringRes int getNoneString() { return R.string.choice_select_cert; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java deleted file mode 100644 index ec91b9fe4..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> - * - * 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.ui.widget; - -public interface Editor { - public interface EditorListener { - public void onDeleted(Editor editor, boolean wasNewItem); - public void onEdited(); - } - - public void setEditorListener(EditorListener listener); - public boolean needsSaving(); -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EmailEditText.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EmailEditText.java index e55f6b1ad..494ccb6d3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EmailEditText.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EmailEditText.java @@ -75,10 +75,10 @@ public class EmailEditText extends AppCompatAutoCompleteTextView { Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email); if (emailMatcher.matches()) { EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0, - R.drawable.uid_mail_ok, 0); + R.drawable.ic_stat_retyped_ok, 0); } else { EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0, - R.drawable.uid_mail_bad, 0); + R.drawable.ic_stat_retyped_bad, 0); } } else { // remove drawable if email is empty diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java index 63a1aade9..48e6c2cee 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java @@ -38,6 +38,7 @@ import com.tokenautocomplete.TokenCompleteTextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; @@ -126,7 +127,13 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView public Loader<Cursor> onCreateLoader(int id, Bundle args) { // These are the rows that we will retrieve. Uri baseUri = KeyRings.buildUnifiedKeyRingsUri(); - String where = KeyRings.HAS_ENCRYPT + " NOT NULL AND " + KeyRings.IS_EXPIRED + " = 0 AND " + + String[] projection = KeyAdapter.getProjectionWith(new String[] { + KeychainContract.KeyRings.HAS_ENCRYPT, + }); + + String where = KeyRings.HAS_ENCRYPT + " NOT NULL AND " + + KeyRings.IS_EXPIRED + " = 0 AND " + Tables.KEYS + "." + KeyRings.IS_REVOKED + " = 0"; if (args != null && args.containsKey(ARG_QUERY)) { @@ -135,12 +142,12 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView where += " AND " + KeyRings.USER_ID + " LIKE ?"; - return new CursorLoader(getContext(), baseUri, KeyAdapter.PROJECTION, where, + return new CursorLoader(getContext(), baseUri, projection, where, new String[]{"%" + query + "%"}, null); } mAdapter.setSearchQuery(null); - return new CursorLoader(getContext(), baseUri, KeyAdapter.PROJECTION, where, null, null); + return new CursorLoader(getContext(), baseUri, projection, where, null, null); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java deleted file mode 100644 index 3fd01958a..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> - * - * 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.ui.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.R; - -public class KeyServerEditor extends LinearLayout implements Editor, OnClickListener { - private EditorListener mEditorListener = null; - - ImageButton mDeleteButton; - TextView mServer; - - public KeyServerEditor(Context context) { - super(context); - } - - public KeyServerEditor(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - setDrawingCacheEnabled(true); - setAlwaysDrawnWithCacheEnabled(true); - - mServer = (TextView) findViewById(R.id.server); - - mDeleteButton = (ImageButton) findViewById(R.id.delete); - mDeleteButton.setOnClickListener(this); - - super.onFinishInflate(); - } - - public void setValue(String value) { - mServer.setText(value); - } - - public String getValue() { - return mServer.getText().toString().trim(); - } - - public void onClick(View v) { - final ViewGroup parent = (ViewGroup) getParent(); - if (v == mDeleteButton) { - parent.removeView(this); - if (mEditorListener != null) { - mEditorListener.onDeleted(this, false); - } - } - } - - @Override - public boolean needsSaving() { - return false; - } - - public void setEditorListener(EditorListener listener) { - mEditorListener = listener; - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java index 61b7c718b..e3ec3d34b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java @@ -17,41 +17,48 @@ package org.sufficientlysecure.keychain.ui.widget; + import android.content.Context; import android.database.Cursor; -import android.graphics.Color; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; -import android.support.v4.widget.CursorAdapter; import android.support.v7.widget.AppCompatSpinner; -import android.text.format.DateUtils; import android.util.AttributeSet; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; -import android.widget.ImageView; import android.widget.SpinnerAdapter; import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; +import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; + /** * Use AppCompatSpinner from AppCompat lib instead of Spinner. Fixes white dropdown icon. * Related: http://stackoverflow.com/a/27713090 */ -public abstract class KeySpinner extends AppCompatSpinner implements LoaderManager.LoaderCallbacks<Cursor> { +public abstract class KeySpinner extends AppCompatSpinner implements + LoaderManager.LoaderCallbacks<Cursor> { + + public static final String ARG_SUPER_STATE = "super_state"; + public static final String ARG_KEY_ID = "key_id"; + public interface OnKeyChangedListener { - public void onKeyChanged(long masterKeyId); + void onKeyChanged(long masterKeyId); } - protected long mSelectedKeyId = Constants.key.none; + protected long mPreSelectedKeyId = Constants.key.none; protected SelectKeyAdapter mAdapter = new SelectKeyAdapter(); protected OnKeyChangedListener mListener; @@ -79,7 +86,8 @@ public abstract class KeySpinner extends AppCompatSpinner implements LoaderManag @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (mListener != null) { - mListener.onKeyChanged(id); + long keyId = getSelectedKeyId(getItemAtPosition(position)); + mListener.onKeyChanged(keyId); } } @@ -111,7 +119,8 @@ public abstract class KeySpinner extends AppCompatSpinner implements LoaderManag if (getContext() instanceof FragmentActivity) { ((FragmentActivity) getContext()).getSupportLoaderManager().restartLoader(LOADER_ID, null, this); } else { - Log.e(Constants.TAG, "KeySpinner must be attached to FragmentActivity, this is " + getContext().getClass()); + throw new AssertionError("KeySpinner must be attached to FragmentActivity, this is " + + getContext().getClass()); } } @@ -129,172 +138,134 @@ public abstract class KeySpinner extends AppCompatSpinner implements LoaderManag } } - public void setSelectedKeyId(long selectedKeyId) { - this.mSelectedKeyId = selectedKeyId; + public long getSelectedKeyId() { + Object item = getSelectedItem(); + return getSelectedKeyId(item); + } + + public long getSelectedKeyId(Object item) { + if (item instanceof KeyItem) { + return ((KeyItem) item).mKeyId; + } + return Constants.key.none; + } + + public void setPreSelectedKeyId(long selectedKeyId) { + mPreSelectedKeyId = selectedKeyId; } protected class SelectKeyAdapter extends BaseAdapter implements SpinnerAdapter { - private CursorAdapter inner; - private int mIndexUserId; - private int mIndexDuplicate; + private KeyAdapter inner; private int mIndexMasterKeyId; - private int mIndexCreationDate; public SelectKeyAdapter() { - inner = new CursorAdapter(getContext(), null, 0) { - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return View.inflate(getContext(), R.layout.keyspinner_item, null); - } + inner = new KeyAdapter(getContext(), null, 0) { @Override - public void bindView(View view, Context context, Cursor cursor) { - TextView vKeyName = (TextView) view.findViewById(R.id.keyspinner_key_name); - ImageView vKeyStatus = (ImageView) view.findViewById(R.id.keyspinner_key_status); - TextView vKeyEmail = (TextView) view.findViewById(R.id.keyspinner_key_email); - TextView vDuplicate = (TextView) view.findViewById(R.id.keyspinner_duplicate); - - KeyRing.UserId userId = KeyRing.splitUserId(cursor.getString(mIndexUserId)); - vKeyName.setText(userId.name); - vKeyEmail.setText(userId.email); - - boolean duplicate = cursor.getLong(mIndexDuplicate) > 0; - if (duplicate) { - String dateTime = DateUtils.formatDateTime(context, - cursor.getLong(mIndexCreationDate) * 1000, - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_YEAR - | DateUtils.FORMAT_ABBREV_MONTH); - - vDuplicate.setText(context.getString(R.string.label_key_created, dateTime)); - vDuplicate.setVisibility(View.VISIBLE); - } else { - vDuplicate.setVisibility(View.GONE); - } - - boolean valid = setStatus(getContext(), cursor, vKeyStatus); - setItemEnabled(view, valid); + public boolean isEnabled(Cursor cursor) { + return KeySpinner.this.isItemEnabled(cursor); } - @Override - public long getItemId(int position) { - try { - return ((Cursor) getItem(position)).getLong(mIndexMasterKeyId); - } catch (Exception e) { - // This can happen on concurrent modification :( - return Constants.key.none; - } - } }; } - private void setItemEnabled(View view, boolean enabled) { - TextView vKeyName = (TextView) view.findViewById(R.id.keyspinner_key_name); - ImageView vKeyStatus = (ImageView) view.findViewById(R.id.keyspinner_key_status); - TextView vKeyEmail = (TextView) view.findViewById(R.id.keyspinner_key_email); - TextView vKeyDuplicate = (TextView) view.findViewById(R.id.keyspinner_duplicate); - - if (enabled) { - vKeyName.setTextColor(Color.BLACK); - vKeyEmail.setTextColor(Color.BLACK); - vKeyDuplicate.setTextColor(Color.BLACK); - vKeyStatus.setVisibility(View.GONE); - view.setClickable(false); - } else { - vKeyName.setTextColor(Color.GRAY); - vKeyEmail.setTextColor(Color.GRAY); - vKeyDuplicate.setTextColor(Color.GRAY); - vKeyStatus.setVisibility(View.VISIBLE); - // this is a HACK. the trick is, if the element itself is clickable, the - // click is not passed on to the view list - view.setClickable(true); - } - } - public Cursor swapCursor(Cursor newCursor) { if (newCursor == null) return inner.swapCursor(null); - mIndexDuplicate = newCursor.getColumnIndex(KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID); - mIndexUserId = newCursor.getColumnIndex(KeychainContract.KeyRings.USER_ID); mIndexMasterKeyId = newCursor.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID); - mIndexCreationDate = newCursor.getColumnIndex(KeychainContract.KeyRings.CREATION); - // pre-select key if mSelectedKeyId is given - if (mSelectedKeyId != Constants.key.none && newCursor.moveToFirst()) { + Cursor oldCursor = inner.swapCursor(newCursor); + + // pre-select key if mPreSelectedKeyId is given + if (mPreSelectedKeyId != Constants.key.none && newCursor.moveToFirst()) { do { - if (newCursor.getLong(mIndexMasterKeyId) == mSelectedKeyId) { - setSelection(newCursor.getPosition() + 1); + if (newCursor.getLong(mIndexMasterKeyId) == mPreSelectedKeyId) { + setSelection(newCursor.getPosition() +1); } } while (newCursor.moveToNext()); } - return inner.swapCursor(newCursor); + return oldCursor; } @Override public int getCount() { - return inner.getCount() + 1; + return inner.getCount() +1; } @Override - public Object getItem(int position) { - if (position == 0) return null; - return inner.getItem(position - 1); + public KeyItem getItem(int position) { + if (position == 0) { + return null; + } + return inner.getItem(position -1); } @Override public long getItemId(int position) { - if (position == 0) return Constants.key.none; - return inner.getItemId(position - 1); + if (position == 0) { + return Constants.key.none; + } + return inner.getItemId(position -1); } @Override public View getView(int position, View convertView, ViewGroup parent) { - try { - View v = getDropDownView(position, convertView, parent); - v.findViewById(R.id.keyspinner_key_email).setVisibility(View.GONE); - return v; - } catch (NullPointerException e) { - // This is for the preview... - return View.inflate(getContext(), android.R.layout.simple_list_item_1, null); - } - } - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - View view; - if (position == 0) { - if (convertView == null) { - view = inner.newView(null, null, parent); - } else { - view = convertView; + // Unfortunately, SpinnerAdapter does not support multiple view + // types. For this reason, we throw away convertViews of a bad + // type. This is sort of a hack, but since the number of elements + // we deal with in KeySpinners is usually very small (number of + // secret keys), this is the easiest solution. (I'm sorry.) + if (convertView != null) { + // This assumes that the inner view has non-null tags on its views! + boolean isWrongType = (convertView.getTag() == null) != (position == 0); + if (isWrongType) { + convertView = null; } - TextView vKeyName = (TextView) view.findViewById(R.id.keyspinner_key_name); - ImageView vKeyStatus = (ImageView) view.findViewById(R.id.keyspinner_key_status); - TextView vKeyEmail = (TextView) view.findViewById(R.id.keyspinner_key_email); - TextView vKeyDuplicate = (TextView) view.findViewById(R.id.keyspinner_duplicate); - - vKeyName.setText(getNoneString()); - vKeyEmail.setVisibility(View.GONE); - vKeyDuplicate.setVisibility(View.GONE); - vKeyStatus.setVisibility(View.GONE); - setItemEnabled(view, true); - } else { - view = inner.getView(position - 1, convertView, parent); - TextView vKeyEmail = (TextView) view.findViewById(R.id.keyspinner_key_email); - vKeyEmail.setVisibility(View.VISIBLE); } + + if (position > 0) { + return inner.getView(position -1, convertView, parent); + } + + View view = convertView != null ? convertView : + LayoutInflater.from(getContext()).inflate( + R.layout.keyspinner_item_none, parent, false); + ((TextView) view.findViewById(R.id.keyspinner_key_name)).setText(getNoneString()); return view; } } - boolean setStatus(Context context, Cursor cursor, ImageView statusView) { + boolean isItemEnabled(Cursor cursor) { return true; } + @Override + public void onRestoreInstanceState(Parcelable state) { + Bundle bundle = (Bundle) state; + + mPreSelectedKeyId = bundle.getLong(ARG_KEY_ID); + + // restore super state + super.onRestoreInstanceState(bundle.getParcelable(ARG_SUPER_STATE)); + + } + + @NonNull + @Override + public Parcelable onSaveInstanceState() { + Bundle bundle = new Bundle(); + + // save super state + bundle.putParcelable(ARG_SUPER_STATE, super.onSaveInstanceState()); + + bundle.putLong(ARG_KEY_ID, getSelectedKeyId()); + return bundle; + } + public @StringRes int getNoneString() { - return R.string.choice_none; + return R.string.cert_none; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PasswordStrengthView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PasswordStrengthView.java index 1487c3053..a7ead8039 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PasswordStrengthView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PasswordStrengthView.java @@ -31,7 +31,9 @@ import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; +import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; /** * Created by Matt Allen @@ -97,26 +99,23 @@ public class PasswordStrengthView extends View { public PasswordStrengthView(Context context, AttributeSet attrs) { super(context, attrs); - int COLOR_FAIL = context.getResources().getColor(R.color.android_red_light); - int COLOR_WEAK = context.getResources().getColor(R.color.android_orange_light); - int COLOR_STRONG = context.getResources().getColor(R.color.android_green_light); + int COLOR_FAIL = getResources().getColor(R.color.password_strength_low); + int COLOR_WEAK = getResources().getColor(R.color.password_strength_medium); + int COLOR_STRONG = getResources().getColor(R.color.password_strength_high); TypedArray style = context.getTheme().obtainStyledAttributes( attrs, R.styleable.PasswordStrengthView, 0, 0); - try { - mStrengthRequirement = style.getInteger(R.styleable.PasswordStrengthView_strength, - STRENGTH_MEDIUM); - mShowGuides = style.getBoolean(R.styleable.PasswordStrengthView_showGuides, true); - mColorFail = style.getColor(R.styleable.PasswordStrengthView_color_fail, COLOR_FAIL); - mColorWeak = style.getColor(R.styleable.PasswordStrengthView_color_weak, COLOR_WEAK); - mColorStrong = style.getColor(R.styleable.PasswordStrengthView_color_strong, - COLOR_STRONG); - } catch (Exception e) { - e.printStackTrace(); - } + mStrengthRequirement = style.getInteger(R.styleable.PasswordStrengthView_strength, + STRENGTH_MEDIUM); + mShowGuides = style.getBoolean(R.styleable.PasswordStrengthView_showGuides, true); + mColorFail = style.getColor(R.styleable.PasswordStrengthView_color_fail, COLOR_FAIL); + mColorWeak = style.getColor(R.styleable.PasswordStrengthView_color_weak, COLOR_WEAK); + mColorStrong = style.getColor(R.styleable.PasswordStrengthView_color_strong, + COLOR_STRONG); + // Create and style the paint used for drawing the guide on the indicator mGuidePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mGuidePaint.setStyle(Paint.Style.FILL_AND_STROKE); @@ -124,6 +123,9 @@ public class PasswordStrengthView extends View { // Create and style paint for indicator mIndicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mIndicatorPaint.setStyle(Paint.Style.FILL); + + style.recycle(); + } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java index df7347fa4..8fb9e38aa 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.ui.widget; + import android.content.Context; import android.database.Cursor; import android.net.Uri; @@ -24,12 +25,9 @@ import android.os.Bundle; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.util.AttributeSet; -import android.widget.ImageView; -import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; +import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; public class SignKeySpinner extends KeySpinner { public SignKeySpinner(Context context) { @@ -50,19 +48,9 @@ public class SignKeySpinner extends KeySpinner { // sample only has one Loader, so we don't care about the ID. Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingsUri(); - // These are the rows that we will retrieve. - String[] projection = new String[]{ - KeychainContract.KeyRings._ID, - KeychainContract.KeyRings.MASTER_KEY_ID, - KeychainContract.KeyRings.KEY_ID, - KeychainContract.KeyRings.USER_ID, - KeychainContract.KeyRings.IS_REVOKED, - KeychainContract.KeyRings.IS_EXPIRED, + String[] projection = KeyAdapter.getProjectionWith(new String[] { KeychainContract.KeyRings.HAS_SIGN, - KeychainContract.KeyRings.HAS_ANY_SECRET, - KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID, - KeychainContract.KeyRings.CREATION - }; + }); String where = KeychainContract.KeyRings.HAS_ANY_SECRET + " = 1"; @@ -71,7 +59,7 @@ public class SignKeySpinner extends KeySpinner { return new CursorLoader(getContext(), baseUri, projection, where, null, null); } - private int mIndexHasSign, mIndexIsRevoked, mIndexIsExpired; + private int mIndexHasSign; @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { @@ -79,23 +67,23 @@ public class SignKeySpinner extends KeySpinner { if (loader.getId() == LOADER_ID) { mIndexHasSign = data.getColumnIndex(KeychainContract.KeyRings.HAS_SIGN); - mIndexIsRevoked = data.getColumnIndex(KeychainContract.KeyRings.IS_REVOKED); - mIndexIsExpired = data.getColumnIndex(KeychainContract.KeyRings.IS_EXPIRED); } } @Override - boolean setStatus(Context context, Cursor cursor, ImageView statusView) { - if (cursor.getInt(mIndexIsRevoked) != 0) { - KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.REVOKED, R.color.bg_gray); + boolean isItemEnabled(Cursor cursor) { + // "none" entry is always enabled! + if (cursor.getPosition() == 0) { + return true; + } + + if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) { return false; } - if (cursor.getInt(mIndexIsExpired) != 0) { - KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.EXPIRED, R.color.bg_gray); + if (cursor.getInt(KeyAdapter.INDEX_IS_EXPIRED) != 0) { return false; } - if (cursor.getInt(mIndexHasSign) == 0) { - KeyFormattingUtils.setStatusImage(getContext(), statusView, null, State.UNAVAILABLE, R.color.bg_gray); + if (cursor.isNull(mIndexHasSign)) { return false; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java new file mode 100644 index 000000000..18e830139 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <look@my.amazin.horse> + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.sufficientlysecure.keychain.ui.widget; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ViewAnimator; + +import org.sufficientlysecure.keychain.R; + + +/** This view is essentially identical to ViewAnimator, but allows specifying the initial view + * for preview as an xml attribute. */ +public class ToolableViewAnimator extends ViewAnimator { + + private int mInitChild = -1; + + public ToolableViewAnimator(Context context) { + super(context); + } + + public ToolableViewAnimator(Context context, AttributeSet attrs) { + super(context, attrs); + + if (isInEditMode()) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ToolableViewAnimator); + mInitChild = a.getInt(R.styleable.ToolableViewAnimator_initialView, -1); + a.recycle(); + } + } + + public ToolableViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs); + + if (isInEditMode()) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ToolableViewAnimator, defStyleAttr, 0); + mInitChild = a.getInt(R.styleable.ToolableViewAnimator_initialView, -1); + a.recycle(); + } + } + + @Override + public void addView(@NonNull View child, int index, ViewGroup.LayoutParams params) { + if (isInEditMode() && mInitChild-- > 0) { + return; + } + super.addView(child, index, params); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java index e1efd5abc..77aa1a055 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java @@ -303,10 +303,13 @@ public class ContactHelper { Cursor contactMasterKey = context.getContentResolver().query(contactUri, new String[]{ContactsContract.Data.DATA2}, null, null, null); if (contactMasterKey != null) { - if (contactMasterKey.moveToNext()) { - return KeychainContract.KeyRings.buildGenericKeyRingUri(contactMasterKey.getLong(0)); + try { + if (contactMasterKey.moveToNext()) { + return KeychainContract.KeyRings.buildGenericKeyRingUri(contactMasterKey.getLong(0)); + } + } finally { + contactMasterKey.close(); } - contactMasterKey.close(); } return null; } @@ -537,7 +540,7 @@ public class ContactHelper { KEYS_TO_CONTACT_PROJECTION, KeychainContract.KeyRings.HAS_ANY_SECRET + "!=0", null, null); - if (cursor != null) { + if (cursor != null) try { while (cursor.moveToNext()) { long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); boolean isExpired = cursor.getInt(INDEX_IS_EXPIRED) != 0; @@ -565,6 +568,8 @@ public class ContactHelper { } } } + } finally { + cursor.close(); } for (long masterKeyId : keysToDelete) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java index 8334b37ec..d7491ab26 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java @@ -18,16 +18,16 @@ package org.sufficientlysecure.keychain.util; import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Messenger; import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; import org.sufficientlysecure.keychain.keyimport.Keyserver; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; -import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import java.net.Proxy; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -35,27 +35,40 @@ import java.util.Locale; import java.util.Set; public class EmailKeyHelper { + // TODO: Make this not require a proxy in it's constructor, redesign when it is to be used + // to import keys, simply use CryptoOperationHelper with this callback + public abstract class ImportContactKeysCallback + implements CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> { - public static void importContacts(Context context, Messenger messenger) { - importAll(context, messenger, ContactHelper.getContactMails(context)); - } + private ArrayList<ParcelableKeyRing> mKeyList; + private String mKeyserver; - public static void importAll(Context context, Messenger messenger, List<String> mails) { - // Collect all candidates as ImportKeysListEntry (set for deduplication) - Set<ImportKeysListEntry> entries = new HashSet<>(); - for (String mail : mails) { - entries.addAll(getEmailKeys(context, mail)); + public ImportContactKeysCallback(Context context, String keyserver, Proxy proxy) { + this(context, ContactHelper.getContactMails(context), keyserver, proxy); } - // Put them in a list and import - ArrayList<ParcelableKeyRing> keys = new ArrayList<>(entries.size()); - for (ImportKeysListEntry entry : entries) { - keys.add(new ParcelableKeyRing(entry.getFingerprintHex(), entry.getKeyIdHex(), null)); + public ImportContactKeysCallback(Context context, List<String> mails, String keyserver, + Proxy proxy) { + Set<ImportKeysListEntry> entries = new HashSet<>(); + for (String mail : mails) { + entries.addAll(getEmailKeys(context, mail, proxy)); + } + + // Put them in a list and import + ArrayList<ParcelableKeyRing> keys = new ArrayList<>(entries.size()); + for (ImportKeysListEntry entry : entries) { + keys.add(new ParcelableKeyRing(entry.getFingerprintHex(), entry.getKeyIdHex(), null)); + } + mKeyList = keys; + mKeyserver = keyserver; + } + @Override + public ImportKeyringParcel createOperationInput() { + return new ImportKeyringParcel(mKeyList, mKeyserver); } - importKeys(context, messenger, keys); } - public static Set<ImportKeysListEntry> getEmailKeys(Context context, String mail) { + public static Set<ImportKeysListEntry> getEmailKeys(Context context, String mail, Proxy proxy) { Set<ImportKeysListEntry> keys = new HashSet<>(); // Try _hkp._tcp SRV record first @@ -63,7 +76,7 @@ public class EmailKeyHelper { if (mailparts.length == 2) { HkpKeyserver hkp = HkpKeyserver.resolve(mailparts[1]); if (hkp != null) { - keys.addAll(getEmailKeys(mail, hkp)); + keys.addAll(getEmailKeys(mail, hkp, proxy)); } } @@ -72,27 +85,17 @@ public class EmailKeyHelper { String server = Preferences.getPreferences(context).getPreferredKeyserver(); if (server != null) { HkpKeyserver hkp = new HkpKeyserver(server); - keys.addAll(getEmailKeys(mail, hkp)); + keys.addAll(getEmailKeys(mail, hkp, proxy)); } } return keys; } - private static void importKeys(Context context, Messenger messenger, ArrayList<ParcelableKeyRing> keys) { - Intent importIntent = new Intent(context, KeychainIntentService.class); - importIntent.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); - Bundle importData = new Bundle(); - importData.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, keys); - importIntent.putExtra(KeychainIntentService.EXTRA_DATA, importData); - importIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - - context.startService(importIntent); - } - - public static List<ImportKeysListEntry> getEmailKeys(String mail, Keyserver keyServer) { + public static List<ImportKeysListEntry> getEmailKeys(String mail, Keyserver keyServer, + Proxy proxy) { Set<ImportKeysListEntry> keys = new HashSet<>(); try { - for (ImportKeysListEntry key : keyServer.search(mail)) { + for (ImportKeysListEntry key : keyServer.search(mail, proxy)) { if (key.isRevoked() || key.isExpired()) continue; for (String userId : key.getUserIds()) { if (userId.toLowerCase().contains(mail.toLowerCase(Locale.ENGLISH))) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java index 7efb7c5af..45dc33906 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java @@ -17,41 +17,40 @@ package org.sufficientlysecure.keychain.util; -import android.app.ProgressDialog; + +import java.io.File; + import android.content.Intent; -import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; +import android.net.Uri; import android.support.v4.app.FragmentActivity; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.ExportResult; -import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; -import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.service.ExportKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; -import java.io.File; - -public class ExportHelper { +public class ExportHelper + implements CryptoOperationHelper.Callback <ExportKeyringParcel, ExportResult> { protected File mExportFile; FragmentActivity mActivity; + private boolean mExportSecret; + private long[] mMasterKeyIds; + public ExportHelper(FragmentActivity activity) { super(); this.mActivity = activity; } - /** - * Show dialog where to export keys - */ - public void showExportKeysDialog(final long[] masterKeyIds, final File exportFile, - final boolean showSecretCheckbox) { + /** Show dialog where to export keys */ + public void showExportKeysDialog(final Long masterKeyId, final File exportFile, + final boolean exportSecret) { mExportFile = exportFile; - String title = null; - if (masterKeyIds == null) { + String title; + if (masterKeyId == null) { // export all keys title = mActivity.getString(R.string.title_export_keys); } else { @@ -59,72 +58,67 @@ public class ExportHelper { title = mActivity.getString(R.string.title_export_key); } - String message = mActivity.getString(R.string.specify_file_to_export_to); - String checkMsg = showSecretCheckbox ? - mActivity.getString(R.string.also_export_secret_keys) : null; + String message; + if (exportSecret) { + message = mActivity.getString(masterKeyId == null + ? R.string.specify_backup_dest_secret + : R.string.specify_backup_dest_secret_single); + } else { + message = mActivity.getString(masterKeyId == null + ? R.string.specify_backup_dest + : R.string.specify_backup_dest_single); + } - FileHelper.saveFile(new FileHelper.FileDialogCallback() { + FileHelper.saveDocumentDialog(new FileHelper.FileDialogCallback() { @Override public void onFileSelected(File file, boolean checked) { mExportFile = file; - exportKeys(masterKeyIds, checked); + exportKeys(masterKeyId == null ? null : new long[] { masterKeyId }, exportSecret); } - }, mActivity.getSupportFragmentManager() ,title, message, exportFile, checkMsg); + }, mActivity.getSupportFragmentManager(), title, message, exportFile, null); } + // TODO: If ExportHelper requires pending data (see CryptoOPerationHelper), activities using + // TODO: this class should be able to call mExportOpHelper.handleActivity + /** * Export keys */ public void exportKeys(long[] masterKeyIds, boolean exportSecret) { Log.d(Constants.TAG, "exportKeys started"); + mExportSecret = exportSecret; + mMasterKeyIds = masterKeyIds; // if masterKeyIds is null it means export all - // Send all information needed to service to export key in other thread - final Intent intent = new Intent(mActivity, KeychainIntentService.class); - - intent.setAction(KeychainIntentService.ACTION_EXPORT_KEYRING); - - // fill values for this action - Bundle data = new Bundle(); - - data.putString(KeychainIntentService.EXPORT_FILENAME, mExportFile.getAbsolutePath()); - data.putBoolean(KeychainIntentService.EXPORT_SECRET, exportSecret); - - if (masterKeyIds == null) { - data.putBoolean(KeychainIntentService.EXPORT_ALL, true); - } else { - data.putLongArray(KeychainIntentService.EXPORT_KEY_RING_MASTER_KEY_ID, masterKeyIds); - } - - intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - - // Message is received after exporting is done in KeychainIntentService - ServiceProgressHandler exportHandler = new ServiceProgressHandler(mActivity, - mActivity.getString(R.string.progress_exporting), - ProgressDialog.STYLE_HORIZONTAL, - ProgressDialogFragment.ServiceType.KEYCHAIN_INTENT) { - public void handleMessage(Message message) { - // handle messages by standard KeychainIntentServiceHandler first - super.handleMessage(message); + CryptoOperationHelper<ExportKeyringParcel, ExportResult> exportOpHelper = + new CryptoOperationHelper<>(1, mActivity, this, R.string.progress_exporting); + exportOpHelper.cryptoOperation(); + } - if (message.arg1 == MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle data = message.getData(); + @Override + public ExportKeyringParcel createOperationInput() { + return new ExportKeyringParcel(mMasterKeyIds, mExportSecret, mExportFile.getAbsolutePath()); + } - ExportResult result = data.getParcelable(ExportResult.EXTRA_RESULT); - result.createNotify(mActivity).show(); - } - } - }; + @Override + final public void onCryptoOperationSuccess(ExportResult result) { + // trigger scan of the created 'media' file so it shows up on MTP + // http://stackoverflow.com/questions/13737261/nexus-4-not-showing-files-via-mtp + mActivity.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mExportFile))); + result.createNotify(mActivity).show(); + } - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(exportHandler); - intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + @Override + public void onCryptoOperationCancelled() { - // show progress dialog - exportHandler.showProgressDialog(mActivity); + } - // start service with intent - mActivity.startService(intent); + @Override + public void onCryptoOperationError(ExportResult result) { + result.createNotify(mActivity).show(); } + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java index 677acb1b8..9fb362412 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java @@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.util; import android.annotation.TargetApi; import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -27,55 +28,116 @@ import android.graphics.Bitmap; import android.graphics.Point; import android.net.Uri; import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.os.Messenger; import android.provider.DocumentsContract; import android.provider.OpenableColumns; +import android.support.annotation.StringRes; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.widget.Toast; +import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.text.DecimalFormat; + +/** This class offers a number of helper functions for saving documents. + * + * There are three entry points here: openDocument, saveDocument and + * saveDocumentDialog. Each behaves a little differently depending on whether + * the Android version used is pre or post KitKat. + * + * - openDocument queries for a document for reading. Used in "open encrypted + * file" ui flow. On pre-kitkat, this relies on an external file manager, + * and will fail with a toast message if none is installed. + * + * - saveDocument queries for a document name for saving. on pre-kitkat, this + * shows a dialog where a filename can be input. on kitkat and up, it + * directly triggers a "save document" intent. Used in "save encrypted file" + * ui flow. + * + * - saveDocumentDialog queries for a document. this shows a dialog on all + * versions of android. the browse button opens an external browser on + * pre-kitkat or the "save document" intent on post-kitkat devices. Used in + * "backup key" ui flow. + * + * It is noteworthy that the "saveDocument" call is essentially substituted + * by the "saveDocumentDialog" on pre-kitkat devices. + * + */ public class FileHelper { - /** - * Checks if external storage is mounted if file is located on external storage - * - * @param file - * @return true if storage is mounted - */ - public static boolean isStorageMounted(String file) { - if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) { - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - return false; - } + public static void openDocument(Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + openDocumentPreKitKat(fragment, last, mimeType, multiple, requestCode); + } else { + openDocumentKitKat(fragment, mimeType, multiple, requestCode); } + } - return true; + public static void saveDocument(Fragment fragment, String targetName, Uri inputUri, + @StringRes int title, @StringRes int message, int requestCode) { + saveDocument(fragment, targetName, inputUri, "*/*", title, message, requestCode); } - /** - * Opens the preferred installed file manager on Android and shows a toast if no manager is - * installed. - * - * @param fragment - * @param last default selected Uri, not supported by all file managers - * @param mimeType can be text/plain for example - * @param requestCode requestCode used to identify the result coming back from file manager to - * onActivityResult() in your activity - */ - public static void openFile(Fragment fragment, Uri last, String mimeType, int requestCode) { + public static void saveDocument(Fragment fragment, String targetName, Uri inputUri, String mimeType, + @StringRes int title, @StringRes int message, int requestCode) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + saveDocumentDialog(fragment, targetName, inputUri, title, message, requestCode); + } else { + saveDocumentKitKat(fragment, mimeType, targetName, requestCode); + } + } + + public static void saveDocumentDialog(final Fragment fragment, String targetName, Uri inputUri, + @StringRes int title, @StringRes int message, final int requestCode) { + + saveDocumentDialog(fragment, targetName, inputUri, title, message, new FileDialogCallback() { + // is this a good idea? seems hacky... + @Override + public void onFileSelected(File file, boolean checked) { + Intent intent = new Intent(); + intent.setData(Uri.fromFile(file)); + fragment.onActivityResult(requestCode, Activity.RESULT_OK, intent); + } + }); + } + + public static void saveDocumentDialog(final Fragment fragment, String targetName, Uri inputUri, + @StringRes int title, @StringRes int message, FileDialogCallback callback) { + + File file = inputUri == null ? null : new File(inputUri.getPath()); + File parentDir = file != null && file.exists() ? file.getParentFile() : Constants.Path.APP_DIR; + File targetFile = new File(parentDir, targetName); + saveDocumentDialog(callback, fragment.getActivity().getSupportFragmentManager(), + fragment.getString(title), fragment.getString(message), targetFile, null); + + } + + /** Opens the preferred installed file manager on Android and shows a toast + * if no manager is installed. */ + private static void openDocumentPreKitKat( + Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); - + if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple); + } intent.setData(last); intent.setType(mimeType); @@ -86,11 +148,34 @@ public class FileHelper { Toast.makeText(fragment.getActivity(), R.string.no_filemanager_installed, Toast.LENGTH_SHORT).show(); } + } - public static void saveFile(final FileDialogCallback callback, final FragmentManager fragmentManager, - final String title, final String message, final File defaultFile, - final String checkMsg) { + /** Opens the storage browser on Android 4.4 or later for opening a file */ + @TargetApi(Build.VERSION_CODES.KITKAT) + private static void openDocumentKitKat(Fragment fragment, String mimeType, boolean multiple, int requestCode) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(mimeType); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple); + fragment.startActivityForResult(intent, requestCode); + } + + /** Opens the storage browser on Android 4.4 or later for saving a file. */ + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void saveDocumentKitKat(Fragment fragment, String mimeType, String suggestedName, int requestCode) { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(mimeType); + intent.putExtra("android.content.extra.SHOW_ADVANCED", true); // Note: This is not documented, but works + intent.putExtra(Intent.EXTRA_TITLE, suggestedName); + fragment.startActivityForResult(intent, requestCode); + } + + public static void saveDocumentDialog( + final FileDialogCallback callback, final FragmentManager fragmentManager, + final String title, final String message, final File defaultFile, + final String checkMsg) { // Message is received after file is selected Handler returnHandler = new Handler() { @Override @@ -117,63 +202,6 @@ public class FileHelper { }); } - public static void saveFile(Fragment fragment, String title, String message, File defaultFile, int requestCode) { - saveFile(fragment, title, message, defaultFile, requestCode, null); - } - - public static void saveFile(final Fragment fragment, String title, String message, File defaultFile, - final int requestCode, String checkMsg) { - saveFile(new FileDialogCallback() { - @Override - public void onFileSelected(File file, boolean checked) { - Intent intent = new Intent(); - intent.setData(Uri.fromFile(file)); - fragment.onActivityResult(requestCode, Activity.RESULT_OK, intent); - } - }, fragment.getActivity().getSupportFragmentManager(), title, message, defaultFile, checkMsg); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void openDocument(Fragment fragment, String mimeType, int requestCode) { - openDocument(fragment, mimeType, false, requestCode); - } - - /** - * Opens the storage browser on Android 4.4 or later for opening a file - * - * @param fragment - * @param mimeType can be text/plain for example - * @param multiple allow file chooser to return multiple files - * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your - */ - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void openDocument(Fragment fragment, String mimeType, boolean multiple, int requestCode) { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType(mimeType); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple); - - fragment.startActivityForResult(intent, requestCode); - } - - /** - * Opens the storage browser on Android 4.4 or later for saving a file - * - * @param fragment - * @param mimeType can be text/plain for example - * @param suggestedName a filename desirable for the file to be saved - * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your - */ - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void saveDocument(Fragment fragment, String mimeType, String suggestedName, int requestCode) { - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType(mimeType); - intent.putExtra("android.content.extra.SHOW_ADVANCED", true); // Note: This is not documented, but works - intent.putExtra(Intent.EXTRA_TITLE, suggestedName); - fragment.startActivityForResult(intent, requestCode); - } - public static String getFilename(Context context, Uri uri) { String filename = null; try { @@ -234,7 +262,78 @@ public class FileHelper { return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; } - public static interface FileDialogCallback { - public void onFileSelected(File file, boolean checked); + public static String readTextFromUri(Context context, Uri outputUri, String charset) + throws IOException { + + byte[] decryptedMessage; + { + InputStream in = context.getContentResolver().openInputStream(outputUri); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[256]; + int read; + while ( (read = in.read(buf)) > 0) { + out.write(buf, 0, read); + } + in.close(); + out.close(); + decryptedMessage = out.toByteArray(); + } + + String plaintext; + if (charset != null) { + try { + plaintext = new String(decryptedMessage, charset); + } catch (UnsupportedEncodingException e) { + // if we can't decode properly, just fall back to utf-8 + plaintext = new String(decryptedMessage); + } + } else { + plaintext = new String(decryptedMessage); + } + + return plaintext; + + } + + public static void copyUriData(Context context, Uri fromUri, Uri toUri) throws IOException { + BufferedInputStream bis = null; + BufferedOutputStream bos = null; + + try { + ContentResolver resolver = context.getContentResolver(); + bis = new BufferedInputStream(resolver.openInputStream(fromUri)); + bos = new BufferedOutputStream(resolver.openOutputStream(toUri)); + byte[] buf = new byte[1024]; + int len; + while ( (len = bis.read(buf)) > 0) { + bos.write(buf, 0, len); + } + } finally { + try { + if (bis != null) { + bis.close(); + } + if (bos != null) { + bos.close(); + } + } catch (IOException e) { + // ignore, it's just stream closin' + } + } + } + + /** Checks if external storage is mounted if file is located on external storage. */ + public static boolean isStorageMounted(String file) { + if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + return false; + } + } + + return true; + } + + public interface FileDialogCallback { + void onFileSelected(File file, boolean checked); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java index 3bbd86d6a..8a614d64d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java @@ -51,15 +51,15 @@ public class KeyUpdateHelper { } // Start the service and update the keys - Intent importIntent = new Intent(mContext, KeychainIntentService.class); - importIntent.setAction(KeychainIntentService.ACTION_DOWNLOAD_AND_IMPORT_KEYS); + Intent importIntent = new Intent(mContext, KeychainService.class); + importIntent.setAction(KeychainService.ACTION_DOWNLOAD_AND_IMPORT_KEYS); Bundle importData = new Bundle(); - importData.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST, + importData.putParcelableArrayList(KeychainService.DOWNLOAD_KEY_LIST, new ArrayList<ImportKeysListEntry>(keys)); - importIntent.putExtra(KeychainIntentService.EXTRA_SERVICE_INTENT, importData); + importIntent.putExtra(KeychainService.EXTRA_SERVICE_INTENT, importData); - importIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, new Messenger(mHandler)); + importIntent.putExtra(KeychainService.EXTRA_MESSENGER, new Messenger(mHandler)); mContext.startService(importIntent); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OrientationUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OrientationUtils.java new file mode 100644 index 000000000..43ed12429 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OrientationUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2015 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; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; + +/** + * Static methods related to device orientation. + */ +public class OrientationUtils { + + /** + * Locks the device window in landscape mode. + */ + public static void lockOrientationLandscape(Activity activity) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + } + + /** + * Locks the device window in portrait mode. + */ + public static void lockOrientationPortrait(Activity activity) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + + /** + * Locks the device window in actual screen mode. + */ + public static void lockOrientation(Activity activity) { + Display display = ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); + int rotation = display.getRotation(); + int tempOrientation = activity.getResources().getConfiguration().orientation; + int orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + + switch (tempOrientation) { + case Configuration.ORIENTATION_LANDSCAPE: { + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + } else { + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + } + break; + } + case Configuration.ORIENTATION_PORTRAIT: { + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) { + orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + } else { + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + } + break; + } + } + activity.setRequestedOrientation(orientation); + } + + /** + * Unlocks the device window in user defined screen mode. + */ + public static void unlockOrientation(Activity activity) { + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER); + } + +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java index 5a314ad0b..eabbf83b8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableFileCache.java @@ -66,21 +66,24 @@ public class ParcelableFileCache<E extends Parcelable> { File tempFile = new File(mContext.getCacheDir(), mFilename); - DataOutputStream oos = new DataOutputStream(new FileOutputStream(tempFile)); - oos.writeInt(numEntries); + DataOutputStream oos = new DataOutputStream(new FileOutputStream(tempFile)); - while (it.hasNext()) { - Parcel p = Parcel.obtain(); // creating empty parcel object - p.writeParcelable(it.next(), 0); // saving bundle as parcel - byte[] buf = p.marshall(); - oos.writeInt(buf.length); - oos.write(buf); - p.recycle(); + try { + oos.writeInt(numEntries); + + while (it.hasNext()) { + Parcel p = Parcel.obtain(); // creating empty parcel object + p.writeParcelable(it.next(), 0); // saving bundle as parcel + byte[] buf = p.marshall(); + oos.writeInt(buf.length); + oos.write(buf); + p.recycle(); + } + } finally { + oos.close(); } - oos.close(); - } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java new file mode 100644 index 000000000..fa4081acc --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableHashMap.java @@ -0,0 +1,63 @@ +package org.sufficientlysecure.keychain.util; + + +import java.util.HashMap; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; + +import org.sufficientlysecure.keychain.KeychainApplication; + + +public class ParcelableHashMap <K extends Parcelable, V extends Parcelable> implements Parcelable { + + HashMap<K,V> mInner; + + public ParcelableHashMap(HashMap<K,V> inner) { + mInner = inner; + } + + protected ParcelableHashMap(@NonNull Parcel in) { + mInner = new HashMap<>(); + ClassLoader loader = KeychainApplication.class.getClassLoader(); + + int num = in.readInt(); + while (num-- > 0) { + K key = in.readParcelable(loader); + V val = in.readParcelable(loader); + mInner.put(key, val); + } + } + + public HashMap<K,V> getMap() { + return mInner; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mInner.size()); + for (HashMap.Entry<K,V> entry : mInner.entrySet()) { + parcel.writeParcelable(entry.getKey(), 0); + parcel.writeParcelable(entry.getValue(), 0); + } + } + + public static final Creator<ParcelableHashMap> CREATOR = new Creator<ParcelableHashMap>() { + @Override + public ParcelableHashMap createFromParcel(Parcel in) { + return new ParcelableHashMap(in); + } + + @Override + public ParcelableHashMap[] newArray(int size) { + return new ParcelableHashMap[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java new file mode 100644 index 000000000..7e788d04c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java @@ -0,0 +1,91 @@ +/* + * 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.util; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.net.InetSocketAddress; +import java.net.Proxy; + +/** + * used to simply transport java.net.Proxy objects created using InetSockets between services/activities + */ +public class ParcelableProxy implements Parcelable { + private String mProxyHost; + private int mProxyPort; + private Proxy.Type mProxyType; + + public ParcelableProxy(String hostName, int port, Proxy.Type type) { + mProxyHost = hostName; + + if (hostName == null) { + return; // represents a null proxy + } + + mProxyPort = port; + + mProxyType = type; + } + + public static ParcelableProxy getForNoProxy() { + return new ParcelableProxy(null, -1, null); + } + + public Proxy getProxy() { + if (mProxyHost == null) { + return null; + } + /* + * InetSocketAddress.createUnresolved so we can use this method even in the main thread + * (no network call) + */ + return new Proxy(mProxyType, InetSocketAddress.createUnresolved(mProxyHost, mProxyPort)); + } + + protected ParcelableProxy(Parcel in) { + mProxyHost = in.readString(); + mProxyPort = in.readInt(); + mProxyType = (Proxy.Type) in.readSerializable(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mProxyHost); + dest.writeInt(mProxyPort); + dest.writeSerializable(mProxyType); + } + + @SuppressWarnings("unused") + public static final Parcelable.Creator<ParcelableProxy> CREATOR = new Parcelable.Creator<ParcelableProxy>() { + @Override + public ParcelableProxy createFromParcel(Parcel in) { + return new ParcelableProxy(in); + } + + @Override + public ParcelableProxy[] newArray(int size) { + return new ParcelableProxy[size]; + } + }; +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Passphrase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Passphrase.java index 06efdde4d..fe42c7a2c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Passphrase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Passphrase.java @@ -117,6 +117,13 @@ public class Passphrase implements Parcelable { } } + /** + * Creates a new String from the char[]. This is considered unsafe! + */ + public String toStringUnsafe() { + return new String(mPassphrase); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -127,11 +134,7 @@ public class Passphrase implements Parcelable { } Passphrase that = (Passphrase) o; - if (!Arrays.equals(mPassphrase, that.mPassphrase)) { - return false; - } - - return true; + return Arrays.equals(mPassphrase, that.mPassphrase); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java index 303687315..4ef215036 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -21,9 +21,13 @@ package org.sufficientlysecure.keychain.util; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Resources; +import android.preference.PreferenceManager; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants.Pref; +import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; +import java.net.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.ListIterator; @@ -35,6 +39,10 @@ import java.util.Vector; public class Preferences { private static Preferences sPreferences; private SharedPreferences mSharedPreferences; + private Resources mResources; + + private static String PREF_FILE_NAME = "APG.main"; + private static int PREF_FILE_MODE = Context.MODE_MULTI_PROCESS; public static synchronized Preferences getPreferences(Context context) { return getPreferences(context, false); @@ -51,12 +59,18 @@ public class Preferences { } private Preferences(Context context) { + mResources = context.getResources(); updateSharedPreferences(context); } + public static void setPreferenceManagerFileAndMode(PreferenceManager manager) { + manager.setSharedPreferencesName(PREF_FILE_NAME); + manager.setSharedPreferencesMode(PREF_FILE_MODE); + } + public void updateSharedPreferences(Context context) { // multi-process safe preferences - mSharedPreferences = context.getSharedPreferences("APG.main", Context.MODE_MULTI_PROCESS); + mSharedPreferences = context.getSharedPreferences(PREF_FILE_NAME, PREF_FILE_MODE); } public String getLanguage() { @@ -138,6 +152,9 @@ public class Preferences { public String[] getKeyServers() { String rawData = mSharedPreferences.getString(Constants.Pref.KEY_SERVERS, Constants.Defaults.KEY_SERVERS); + if (rawData.equals("")) { + return new String[0]; + } Vector<String> servers = new Vector<>(); String chunks[] = rawData.split(","); for (String c : chunks) { @@ -150,7 +167,8 @@ public class Preferences { } public String getPreferredKeyserver() { - return getKeyServers()[0]; + String[] keyservers = getKeyServers(); + return keyservers.length == 0 ? null : keyservers[0]; } public void setKeyServers(String[] value) { @@ -182,6 +200,142 @@ public class Preferences { editor.commit(); } + public void setFilesUseCompression(boolean compress) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.FILE_USE_COMPRESSION, compress); + editor.commit(); + } + + public boolean getFilesUseCompression() { + return mSharedPreferences.getBoolean(Pref.FILE_USE_COMPRESSION, true); + } + + public void setTextUseCompression(boolean compress) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.TEXT_USE_COMPRESSION, compress); + editor.commit(); + } + + public boolean getTextUseCompression() { + return mSharedPreferences.getBoolean(Pref.TEXT_USE_COMPRESSION, true); + } + + public String getTheme() { + return mSharedPreferences.getString(Pref.THEME, Pref.Theme.LIGHT); + } + + public void setTheme(String value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putString(Constants.Pref.THEME, value); + editor.commit(); + } + + public void setUseArmor(boolean useArmor) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.USE_ARMOR, useArmor); + editor.commit(); + } + + public boolean getUseArmor() { + return mSharedPreferences.getBoolean(Pref.USE_ARMOR, false); + } + + public void setEncryptFilenames(boolean encryptFilenames) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.ENCRYPT_FILENAMES, encryptFilenames); + editor.commit(); + } + + public boolean getEncryptFilenames() { + return mSharedPreferences.getBoolean(Pref.ENCRYPT_FILENAMES, true); + } + + // proxy preference functions start here + + public boolean getUseNormalProxy() { + return mSharedPreferences.getBoolean(Constants.Pref.USE_NORMAL_PROXY, false); + } + + public boolean getUseTorProxy() { + return mSharedPreferences.getBoolean(Constants.Pref.USE_TOR_PROXY, false); + } + + public String getProxyHost() { + return mSharedPreferences.getString(Constants.Pref.PROXY_HOST, null); + } + + /** + * we store port as String for easy interfacing with EditTextPreference, but return it as an integer + * + * @return port number of proxy + */ + public int getProxyPort() { + return Integer.parseInt(mSharedPreferences.getString(Pref.PROXY_PORT, "-1")); + } + + /** + * we store port as String for easy interfacing with EditTextPreference, but return it as an integer + * + * @param port proxy port + */ + public void setProxyPort(String port) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putString(Pref.PROXY_PORT, port); + editor.commit(); + } + + public Proxy.Type getProxyType() { + final String typeHttp = Pref.ProxyType.TYPE_HTTP; + final String typeSocks = Pref.ProxyType.TYPE_SOCKS; + + String type = mSharedPreferences.getString(Pref.PROXY_TYPE, typeHttp); + + switch (type) { + case typeHttp: + return Proxy.Type.HTTP; + case typeSocks: + return Proxy.Type.SOCKS; + default: // shouldn't happen + Log.e(Constants.TAG, "Invalid Proxy Type in preferences"); + return null; + } + } + + public ProxyPrefs getProxyPrefs() { + boolean useTor = getUseTorProxy(); + boolean useNormalProxy = getUseNormalProxy(); + + if (useTor) { + return new ProxyPrefs(true, false, Constants.Orbot.PROXY_HOST, Constants.Orbot.PROXY_PORT, + Constants.Orbot.PROXY_TYPE); + } else if (useNormalProxy) { + return new ProxyPrefs(false, true, getProxyHost(), getProxyPort(), getProxyType()); + } else { + return new ProxyPrefs(false, false, null, -1, null); + } + } + + public static class ProxyPrefs { + public final ParcelableProxy parcelableProxy; + public final boolean torEnabled; + public final boolean normalPorxyEnabled; + + /** + * torEnabled and normalProxyEnabled are not expected to both be true + * + * @param torEnabled if Tor is to be used + * @param normalPorxyEnabled if user-specified proxy is to be used + */ + public ProxyPrefs(boolean torEnabled, boolean normalPorxyEnabled, String hostName, int port, Proxy.Type type) { + this.torEnabled = torEnabled; + this.normalPorxyEnabled = normalPorxyEnabled; + if (!torEnabled && !normalPorxyEnabled) this.parcelableProxy = new ParcelableProxy(null, -1, null); + else this.parcelableProxy = new ParcelableProxy(hostName, port, type); + } + } + + // cloud prefs + public CloudSearchPrefs getCloudSearchPrefs() { return new CloudSearchPrefs(mSharedPreferences.getBoolean(Pref.SEARCH_KEYSERVER, true), mSharedPreferences.getBoolean(Pref.SEARCH_KEYBASE, true), @@ -205,7 +359,39 @@ public class Preferences { } } - public void updatePreferences() { + // experimental prefs + + public void setExperimentalEnableWordConfirm(boolean enableWordConfirm) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM, enableWordConfirm); + editor.commit(); + } + + public boolean getExperimentalEnableWordConfirm() { + return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM, false); + } + + public void setExperimentalEnableLinkedIdentities(boolean enableLinkedIdentities) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES, enableLinkedIdentities); + editor.commit(); + } + + public boolean getExperimentalEnableLinkedIdentities() { + return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES, false); + } + + public void setExperimentalEnableKeybase(boolean enableKeybase) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.EXPERIMENTAL_ENABLE_KEYBASE, enableKeybase); + editor.commit(); + } + + public boolean getExperimentalEnableKeybase() { + return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_KEYBASE, false); + } + + public void upgradePreferences(Context context) { if (mSharedPreferences.getInt(Constants.Pref.PREF_DEFAULT_VERSION, 0) != Constants.Defaults.PREF_VERSION) { switch (mSharedPreferences.getInt(Constants.Pref.PREF_DEFAULT_VERSION, 0)) { @@ -239,6 +425,14 @@ public class Preferences { } // fall through case 4: { + setTheme(Constants.Pref.Theme.DEFAULT); + } + // fall through + case 5: { + KeyserverSyncAdapterService.enableKeyserverSync(context); + } + // fall through + case 6: { } } @@ -248,4 +442,9 @@ public class Preferences { .commit(); } } + + public void clear() { + mSharedPreferences.edit().clear().commit(); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java index 120b84a3b..0297d149c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java @@ -91,6 +91,7 @@ public class ShareHelper { // Create chooser with only one Intent in it Intent chooserIntent = Intent.createChooser(targetedShareIntents.remove(targetedShareIntents.size() - 1), title); // append all other Intents + // TODO this line looks wrong?! chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedShareIntents.toArray(new Parcelable[]{})); return chooserIntent; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java index 4ff14e3bb..d1d1ada2a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.util; import android.content.res.AssetManager; +import com.squareup.okhttp.OkHttpClient; import org.sufficientlysecure.keychain.Constants; import java.io.ByteArrayInputStream; @@ -26,7 +27,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; -import java.net.URLConnection; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -61,7 +61,7 @@ public class TlsHelper { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int reads = is.read(); - while(reads != -1){ + while (reads != -1) { baos.write(reads); reads = is.read(); } @@ -74,15 +74,56 @@ public class TlsHelper { } } - public static URLConnection openConnection(URL url) throws IOException, TlsHelperException { + public static void pinCertificateIfNecessary(OkHttpClient client, URL url) throws TlsHelperException, IOException { if (url.getProtocol().equals("https")) { for (String domain : sStaticCA.keySet()) { if (url.getHost().endsWith(domain)) { - return openCAConnection(sStaticCA.get(domain), url); + pinCertificate(sStaticCA.get(domain), client); } } } - return url.openConnection(); + } + + /** + * Modifies the client to accept only requests with a given certificate. Applies to all URLs requested by the + * client. + * Therefore a client that is pinned this way should be used to only make requests to URLs with passed certificate. + * TODO: Refactor - More like SSH StrictHostKeyChecking than pinning? + * + * @param certificate certificate to pin + * @param client OkHttpClient to enforce pinning on + * @throws TlsHelperException + * @throws IOException + */ + private static void pinCertificate(byte[] certificate, OkHttpClient client) + throws TlsHelperException, IOException { + // We don't use OkHttp's CertificatePinner since it depends on a TrustManager to verify it too. Refer to + // note at end of description: http://square.github.io/okhttp/javadoc/com/squareup/okhttp/CertificatePinner.html + // Creating our own TrustManager that trusts only our certificate eliminates the need for certificate pinning + try { + // Load CA + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate ca = cf.generateCertificate(new ByteArrayInputStream(certificate)); + + // Create a KeyStore containing our trusted CAs + String keyStoreType = KeyStore.getDefaultType(); + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(null, null); + keyStore.setCertificateEntry("ca", ca); + + // Create a TrustManager that trusts the CAs in our KeyStore + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init(keyStore); + + // Create an SSLContext that uses our TrustManager + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, tmf.getTrustManagers(), null); + + client.setSslSocketFactory(context.getSocketFactory()); + } catch (CertificateException | KeyStoreException | KeyManagementException | NoSuchAlgorithmException e) { + throw new TlsHelperException(e); + } } /** diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java new file mode 100644 index 000000000..d85ad9128 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java @@ -0,0 +1,463 @@ +/* This is the license for Orlib, a free software project to + provide anonymity on the Internet from a Google Android smartphone. + + For more information about Orlib, see https://guardianproject.info/ + + If you got this file as a part of a larger bundle, there may be other + license terms that you should be aware of. + =============================================================================== + Orlib is distributed under this license (aka the 3-clause BSD license) + + Copyright (c) 2009-2010, Nathan Freitas, The Guardian Project + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + * Neither the names of the copyright owners nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ***** + Orlib contains a binary distribution of the JSocks library: + http://code.google.com/p/jsocks-mirror/ + which is licensed under the GNU Lesser General Public License: + http://www.gnu.org/licenses/lgpl.html + + ***** +*/ + +package org.sufficientlysecure.keychain.util.orbot; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.text.TextUtils; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.dialog.SupportInstallDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.OrbotStartDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.PreferenceInstallDialogFragment; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.Preferences; + +import java.util.List; + +/** + * This class is taken from the NetCipher library: https://github.com/guardianproject/NetCipher/ + */ +public class OrbotHelper { + + public interface DialogActions { + void onOrbotStarted(); + + void onNeutralButton(); + + void onCancel(); + } + + private final static int REQUEST_CODE_STATUS = 100; + + public final static String ORBOT_PACKAGE_NAME = "org.torproject.android"; + public final static String ORBOT_MARKET_URI = "market://details?id=" + ORBOT_PACKAGE_NAME; + public final static String ORBOT_FDROID_URI = "https://f-droid.org/repository/browse/?fdid=" + + ORBOT_PACKAGE_NAME; + public final static String ORBOT_PLAY_URI = "https://play.google.com/store/apps/details?id=" + + ORBOT_PACKAGE_NAME; + + /** + * A request to Orbot to transparently start Tor services + */ + public final static String ACTION_START = "org.torproject.android.intent.action.START"; + /** + * {@link Intent} send by Orbot with {@code ON/OFF/STARTING/STOPPING} status + */ + public final static String ACTION_STATUS = "org.torproject.android.intent.action.STATUS"; + /** + * {@code String} that contains a status constant: {@link #STATUS_ON}, + * {@link #STATUS_OFF}, {@link #STATUS_STARTING}, or + * {@link #STATUS_STOPPING} + */ + public final static String EXTRA_STATUS = "org.torproject.android.intent.extra.STATUS"; + /** + * A {@link String} {@code packageName} for Orbot to direct its status reply + * to, used in {@link #ACTION_START} {@link Intent}s sent to Orbot + */ + public final static String EXTRA_PACKAGE_NAME = "org.torproject.android.intent.extra.PACKAGE_NAME"; + + /** + * All tor-related services and daemons are stopped + */ + @SuppressWarnings("unused") // we might use this later, sent by Orbot + public final static String STATUS_OFF = "OFF"; + /** + * All tor-related services and daemons have completed starting + */ + public final static String STATUS_ON = "ON"; + @SuppressWarnings("unused") // we might use this later, sent by Orbot + public final static String STATUS_STARTING = "STARTING"; + @SuppressWarnings("unused") // we might use this later, sent by Orbot + public final static String STATUS_STOPPING = "STOPPING"; + /** + * The user has disabled the ability for background starts triggered by + * apps. Fallback to the old Intent that brings up Orbot. + */ + public final static String STATUS_STARTS_DISABLED = "STARTS_DISABLED"; + + public final static String ACTION_START_TOR = "org.torproject.android.START_TOR"; + /** + * request code used to start tor + */ + public final static int START_TOR_RESULT = 0x9234; + + private final static String FDROID_PACKAGE_NAME = "org.fdroid.fdroid"; + private final static String PLAY_PACKAGE_NAME = "com.android.vending"; + + private OrbotHelper() { + // only static utility methods, do not instantiate + } + + public static boolean isOrbotRunning(Context context) { + int procId = TorServiceUtils.findProcessId(context); + + return (procId != -1); + } + + public static boolean isOrbotInstalled(Context context) { + return isAppInstalled(context, ORBOT_PACKAGE_NAME); + } + + private static boolean isAppInstalled(Context context, String uri) { + try { + PackageManager pm = context.getPackageManager(); + pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + /** + * First, checks whether Orbot is installed, then checks whether Orbot is + * running. If Orbot is installed and not running, then an {@link Intent} is + * sent to request Orbot to start, which will show the main Orbot screen. + * The result will be returned in + * {@link Activity#onActivityResult(int requestCode, int resultCode, Intent data)} + * with a {@code requestCode} of {@code START_TOR_RESULT} + * + * @param activity the {@link Activity} that gets the + * {@code START_TOR_RESULT} result + * @return whether the start request was sent to Orbot + */ + public static boolean requestShowOrbotStart(Activity activity) { + if (OrbotHelper.isOrbotInstalled(activity)) { + if (!OrbotHelper.isOrbotRunning(activity)) { + Intent intent = getShowOrbotStartIntent(); + activity.startActivityForResult(intent, START_TOR_RESULT); + return true; + } + } + return false; + } + + public static Intent getShowOrbotStartIntent() { + Intent intent = new Intent(ACTION_START_TOR); + intent.setPackage(ORBOT_PACKAGE_NAME); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } + + /** + * First, checks whether Orbot is installed. If Orbot is installed, then a + * broadcast {@link Intent} is sent to request Orbot to start transparently + * in the background. When Orbot receives this {@code Intent}, it will + * immediately reply to this all with its status via an + * {@link #ACTION_STATUS} {@code Intent} that is broadcast to the + * {@code packageName} of the provided {@link Context} (i.e. + * {@link Context#getPackageName()}. + * + * @param context the app {@link Context} will receive the reply + * @return whether the start request was sent to Orbot + */ + public static boolean requestStartTor(Context context) { + if (OrbotHelper.isOrbotInstalled(context)) { + Log.i("OrbotHelper", "requestStartTor " + context.getPackageName()); + Intent intent = getOrbotStartIntent(); + intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName()); + context.sendBroadcast(intent); + return true; + } + return false; + } + + public static Intent getOrbotStartIntent() { + Intent intent = new Intent(ACTION_START); + intent.setPackage(ORBOT_PACKAGE_NAME); + return intent; + } + + public static Intent getOrbotInstallIntent(Context context) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(ORBOT_MARKET_URI)); + + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> resInfos = pm.queryIntentActivities(intent, 0); + + String foundPackageName = null; + for (ResolveInfo r : resInfos) { + Log.i("OrbotHelper", "market: " + r.activityInfo.packageName); + if (TextUtils.equals(r.activityInfo.packageName, FDROID_PACKAGE_NAME) + || TextUtils.equals(r.activityInfo.packageName, PLAY_PACKAGE_NAME)) { + foundPackageName = r.activityInfo.packageName; + break; + } + } + + if (foundPackageName == null) { + intent.setData(Uri.parse(ORBOT_FDROID_URI)); + } else { + intent.setPackage(foundPackageName); + } + return intent; + } + + /** + * hack to get around the fact that PreferenceActivity still supports only android.app.DialogFragment + */ + public static android.app.DialogFragment getPreferenceInstallDialogFragment() { + return PreferenceInstallDialogFragment.newInstance(R.string.orbot_install_dialog_title, + R.string.orbot_install_dialog_content, ORBOT_PACKAGE_NAME); + } + + public static DialogFragment getInstallDialogFragmentWithThirdButton(Messenger messenger, int middleButton) { + return SupportInstallDialogFragment.newInstance(messenger, R.string.orbot_install_dialog_title, + R.string.orbot_install_dialog_content, ORBOT_PACKAGE_NAME, middleButton, true); + } + + public static DialogFragment getOrbotStartDialogFragment(Messenger messenger, int middleButton) { + return OrbotStartDialogFragment.newInstance(messenger, R.string.orbot_start_dialog_title, R.string + .orbot_start_dialog_content, + middleButton); + } + + /** + * checks preferences to see if Orbot is required, and if yes, if it is installed and running + * + * @param context used to retrieve preferences + * @return false if Tor is selected proxy and Orbot is not installed or running, true + * otherwise + */ + public static boolean isOrbotInRequiredState(Context context) { + Preferences.ProxyPrefs proxyPrefs = Preferences.getPreferences(context).getProxyPrefs(); + if (!proxyPrefs.torEnabled) { + return true; + } else if (!OrbotHelper.isOrbotInstalled(context) || !OrbotHelper.isOrbotRunning(context)) { + return false; + } + return true; + } + + /** + * checks if Tor is enabled and if it is, that Orbot is installed and running. Generates appropriate dialogs. + * + * @param middleButton resourceId of string to display as the middle button of install and enable dialogs + * @param proxyPrefs proxy preferences used to determine if Tor is required to be started + * @return true if Tor is not enabled or Tor is enabled and Orbot is installed and running, else false + */ + public static boolean putOrbotInRequiredState(final int middleButton, + final DialogActions dialogActions, + Preferences.ProxyPrefs proxyPrefs, + final FragmentActivity fragmentActivity) { + + if (!proxyPrefs.torEnabled) { + return true; + } + + if (!OrbotHelper.isOrbotInstalled(fragmentActivity)) { + Handler ignoreTorHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SupportInstallDialogFragment.MESSAGE_MIDDLE_CLICKED: + dialogActions.onNeutralButton(); + break; + case SupportInstallDialogFragment.MESSAGE_DIALOG_DISMISSED: + // both install and cancel buttons mean we don't go ahead with an + // operation, so it's okay to cancel + dialogActions.onCancel(); + break; + } + } + }; + + OrbotHelper.getInstallDialogFragmentWithThirdButton( + new Messenger(ignoreTorHandler), + middleButton + ).show(fragmentActivity.getSupportFragmentManager(), "OrbotHelperOrbotInstallDialog"); + + return false; + } else if (!OrbotHelper.isOrbotRunning(fragmentActivity)) { + + final Handler dialogHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case OrbotStartDialogFragment.MESSAGE_MIDDLE_BUTTON: + dialogActions.onNeutralButton(); + break; + case OrbotStartDialogFragment.MESSAGE_DIALOG_CANCELLED: + dialogActions.onCancel(); + break; + case OrbotStartDialogFragment.MESSAGE_ORBOT_STARTED: + dialogActions.onOrbotStarted(); + break; + } + } + }; + + new SilentStartManager() { + + @Override + protected void onOrbotStarted() { + dialogActions.onOrbotStarted(); + } + + @Override + protected void onSilentStartDisabled() { + getOrbotStartDialogFragment(new Messenger(dialogHandler), middleButton) + .show(fragmentActivity.getSupportFragmentManager(), + "OrbotHelperOrbotStartDialog"); + } + }.startOrbotAndListen(fragmentActivity, true); + + return false; + } else { + return true; + } + } + + public static boolean putOrbotInRequiredState(DialogActions dialogActions, + FragmentActivity fragmentActivity) { + return putOrbotInRequiredState(R.string.orbot_ignore_tor, + dialogActions, + Preferences.getPreferences(fragmentActivity).getProxyPrefs(), + fragmentActivity); + } + + /** + * will attempt a silent start, which if disabled will fallback to the + * {@link #requestShowOrbotStart(Activity) requestShowOrbotStart} method, which returns the + * result in {@link Activity#onActivityResult(int requestCode, int resultCode, Intent data)} + * with a {@code requestCode} of {@code START_TOR_RESULT}, which will have to be implemented by + * activities wishing to respond to a change in Orbot state. + */ + public static void bestPossibleOrbotStart(final DialogActions dialogActions, + final Activity activity, + boolean showProgress) { + new SilentStartManager() { + + @Override + protected void onOrbotStarted() { + dialogActions.onOrbotStarted(); + } + + @Override + protected void onSilentStartDisabled() { + requestShowOrbotStart(activity); + } + }.startOrbotAndListen(activity, showProgress); + } + + /** + * base class for listening to silent orbot starts. Also handles display of progress dialog. + */ + public static abstract class SilentStartManager { + + private ProgressDialog mProgressDialog; + + public void startOrbotAndListen(final Context context, final boolean showProgress) { + Log.d(Constants.TAG, "starting orbot listener"); + if (showProgress) { + showProgressDialog(context); + } + + final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getStringExtra(OrbotHelper.EXTRA_STATUS)) { + case OrbotHelper.STATUS_ON: + context.unregisterReceiver(this); + // generally Orbot starts working a little after this status is received + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (showProgress) { + mProgressDialog.dismiss(); + } + onOrbotStarted(); + } + }, 1000); + break; + case OrbotHelper.STATUS_STARTS_DISABLED: + context.unregisterReceiver(this); + if (showProgress) { + mProgressDialog.dismiss(); + } + onSilentStartDisabled(); + break; + + } + Log.d(Constants.TAG, "Orbot silent start broadcast: " + + intent.getStringExtra(OrbotHelper.EXTRA_STATUS)); + } + }; + context.registerReceiver(receiver, new IntentFilter(OrbotHelper.ACTION_STATUS)); + + requestStartTor(context); + } + + private void showProgressDialog(Context context) { + mProgressDialog = new ProgressDialog(ThemeChanger.getDialogThemeWrapper(context)); + mProgressDialog.setMessage(context.getString(R.string.progress_starting_orbot)); + mProgressDialog.setCancelable(false); + mProgressDialog.show(); + } + + protected abstract void onOrbotStarted(); + + protected abstract void onSilentStartDisabled(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java new file mode 100644 index 000000000..2638f8cd5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/TorServiceUtils.java @@ -0,0 +1,156 @@ +/* This is the license for Orlib, a free software project to + provide anonymity on the Internet from a Google Android smartphone. + + For more information about Orlib, see https://guardianproject.info/ + + If you got this file as a part of a larger bundle, there may be other + license terms that you should be aware of. + =============================================================================== + Orlib is distributed under this license (aka the 3-clause BSD license) + + Copyright (c) 2009-2010, Nathan Freitas, The Guardian Project + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + + * Neither the names of the copyright owners nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ***** + Orlib contains a binary distribution of the JSocks library: + http://code.google.com/p/jsocks-mirror/ + which is licensed under the GNU Lesser General Public License: + http://www.gnu.org/licenses/lgpl.html + + ***** +*/ + +package org.sufficientlysecure.keychain.util.orbot; + +import android.content.Context; + +import org.sufficientlysecure.keychain.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.net.URLEncoder; +import java.util.StringTokenizer; + +/** + * This class is taken from the NetCipher library: https://github.com/guardianproject/NetCipher/ + */ +public class TorServiceUtils { + + private final static String TAG = "TorUtils"; + + public final static String SHELL_CMD_PS = "ps"; + public final static String SHELL_CMD_PIDOF = "pidof"; + + public static int findProcessId(Context context) { + String dataPath = context.getFilesDir().getParentFile().getParentFile().getAbsolutePath(); + String command = dataPath + "/" + OrbotHelper.ORBOT_PACKAGE_NAME + "/app_bin/tor"; + int procId = -1; + + try { + procId = findProcessIdWithPidOf(command); + + if (procId == -1) + procId = findProcessIdWithPS(command); + } catch (Exception e) { + try { + procId = findProcessIdWithPS(command); + } catch (Exception e2) { + Log.e(TAG, "Unable to get proc id for command: " + URLEncoder.encode(command), e2); + } + } + + return procId; + } + + // use 'pidof' command + public static int findProcessIdWithPidOf(String command) throws Exception { + + int procId = -1; + + Runtime r = Runtime.getRuntime(); + + Process procPs; + + String baseName = new File(command).getName(); + // fix contributed my mikos on 2010.12.10 + procPs = r.exec(new String[]{ + SHELL_CMD_PIDOF, baseName + }); + // procPs = r.exec(SHELL_CMD_PIDOF); + + BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream())); + String line; + + while ((line = reader.readLine()) != null) { + + try { + // this line should just be the process id + procId = Integer.parseInt(line.trim()); + break; + } catch (NumberFormatException e) { + Log.e("TorServiceUtils", "unable to parse process pid: " + line, e); + } + } + + return procId; + + } + + // use 'ps' command + public static int findProcessIdWithPS(String command) throws Exception { + + int procId = -1; + + Runtime r = Runtime.getRuntime(); + + Process procPs; + + procPs = r.exec(SHELL_CMD_PS); + + BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream())); + String line; + + while ((line = reader.readLine()) != null) { + if (line.contains(' ' + command)) { + + StringTokenizer st = new StringTokenizer(line, " "); + st.nextToken(); // proc owner + + procId = Integer.parseInt(st.nextToken().trim()); + + break; + } + } + + return procId; + + } +}
\ No newline at end of file |