diff options
author | Dominik Schürmann <dominik@dominikschuermann.de> | 2013-01-16 14:31:16 +0100 |
---|---|---|
committer | Dominik Schürmann <dominik@dominikschuermann.de> | 2013-01-16 14:31:16 +0100 |
commit | 1feb948acf81532f82b36456080920543004b097 (patch) | |
tree | f22e51163db4303ad72b9205a3347aea8211c15e /OpenPGP-Keychain/src/org | |
parent | dbbd8f6856086a9aa17b565080959fb77dc24cd9 (diff) | |
download | open-keychain-1feb948acf81532f82b36456080920543004b097.tar.gz open-keychain-1feb948acf81532f82b36456080920543004b097.tar.bz2 open-keychain-1feb948acf81532f82b36456080920543004b097.zip |
Renaming APG to OpenPGP Keychain
Diffstat (limited to 'OpenPGP-Keychain/src/org')
87 files changed, 18811 insertions, 0 deletions
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Constants.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Constants.java new file mode 100644 index 000000000..10e431d4b --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Constants.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain; + +import android.os.Environment; + +public final class Constants { + + public static final boolean DEBUG = true; + + public static final String TAG = "APG"; + + public static final String PACKAGE_NAME = "org.sufficientlysecure.keychain"; + + public static final String NFC_MIME = "application/org.sufficientlysecure.keychain"; + + public static final String PERMISSION_ACCESS_KEY_DATABASE = PACKAGE_NAME + + ".permission.ACCESS_KEY_DATABASE"; + public static final String PERMISSION_ACCESS_API = PACKAGE_NAME + ".permission.ACCESS_API"; + + public static final String INTENT_PREFIX = PACKAGE_NAME + ".action."; + + public static final class path { + public static final String APP_DIR = Environment.getExternalStorageDirectory() + "/APG"; + } + + public static final class pref { + public static final String DEFAULT_ENCRYPTION_ALGORITHM = "defaultEncryptionAlgorithm"; + public static final String DEFAULT_HASH_ALGORITHM = "defaultHashAlgorithm"; + public static final String DEFAULT_ASCII_ARMOUR = "defaultAsciiArmour"; + public static final String DEFAULT_MESSAGE_COMPRESSION = "defaultMessageCompression"; + public static final String DEFAULT_FILE_COMPRESSION = "defaultFileCompression"; + public static final String PASS_PHRASE_CACHE_TTL = "passPhraseCacheTtl"; + public static final String LANGUAGE = "language"; + public static final String FORCE_V3_SIGNATURES = "forceV3Signatures"; + public static final String KEY_SERVERS = "keyServers"; + } + + public static final class defaults { + public static final String KEY_SERVERS = "pool.sks-keyservers.net, subkeys.pgp.net, pgp.mit.edu"; + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Id.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Id.java new file mode 100644 index 000000000..382f144d7 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Id.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain; + +import org.spongycastle.bcpg.CompressionAlgorithmTags; + +/** + * + * TODO: + * + * - refactor ids, some are not needed and can be done with xml + * + */ +public final class Id { + + public static final String TAG = "APG"; + + public static final class menu { + public static final int export = 0x21070001; + public static final int delete = 0x21070002; + public static final int edit = 0x21070003; + public static final int update = 0x21070004; + public static final int exportToServer = 0x21070005; + public static final int share = 0x21070006; + public static final int share_qr_code = 0x21070007; + public static final int share_nfc = 0x21070008; + public static final int signKey = 0x21070009; + + public static final class option { + public static final int new_pass_phrase = 0x21070001; + public static final int create = 0x21070002; + public static final int about = 0x21070003; + public static final int manage_public_keys = 0x21070004; + public static final int manage_secret_keys = 0x21070005; + public static final int export_keys = 0x21070007; + public static final int preferences = 0x21070008; + public static final int search = 0x21070009; + public static final int help = 0x21070010; + public static final int key_server = 0x21070011; + public static final int scanQRCode = 0x21070012; + public static final int encrypt = 0x21070013; + public static final int encrypt_to_clipboard = 0x21070014; + public static final int decrypt = 0x21070015; + public static final int reply = 0x21070016; + public static final int cancel = 0x21070017; + public static final int save = 0x21070018; + public static final int okay = 0x21070019; + public static final int import_from_file = 0x21070020; + public static final int import_from_qr_code = 0x21070021; + public static final int import_from_nfc = 0x21070022; + } + } + + // use only lower 16 bits due to compatibility lib + public static final class message { + public static final int progress_update = 0x00006001; + public static final int done = 0x00006002; + public static final int import_keys = 0x00006003; + public static final int export_keys = 0x00006004; + public static final int import_done = 0x00006005; + public static final int export_done = 0x00006006; + public static final int create_key = 0x00006007; + public static final int edit_key = 0x00006008; + public static final int delete_done = 0x00006009; + public static final int query_done = 0x00006010; + public static final int unknown_signature_key = 0x00006011; + } + + // public static final class message { + // public static final int progress_update = 0x21070001; + // public static final int done = 0x21070002; + // public static final int import_keys = 0x21070003; + // public static final int export_keys = 0x21070004; + // public static final int import_done = 0x21070005; + // public static final int export_done = 0x21070006; + // public static final int create_key = 0x21070007; + // public static final int edit_key = 0x21070008; + // public static final int delete_done = 0x21070009; + // public static final int query_done = 0x21070010; + // public static final int unknown_signature_key = 0x21070011; + // } + + // use only lower 16 bits due to compatibility lib + public static final class request { + public static final int public_keys = 0x00007001; + public static final int secret_keys = 0x00007002; + public static final int filename = 0x00007003; + public static final int output_filename = 0x00007004; + public static final int key_server_preference = 0x00007005; + public static final int look_up_key_id = 0x00007006; + public static final int export_to_server = 0x00007007; + public static final int import_from_qr_code = 0x00007008; + public static final int sign_key = 0x00007009; + } + + // public static final class request { + // public static final int public_keys = 0x21070001; + // public static final int secret_keys = 0x21070002; + // public static final int filename = 0x21070003; + // public static final int output_filename = 0x21070004; + // public static final int key_server_preference = 0x21070005; + // public static final int look_up_key_id = 0x21070006; + // public static final int export_to_server = 0x21070007; + // public static final int import_from_qr_code = 0x21070008; + // public static final int sign_key = 0x21070009; + // } + + public static final class dialog { + public static final int pass_phrase = 0x21070001; + public static final int encrypting = 0x21070002; + public static final int decrypting = 0x21070003; + public static final int new_pass_phrase = 0x21070004; + public static final int pass_phrases_do_not_match = 0x21070005; + public static final int no_pass_phrase = 0x21070006; + public static final int saving = 0x21070007; + public static final int delete_key = 0x21070008; + public static final int import_keys = 0x21070009; + public static final int importing = 0x2107000a; + public static final int export_key = 0x2107000b; + public static final int export_keys = 0x2107000c; + public static final int exporting = 0x2107000d; + public static final int new_account = 0x2107000e; + // public static final int about = 0x2107000f; + public static final int change_log = 0x21070010; + public static final int output_filename = 0x21070011; + public static final int delete_file = 0x21070012; + public static final int deleting = 0x21070013; + public static final int help = 0x21070014; + public static final int querying = 0x21070015; + public static final int lookup_unknown_key = 0x21070016; + public static final int signing = 0x21070017; + } + + public static final class task { + public static final int import_keys = 0x21070001; + public static final int export_keys = 0x21070002; + } + + // public static final class database { + // public static final int type_public = 0; + // public static final int type_secret = 1; + // } + + public static final class type { + public static final int public_key = 0x21070001; + public static final int secret_key = 0x21070002; + public static final int user_id = 0x21070003; + public static final int key = 0x21070004; + } + + public static final class choice { + public static final class algorithm { + public static final int dsa = 0x21070001; + public static final int elgamal = 0x21070002; + public static final int rsa = 0x21070003; + } + + public static final class compression { + public static final int none = 0x21070001; + public static final int zlib = CompressionAlgorithmTags.ZLIB; + public static final int bzip2 = CompressionAlgorithmTags.BZIP2; + public static final int zip = CompressionAlgorithmTags.ZIP; + } + + public static final class usage { + public static final int sign_only = 0x21070001; + public static final int encrypt_only = 0x21070002; + public static final int sign_and_encrypt = 0x21070003; + } + + public static final class action { + public static final int encrypt = 0x21070001; + public static final int decrypt = 0x21070002; + public static final int import_public = 0x21070003; + public static final int import_secret = 0x21070004; + } + } + + public static final class return_value { + public static final int ok = 0; + public static final int error = -1; + public static final int no_master_key = -2; + public static final int updated = 1; + public static final int bad = -3; + } + + public static final class target { + public static final int clipboard = 0x21070001; + public static final int email = 0x21070002; + public static final int file = 0x21070003; + public static final int message = 0x21070004; + } + + public static final class mode { + public static final int undefined = 0x21070001; + public static final int byte_array = 0x21070002; + public static final int file = 0x21070003; + public static final int stream = 0x21070004; + } + + public static final class key { + public static final int none = 0; + public static final int symmetric = -1; + } + + public static final class content { + public static final int unknown = 0; + public static final int encrypted_data = 1; + public static final int keys = 2; + } + + public static final class keyserver { + public static final int search = 0x21070001; + public static final int get = 0x21070002; + public static final int add = 0x21070003; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/KeychainApplication.java new file mode 100644 index 000000000..dbe9f6394 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/KeychainApplication.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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; + +import java.io.File; +import java.security.Security; + +import org.spongycastle.jce.provider.BouncyCastleProvider; + +import android.app.Application; +import android.os.Environment; + +public class KeychainApplication extends Application { + + static { + // Define Java Security Provider to be Bouncy Castle + Security.addProvider(new BouncyCastleProvider()); + } + + @Override + public void onCreate() { + super.onCreate(); + + // Create APG directory on sdcard if not existing + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + File dir = new File(Constants.path.APP_DIR); + if (!dir.exists() && !dir.mkdirs()) { + // ignore this for now, it's not crucial + // that the directory doesn't exist at this point + } + } + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java new file mode 100644 index 000000000..5ea5bd09a --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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 java.lang.reflect.Method; + +import android.content.Context; + +import org.sufficientlysecure.keychain.util.Log; + +public class ClipboardReflection { + + private static final String clipboardLabel = "APG"; + + /** + * 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("ProjectsException", "There was and error copying the text to the clipboard: " + + e.getMessage()); + } + } + + /** + * Wrapper around ClipboardManager based on Android version using Reflection API + * + * @param context + * @param text + */ + 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); + + 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("ProjectsException", "There was and error getting the text from the clipboard: " + + e.getMessage()); + + return null; + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java new file mode 100644 index 000000000..8c0a26a28 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.os.Build; +import android.os.Handler; + +/** + * Bug on Android >= 4.2 + * + * http://code.google.com/p/android/issues/detail?id=41901 + * + * DialogFragment disappears on pressing home and comming back. This also happens especially in + * FileDialogFragment after launching a file manager and coming back. + * + * Usage: <code> + * DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + * public void run() { + * // show dialog... + * } + * }); + * </code> + */ +public class DialogFragmentWorkaround { + public static final SDKLevel17Interface INTERFACE = ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) ? new SDKLevel17Impl() + : new SDKLevelPriorLevel17Impl()); + + private static final int RUNNABLE_DELAY = 300; + + public interface SDKLevel17Interface { + // Workaround for http://code.google.com/p/android/issues/detail?id=41901 + void runnableRunDelayed(Runnable runnable); + } + + private static class SDKLevelPriorLevel17Impl implements SDKLevel17Interface { + @Override + public void runnableRunDelayed(Runnable runnable) { + runnable.run(); + } + } + + private static class SDKLevel17Impl implements SDKLevel17Interface { + @Override + public void runnableRunDelayed(Runnable runnable) { + new Handler().postDelayed(runnable, RUNNABLE_DELAY); + } + } + + // Can't instantiate this class + private DialogFragmentWorkaround() { + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java new file mode 100644 index 000000000..a1f27ca20 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.view.View; +import android.widget.ListView; + +import com.actionbarsherlock.app.SherlockListFragment; + +/** + * Bug on Android >= 4.1 + * + * http://code.google.com/p/android/issues/detail?id=35885 + * + * Items are not checked in layout + */ +public class ListFragmentWorkaround extends SherlockListFragment { + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + l.setItemChecked(position, l.isItemChecked(position)); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/FileHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/FileHelper.java new file mode 100644 index 000000000..f16fd874b --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/FileHelper.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.helper; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.widget.Toast; + +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; + } + } + + return true; + } + + /** + * Opens the preferred installed file manager on Android and shows a toast if no manager is + * installed. + * + * @param activity + * @param filename + * default selected file, not supported by all file managers + * @param type + * 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(Activity activity, String filename, String type, int requestCode) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + + intent.setData(Uri.parse("file://" + filename)); + intent.setType(type); + + try { + activity.startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + // No compatible file manager was found. + Toast.makeText(activity, R.string.noFilemanagerInstalled, Toast.LENGTH_SHORT).show(); + } + } + + /** + * Get a file path from a Uri. + * + * from https://github.com/iPaulPro/aFileChooser/blob/master/aFileChooser/src/com/ipaulpro/ + * afilechooser/utils/FileUtils.java + * + * @param context + * @param uri + * @return + * + * @author paulburke + */ + public static String getPath(Context context, Uri uri) { + Log.d(Constants.TAG + " File -", + "Authority: " + uri.getAuthority() + ", Fragment: " + uri.getFragment() + + ", Port: " + uri.getPort() + ", Query: " + uri.getQuery() + ", Scheme: " + + uri.getScheme() + ", Host: " + uri.getHost() + ", Segments: " + + uri.getPathSegments().toString()); + + if ("content".equalsIgnoreCase(uri.getScheme())) { + String[] projection = { "_data" }; + Cursor cursor = null; + + try { + cursor = context.getContentResolver().query(uri, projection, null, null, null); + int column_index = cursor.getColumnIndexOrThrow("_data"); + if (cursor.moveToFirst()) { + return cursor.getString(column_index); + } + } catch (Exception e) { + // Eat it + } + } + + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java new file mode 100644 index 000000000..6a71ca0ba --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.helper; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Iterator; +import java.util.Set; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.SherlockFragmentActivity; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.widget.Toast; + +public class OtherHelper { + + /** + * Return the number if days between two dates + * + * @param first + * @param second + * @return number of days + */ + public static long getNumDaysBetween(GregorianCalendar first, GregorianCalendar second) { + GregorianCalendar tmp = new GregorianCalendar(); + tmp.setTime(first.getTime()); + long numDays = (second.getTimeInMillis() - first.getTimeInMillis()) / 1000 / 86400; + tmp.add(Calendar.DAY_OF_MONTH, (int) numDays); + while (tmp.before(second)) { + tmp.add(Calendar.DAY_OF_MONTH, 1); + ++numDays; + } + return numDays; + } + + /** + * Logs bundle content to debug for inspecting the content + * + * @param bundle + * @param bundleName + */ + public static void logDebugBundle(Bundle bundle, String bundleName) { + if (Constants.DEBUG) { + if (bundle != null) { + Set<String> ks = bundle.keySet(); + Iterator<String> iterator = ks.iterator(); + + Log.d(Constants.TAG, "Bundle " + bundleName + ":"); + Log.d(Constants.TAG, "------------------------------"); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = bundle.get(key); + + if (value != null) { + Log.d(Constants.TAG, key + " : " + value.toString()); + } else { + Log.d(Constants.TAG, key + " : null"); + } + } + Log.d(Constants.TAG, "------------------------------"); + } else { + Log.d(Constants.TAG, "Bundle " + bundleName + ": null"); + } + } + } + + /** + * Set actionbar without home button if called from another app + * + * @param activity + */ + public static void setActionBarBackButton(SherlockFragmentActivity activity) { + // set actionbar without home button if called from another app + final ActionBar actionBar = activity.getSupportActionBar(); + Log.d(Constants.TAG, "calling package (only set when using startActivityForResult)=" + + activity.getCallingPackage()); + if (activity.getCallingPackage() != null + && activity.getCallingPackage().equals(Constants.PACKAGE_NAME)) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + } else { + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setHomeButtonEnabled(false); + } + } + + /** + * Check if the calling package has the needed permission to invoke an intent with specific + * restricted actions. + * + * If pkgName is null, this will also deny the use of the given action + * + * @param activity + * @param pkgName + * @param permName + * @param action + * @param restrictedActions + */ + public static void checkPackagePermissionForActions(Activity activity, String pkgName, + String permName, String action, String[] restrictedActions) { + if (action != null) { + PackageManager pkgManager = activity.getPackageManager(); + + for (int i = 0; i < restrictedActions.length; i++) { + if (restrictedActions[i].equals(action)) { + if (pkgName != null + && (pkgManager.checkPermission(permName, pkgName) == PackageManager.PERMISSION_GRANTED || pkgName + .equals(Constants.PACKAGE_NAME))) { + Log.d(Constants.TAG, pkgName + " has permission " + permName + ". Action " + + action + " was granted!"); + } else { + String error = pkgName + " does NOT have permission " + permName + + ". Action " + action + " was NOT granted!"; + Log.e(Constants.TAG, error); + Toast.makeText(activity, activity.getString(R.string.errorMessage, error), + Toast.LENGTH_LONG).show(); + + // end activity + activity.setResult(Activity.RESULT_CANCELED, null); + activity.finish(); + } + } + } + } + } + + /** + * Splits userId string into naming part and email part + * + * @param userId + * @return array with naming (0) and email (1) + */ + public static String[] splitUserId(String userId) { + String[] output = new String[2]; + + String chunks[] = userId.split(" <", 2); + userId = chunks[0]; + if (chunks.length > 1) { + output[1] = "<" + chunks[1]; + } + output[0] = userId; + + return output; + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpConversionHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpConversionHelper.java new file mode 100644 index 000000000..3be4c595c --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpConversionHelper.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.helper; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; + +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPObjectFactory; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPUtil; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + + +public class PgpConversionHelper { + + /** + * Convert from byte[] to PGPKeyRing + * + * @param keysBytes + * @return + */ + public static PGPKeyRing BytesToPGPKeyRing(byte[] keysBytes) { + PGPObjectFactory factory = new PGPObjectFactory(keysBytes); + PGPKeyRing keyRing = null; + try { + if ((keyRing = (PGPKeyRing) factory.nextObject()) == null) { + Log.e(Constants.TAG, "No keys given!"); + } + } catch (IOException e) { + Log.e(Constants.TAG, "Error while converting to PGPKeyRing!", e); + } + + return keyRing; + } + + /** + * Convert from byte[] to ArrayList<PGPSecretKey> + * + * @param keysBytes + * @return + */ + public static ArrayList<PGPSecretKey> BytesToPGPSecretKeyList(byte[] keysBytes) { + PGPSecretKeyRing keyRing = (PGPSecretKeyRing) BytesToPGPKeyRing(keysBytes); + ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>(); + + @SuppressWarnings("unchecked") + Iterator<PGPSecretKey> itr = keyRing.getSecretKeys(); + while (itr.hasNext()) { + keys.add(itr.next()); + } + + return keys; + } + + /** + * Convert from byte[] to PGPSecretKey + * + * Singles keys are encoded as keyRings with one single key in it by Bouncy Castle + * + * @param keysBytes + * @return + */ + public static PGPSecretKey BytesToPGPSecretKey(byte[] keyBytes) { + PGPSecretKey key = BytesToPGPSecretKeyList(keyBytes).get(0); + + return key; + } + + /** + * Convert from ArrayList<PGPSecretKey> to byte[] + * + * @param keys + * @return + */ + public static byte[] PGPSecretKeyArrayListToBytes(ArrayList<PGPSecretKey> keys) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + for (PGPSecretKey key : keys) { + try { + key.encode(os); + } catch (IOException e) { + Log.e(Constants.TAG, "Error while converting ArrayList<PGPSecretKey> to byte[]!", e); + } + } + + return os.toByteArray(); + } + + /** + * Convert from PGPSecretKey to byte[] + * + * @param keysBytes + * @return + */ + public static byte[] PGPSecretKeyToBytes(PGPSecretKey key) { + try { + return key.getEncoded(); + } catch (IOException e) { + Log.e(Constants.TAG, "Encoding failed", e); + + return null; + } + } + + /** + * Convert from PGPSecretKeyRing to byte[] + * + * @param keysBytes + * @return + */ + public static byte[] PGPSecretKeyRingToBytes(PGPSecretKeyRing keyRing) { + try { + return keyRing.getEncoded(); + } catch (IOException e) { + Log.e(Constants.TAG, "Encoding failed", e); + + return null; + } + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpHelper.java new file mode 100644 index 000000000..f59bd58de --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpHelper.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.helper; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.Vector; + +import org.spongycastle.bcpg.sig.KeyFlags; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureSubpacketVector; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.content.Context; + +public class PgpHelper { + + public static Date getCreationDate(PGPPublicKey key) { + return key.getCreationTime(); + } + + public static Date getCreationDate(PGPSecretKey key) { + return key.getPublicKey().getCreationTime(); + } + + @SuppressWarnings("unchecked") + public static PGPPublicKey getMasterKey(PGPPublicKeyRing keyRing) { + if (keyRing == null) { + return null; + } + for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) { + if (key.isMasterKey()) { + return key; + } + } + + return null; + } + + @SuppressWarnings("unchecked") + public static PGPSecretKey getMasterKey(PGPSecretKeyRing keyRing) { + if (keyRing == null) { + return null; + } + for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) { + if (key.isMasterKey()) { + return key; + } + } + + return null; + } + + @SuppressWarnings("unchecked") + public static Vector<PGPPublicKey> getEncryptKeys(PGPPublicKeyRing keyRing) { + Vector<PGPPublicKey> encryptKeys = new Vector<PGPPublicKey>(); + + for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) { + if (isEncryptionKey(key)) { + encryptKeys.add(key); + } + } + + return encryptKeys; + } + + @SuppressWarnings("unchecked") + public static Vector<PGPSecretKey> getSigningKeys(PGPSecretKeyRing keyRing) { + Vector<PGPSecretKey> signingKeys = new Vector<PGPSecretKey>(); + + for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) { + if (isSigningKey(key)) { + signingKeys.add(key); + } + } + + return signingKeys; + } + + public static Vector<PGPPublicKey> getUsableEncryptKeys(PGPPublicKeyRing keyRing) { + Vector<PGPPublicKey> usableKeys = new Vector<PGPPublicKey>(); + Vector<PGPPublicKey> encryptKeys = getEncryptKeys(keyRing); + PGPPublicKey masterKey = null; + for (int i = 0; i < encryptKeys.size(); ++i) { + PGPPublicKey key = encryptKeys.get(i); + if (!isExpired(key)) { + if (key.isMasterKey()) { + masterKey = key; + } else { + usableKeys.add(key); + } + } + } + if (masterKey != null) { + usableKeys.add(masterKey); + } + return usableKeys; + } + + public static boolean isExpired(PGPPublicKey key) { + Date creationDate = getCreationDate(key); + Date expiryDate = getExpiryDate(key); + Date now = new Date(); + if (now.compareTo(creationDate) >= 0 + && (expiryDate == null || now.compareTo(expiryDate) <= 0)) { + return false; + } + return true; + } + + public static boolean isExpired(PGPSecretKey key) { + return isExpired(key.getPublicKey()); + } + + public static Vector<PGPSecretKey> getUsableSigningKeys(PGPSecretKeyRing keyRing) { + Vector<PGPSecretKey> usableKeys = new Vector<PGPSecretKey>(); + Vector<PGPSecretKey> signingKeys = getSigningKeys(keyRing); + PGPSecretKey masterKey = null; + for (int i = 0; i < signingKeys.size(); ++i) { + PGPSecretKey key = signingKeys.get(i); + if (key.isMasterKey()) { + masterKey = key; + } else { + usableKeys.add(key); + } + } + if (masterKey != null) { + usableKeys.add(masterKey); + } + return usableKeys; + } + + public static Date getExpiryDate(PGPPublicKey key) { + Date creationDate = getCreationDate(key); + if (key.getValidDays() == 0) { + // no expiry + return null; + } + Calendar calendar = GregorianCalendar.getInstance(); + calendar.setTime(creationDate); + calendar.add(Calendar.DATE, key.getValidDays()); + Date expiryDate = calendar.getTime(); + + return expiryDate; + } + + public static Date getExpiryDate(PGPSecretKey key) { + return getExpiryDate(key.getPublicKey()); + } + + public static PGPPublicKey getEncryptPublicKey(Context context, long masterKeyId) { + PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(context, + masterKeyId); + if (keyRing == null) { + Log.e(Constants.TAG, "keyRing is null!"); + return null; + } + Vector<PGPPublicKey> encryptKeys = getUsableEncryptKeys(keyRing); + if (encryptKeys.size() == 0) { + Log.e(Constants.TAG, "encryptKeys is null!"); + return null; + } + return encryptKeys.get(0); + } + + public static PGPSecretKey getSigningKey(Context context, long masterKeyId) { + PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(context, + masterKeyId); + if (keyRing == null) { + return null; + } + Vector<PGPSecretKey> signingKeys = getUsableSigningKeys(keyRing); + if (signingKeys.size() == 0) { + return null; + } + return signingKeys.get(0); + } + + @SuppressWarnings("unchecked") + public static String getMainUserId(PGPPublicKey key) { + for (String userId : new IterableIterator<String>(key.getUserIDs())) { + return userId; + } + return null; + } + + @SuppressWarnings("unchecked") + public static String getMainUserId(PGPSecretKey key) { + for (String userId : new IterableIterator<String>(key.getUserIDs())) { + return userId; + } + return null; + } + + public static String getMainUserIdSafe(Context context, PGPPublicKey key) { + String userId = getMainUserId(key); + if (userId == null || userId.equals("")) { + userId = context.getString(R.string.unknownUserId); + } + return userId; + } + + public static String getMainUserIdSafe(Context context, PGPSecretKey key) { + String userId = getMainUserId(key); + if (userId == null || userId.equals("")) { + userId = context.getString(R.string.unknownUserId); + } + return userId; + } + + @SuppressWarnings("unchecked") + public static boolean isEncryptionKey(PGPPublicKey key) { + if (!key.isEncryptionKey()) { + return false; + } + + if (key.getVersion() <= 3) { + // this must be true now + return key.isEncryptionKey(); + } + + // special cases + if (key.getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT) { + return true; + } + + if (key.getAlgorithm() == PGPPublicKey.RSA_ENCRYPT) { + return true; + } + + for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) { + if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) { + continue; + } + PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets(); + + if (hashed != null + && (hashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) { + return true; + } + + PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets(); + + if (unhashed != null + && (unhashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) { + return true; + } + } + return false; + } + + public static boolean isEncryptionKey(PGPSecretKey key) { + return isEncryptionKey(key.getPublicKey()); + } + + @SuppressWarnings("unchecked") + public static boolean isSigningKey(PGPPublicKey key) { + if (key.getVersion() <= 3) { + return true; + } + + // special case + if (key.getAlgorithm() == PGPPublicKey.RSA_SIGN) { + return true; + } + + for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) { + if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) { + continue; + } + PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets(); + + if (hashed != null && (hashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) { + return true; + } + + PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets(); + + if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) { + return true; + } + } + + return false; + } + + public static boolean isSigningKey(PGPSecretKey key) { + return isSigningKey(key.getPublicKey()); + } + + public static String getAlgorithmInfo(PGPPublicKey key) { + return getAlgorithmInfo(key.getAlgorithm(), key.getBitStrength()); + } + + public static String getAlgorithmInfo(PGPSecretKey key) { + return getAlgorithmInfo(key.getPublicKey()); + } + + public static String getAlgorithmInfo(int algorithm, int keySize) { + String algorithmStr = null; + + switch (algorithm) { + case PGPPublicKey.RSA_ENCRYPT: + case PGPPublicKey.RSA_GENERAL: + case PGPPublicKey.RSA_SIGN: { + algorithmStr = "RSA"; + break; + } + + case PGPPublicKey.DSA: { + algorithmStr = "DSA"; + break; + } + + case PGPPublicKey.ELGAMAL_ENCRYPT: + case PGPPublicKey.ELGAMAL_GENERAL: { + algorithmStr = "ElGamal"; + break; + } + + default: { + algorithmStr = "???"; + break; + } + } + return algorithmStr + ", " + keySize + "bit"; + } + + public static String convertFingerprintToHex(byte[] fp) { + String fingerPrint = ""; + for (int i = 0; i < fp.length; ++i) { + if (i != 0 && i % 10 == 0) { + fingerPrint += " "; + } else if (i != 0 && i % 2 == 0) { + fingerPrint += " "; + } + String chunk = Integer.toHexString((fp[i] + 256) % 256).toUpperCase(Locale.US); + while (chunk.length() < 2) { + chunk = "0" + chunk; + } + fingerPrint += chunk; + } + + return fingerPrint; + + } + + public static String getFingerPrint(Context context, long keyId) { + PGPPublicKey key = ProviderHelper.getPGPPublicKeyByKeyId(context, keyId); + // if it is no public key get it from your own keys... + if (key == null) { + PGPSecretKey secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, keyId); + if (secretKey == null) { + Log.e(Constants.TAG, "Key could not be found!"); + return null; + } + key = secretKey.getPublicKey(); + } + + return convertFingerprintToHex(key.getFingerprint()); + } + + public static String getSmallFingerPrint(long keyId) { + String fingerPrint = Long.toHexString(keyId & 0xffffffffL).toUpperCase(Locale.US); + while (fingerPrint.length() < 8) { + fingerPrint = "0" + fingerPrint; + } + return fingerPrint; + } + + public static String keyToHex(long keyId) { + return getSmallFingerPrint(keyId >> 32) + getSmallFingerPrint(keyId); + } + + public static long keyFromHex(String data) { + int len = data.length(); + String s2 = data.substring(len - 8); + String s1 = data.substring(0, len - 8); + return (Long.parseLong(s1, 16) << 32) | Long.parseLong(s2, 16); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpMain.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpMain.java new file mode 100644 index 000000000..26842e4dc --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpMain.java @@ -0,0 +1,1857 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.helper; + +import org.spongycastle.bcpg.ArmoredInputStream; +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.spongycastle.bcpg.BCPGOutputStream; +import org.spongycastle.bcpg.CompressionAlgorithmTags; +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.spongycastle.bcpg.sig.KeyFlags; +import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.spongycastle.jce.spec.ElGamalParameterSpec; +import org.spongycastle.openpgp.PGPCompressedData; +import org.spongycastle.openpgp.PGPCompressedDataGenerator; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.spongycastle.openpgp.PGPEncryptedDataGenerator; +import org.spongycastle.openpgp.PGPEncryptedDataList; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPKeyPair; +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPKeyRingGenerator; +import org.spongycastle.openpgp.PGPLiteralData; +import org.spongycastle.openpgp.PGPLiteralDataGenerator; +import org.spongycastle.openpgp.PGPObjectFactory; +import org.spongycastle.openpgp.PGPOnePassSignature; +import org.spongycastle.openpgp.PGPOnePassSignatureList; +import org.spongycastle.openpgp.PGPPBEEncryptedData; +import org.spongycastle.openpgp.PGPPrivateKey; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyEncryptedData; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPPublicKeyRingCollection; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureGenerator; +import org.spongycastle.openpgp.PGPSignatureList; +import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.spongycastle.openpgp.PGPSignatureSubpacketVector; +import org.spongycastle.openpgp.PGPUtil; +import org.spongycastle.openpgp.PGPV3SignatureGenerator; +import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.spongycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.spongycastle.openpgp.operator.PGPContentSignerBuilder; +import org.spongycastle.openpgp.operator.PGPDigestCalculator; +import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyPair; +import org.spongycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; +import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.util.HkpKeyServer; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.PositionAwareInputStream; +import org.sufficientlysecure.keychain.util.Primes; +import org.sufficientlysecure.keychain.util.ProgressDialogUpdater; +import org.sufficientlysecure.keychain.util.KeyServer.AddKeyException; +import org.sufficientlysecure.keychain.R; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.os.Environment; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.SignatureException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.regex.Pattern; + +/** + * TODO: + * + * - Separate this file into different helpers + * + */ +public class PgpMain { + + static { + // register spongy castle provider + Security.addProvider(new BouncyCastleProvider()); + } + + // Not BC due to the use of Spongy Castle for Android + public static final String BOUNCY_CASTLE_PROVIDER_NAME = "SC"; + + private static final int[] PREFERRED_SYMMETRIC_ALGORITHMS = new int[] { + SymmetricKeyAlgorithmTags.AES_256, SymmetricKeyAlgorithmTags.AES_192, + SymmetricKeyAlgorithmTags.AES_128, SymmetricKeyAlgorithmTags.CAST5, + SymmetricKeyAlgorithmTags.TRIPLE_DES }; + private static final int[] PREFERRED_HASH_ALGORITHMS = new int[] { HashAlgorithmTags.SHA1, + HashAlgorithmTags.SHA256, HashAlgorithmTags.RIPEMD160 }; + private static final int[] PREFERRED_COMPRESSION_ALGORITHMS = new int[] { + CompressionAlgorithmTags.ZLIB, CompressionAlgorithmTags.BZIP2, + CompressionAlgorithmTags.ZIP }; + + public static Pattern PGP_MESSAGE = Pattern.compile( + ".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL); + + public static Pattern PGP_SIGNED_MESSAGE = Pattern + .compile( + ".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*", + Pattern.DOTALL); + + public static Pattern PGP_PUBLIC_KEY = Pattern.compile( + ".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----).*", + Pattern.DOTALL); + + private static String mEditPassPhrase = null; + + public static class PgpGeneralException extends Exception { + static final long serialVersionUID = 0xf812773342L; + + public PgpGeneralException(String message) { + super(message); + } + } + + public static class NoAsymmetricEncryptionException extends Exception { + static final long serialVersionUID = 0xf812773343L; + + public NoAsymmetricEncryptionException() { + super(); + } + } + + public static void setEditPassPhrase(String passPhrase) { + mEditPassPhrase = passPhrase; + } + + public static String getEditPassPhrase() { + return mEditPassPhrase; + } + + public static void updateProgress(ProgressDialogUpdater progress, int message, int current, + int total) { + if (progress != null) { + progress.setProgress(message, current, total); + } + } + + public static void updateProgress(ProgressDialogUpdater progress, int current, int total) { + if (progress != null) { + progress.setProgress(current, total); + } + } + + /** + * Creates new secret key. The returned PGPSecretKeyRing contains only one newly generated key + * when this key is the new masterkey. If a masterkey is supplied in the parameters + * PGPSecretKeyRing contains the masterkey and the new key as a subkey (certified by the + * masterkey). + * + * @param context + * @param algorithmChoice + * @param keySize + * @param passPhrase + * @param masterSecretKey + * @return + * @throws NoSuchAlgorithmException + * @throws PGPException + * @throws NoSuchProviderException + * @throws PgpGeneralException + * @throws InvalidAlgorithmParameterException + */ + public static PGPSecretKeyRing createKey(Context context, int algorithmChoice, int keySize, + String passPhrase, PGPSecretKey masterSecretKey) throws NoSuchAlgorithmException, + PGPException, NoSuchProviderException, PgpGeneralException, + InvalidAlgorithmParameterException { + + if (keySize < 512) { + throw new PgpGeneralException(context.getString(R.string.error_keySizeMinimum512bit)); + } + + if (passPhrase == null) { + passPhrase = ""; + } + + int algorithm = 0; + KeyPairGenerator keyGen = null; + + switch (algorithmChoice) { + case Id.choice.algorithm.dsa: { + keyGen = KeyPairGenerator.getInstance("DSA", BOUNCY_CASTLE_PROVIDER_NAME); + keyGen.initialize(keySize, new SecureRandom()); + algorithm = PGPPublicKey.DSA; + break; + } + + case Id.choice.algorithm.elgamal: { + if (masterSecretKey == null) { + throw new PgpGeneralException( + context.getString(R.string.error_masterKeyMustNotBeElGamal)); + } + keyGen = KeyPairGenerator.getInstance("ElGamal", BOUNCY_CASTLE_PROVIDER_NAME); + BigInteger p = Primes.getBestPrime(keySize); + BigInteger g = new BigInteger("2"); + + ElGamalParameterSpec elParams = new ElGamalParameterSpec(p, g); + + keyGen.initialize(elParams); + algorithm = PGPPublicKey.ELGAMAL_ENCRYPT; + break; + } + + case Id.choice.algorithm.rsa: { + keyGen = KeyPairGenerator.getInstance("RSA", BOUNCY_CASTLE_PROVIDER_NAME); + keyGen.initialize(keySize, new SecureRandom()); + + algorithm = PGPPublicKey.RSA_GENERAL; + break; + } + + default: { + throw new PgpGeneralException(context.getString(R.string.error_unknownAlgorithmChoice)); + } + } + + // build new key pair + PGPKeyPair keyPair = new JcaPGPKeyPair(algorithm, keyGen.generateKeyPair(), new Date()); + + // define hashing and signing algos + PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get( + HashAlgorithmTags.SHA1); + + // Build key encrypter and decrypter based on passphrase + PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder( + PGPEncryptedData.CAST5, sha1Calc).setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build( + passPhrase.toCharArray()); + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + BOUNCY_CASTLE_PROVIDER_NAME).build(passPhrase.toCharArray()); + + PGPKeyRingGenerator ringGen = null; + PGPContentSignerBuilder certificationSignerBuilder = null; + if (masterSecretKey == null) { + certificationSignerBuilder = new JcaPGPContentSignerBuilder(keyPair.getPublicKey() + .getAlgorithm(), HashAlgorithmTags.SHA1); + + // build keyRing with only this one master key in it! + ringGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, keyPair, "", + sha1Calc, null, null, certificationSignerBuilder, keyEncryptor); + } else { + PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey(); + PGPPrivateKey masterPrivateKey = masterSecretKey.extractPrivateKey(keyDecryptor); + PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey); + + certificationSignerBuilder = new JcaPGPContentSignerBuilder(masterKeyPair + .getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1); + + // build keyRing with master key and new key as subkey (certified by masterkey) + ringGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, masterKeyPair, + "", sha1Calc, null, null, certificationSignerBuilder, keyEncryptor); + + ringGen.addSubKey(keyPair); + } + + PGPSecretKeyRing secKeyRing = ringGen.generateSecretKeyRing(); + + return secKeyRing; + } + + public static void buildSecretKey(Context context, ArrayList<String> userIds, + ArrayList<PGPSecretKey> keys, ArrayList<Integer> keysUsages, long masterKeyId, + String oldPassPhrase, String newPassPhrase, ProgressDialogUpdater progress) + throws PgpGeneralException, NoSuchProviderException, PGPException, + NoSuchAlgorithmException, SignatureException, IOException { + + Log.d(Constants.TAG, "userIds: " + userIds.toString()); + + updateProgress(progress, R.string.progress_buildingKey, 0, 100); + + if (oldPassPhrase == null) { + oldPassPhrase = ""; + } + if (newPassPhrase == null) { + newPassPhrase = ""; + } + + updateProgress(progress, R.string.progress_preparingMasterKey, 10, 100); + + int usageId = keysUsages.get(0); + boolean canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt); + boolean canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt); + + String mainUserId = userIds.get(0); + + PGPSecretKey masterKey = keys.get(0); + + // this removes all userIds and certifications previously attached to the masterPublicKey + PGPPublicKey tmpKey = masterKey.getPublicKey(); + PGPPublicKey masterPublicKey = new PGPPublicKey(tmpKey.getAlgorithm(), + tmpKey.getKey(new BouncyCastleProvider()), tmpKey.getCreationTime()); + + // already done by code above: + // PGPPublicKey masterPublicKey = masterKey.getPublicKey(); + // // Somehow, the PGPPublicKey already has an empty certification attached to it when the + // // keyRing is generated the first time, we remove that when it exists, before adding the + // new + // // ones + // PGPPublicKey masterPublicKeyRmCert = PGPPublicKey.removeCertification(masterPublicKey, + // ""); + // if (masterPublicKeyRmCert != null) { + // masterPublicKey = masterPublicKeyRmCert; + // } + + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray()); + PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor); + + updateProgress(progress, R.string.progress_certifyingMasterKey, 20, 100); + + for (String userId : userIds) { + PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( + masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1) + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME); + PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder); + + sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey); + + PGPSignature certification = sGen.generateCertification(userId, masterPublicKey); + + masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, certification); + } + + // TODO: cross-certify the master key with every sub key (APG 1) + + PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey); + + PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator(); + PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator(); + + int keyFlags = KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA; + if (canEncrypt) { + keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE; + } + hashedPacketsGen.setKeyFlags(true, keyFlags); + + hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS); + hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS); + hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS); + + // TODO: this doesn't work quite right yet (APG 1) + // if (keyEditor.getExpiryDate() != null) { + // GregorianCalendar creationDate = new GregorianCalendar(); + // creationDate.setTime(getCreationDate(masterKey)); + // GregorianCalendar expiryDate = keyEditor.getExpiryDate(); + // long numDays = Utils.getNumDaysBetween(creationDate, expiryDate); + // if (numDays <= 0) { + // throw new GeneralException( + // context.getString(R.string.error_expiryMustComeAfterCreation)); + // } + // hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400); + // } + + updateProgress(progress, R.string.progress_buildingMasterKeyRing, 30, 100); + + // define hashing and signing algos + PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get( + HashAlgorithmTags.SHA1); + PGPContentSignerBuilder certificationSignerBuilder = new JcaPGPContentSignerBuilder( + masterKeyPair.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1); + + // Build key encrypter based on passphrase + PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder( + PGPEncryptedData.CAST5, sha1Calc).setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build( + newPassPhrase.toCharArray()); + + PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, + masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(), + unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor); + + updateProgress(progress, R.string.progress_addingSubKeys, 40, 100); + + for (int i = 1; i < keys.size(); ++i) { + updateProgress(progress, 40 + 50 * (i - 1) / (keys.size() - 1), 100); + + PGPSecretKey subKey = keys.get(i); + PGPPublicKey subPublicKey = subKey.getPublicKey(); + + PBESecretKeyDecryptor keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder() + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray()); + PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(keyDecryptor2); + + // TODO: now used without algorithm and creation time?! (APG 1) + PGPKeyPair subKeyPair = new PGPKeyPair(subPublicKey, subPrivateKey); + + hashedPacketsGen = new PGPSignatureSubpacketGenerator(); + unhashedPacketsGen = new PGPSignatureSubpacketGenerator(); + + keyFlags = 0; + + usageId = keysUsages.get(i); + canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt); + canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt); + if (canSign) { + keyFlags |= KeyFlags.SIGN_DATA; + } + if (canEncrypt) { + keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE; + } + hashedPacketsGen.setKeyFlags(true, keyFlags); + + // TODO: this doesn't work quite right yet (APG 1) + // if (keyEditor.getExpiryDate() != null) { + // GregorianCalendar creationDate = new GregorianCalendar(); + // creationDate.setTime(getCreationDate(masterKey)); + // GregorianCalendar expiryDate = keyEditor.getExpiryDate(); + // long numDays = Utils.getNumDaysBetween(creationDate, expiryDate); + // if (numDays <= 0) { + // throw new GeneralException( + // context.getString(R.string.error_expiryMustComeAfterCreation)); + // } + // hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400); + // } + + keyGen.addSubKey(subKeyPair, hashedPacketsGen.generate(), unhashedPacketsGen.generate()); + } + + PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing(); + PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing(); + + updateProgress(progress, R.string.progress_savingKeyRing, 90, 100); + + ProviderHelper.saveKeyRing(context, secretKeyRing); + ProviderHelper.saveKeyRing(context, publicKeyRing); + + updateProgress(progress, R.string.progress_done, 100, 100); + } + + /** + * TODO: implement Id.return_value.updated as status when key already existed + * + * @param context + * @param keyring + * @return + */ + public static int storeKeyRingInCache(Context context, PGPKeyRing keyring) { + int status = Integer.MIN_VALUE; // out of bounds value (Id.return_value.*) + try { + if (keyring instanceof PGPSecretKeyRing) { + PGPSecretKeyRing secretKeyRing = (PGPSecretKeyRing) keyring; + boolean save = true; + try { + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder() + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(new char[] {}); + PGPPrivateKey testKey = secretKeyRing.getSecretKey().extractPrivateKey( + keyDecryptor); + if (testKey == null) { + // this is bad, something is very wrong... likely a --export-secret-subkeys + // export + save = false; + status = Id.return_value.bad; + } + } catch (PGPException e) { + // all good if this fails, we likely didn't use the right password + } + + if (save) { + ProviderHelper.saveKeyRing(context, secretKeyRing); + // TODO: remove status returns, use exceptions! + status = Id.return_value.ok; + } + } else if (keyring instanceof PGPPublicKeyRing) { + PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring; + ProviderHelper.saveKeyRing(context, publicKeyRing); + // TODO: remove status returns, use exceptions! + status = Id.return_value.ok; + } + } catch (IOException e) { + status = Id.return_value.error; + } + + return status; + } + + public static boolean uploadKeyRingToServer(HkpKeyServer server, PGPPublicKeyRing keyring) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ArmoredOutputStream aos = new ArmoredOutputStream(bos); + try { + aos.write(keyring.getEncoded()); + aos.close(); + + String armouredKey = bos.toString("UTF-8"); + server.add(armouredKey); + + return true; + } catch (IOException e) { + return false; + } catch (AddKeyException e) { + // TODO: tell the user? + return false; + } finally { + try { + bos.close(); + } catch (IOException e) { + } + } + } + + public static Bundle importKeyRings(Context context, InputData data, + ProgressDialogUpdater progress) throws PgpGeneralException, FileNotFoundException, + PGPException, IOException { + Bundle returnData = new Bundle(); + + updateProgress(progress, R.string.progress_importingSecretKeys, 0, 100); + + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + throw new PgpGeneralException(context.getString(R.string.error_externalStorageNotReady)); + } + + PositionAwareInputStream progressIn = new PositionAwareInputStream(data.getInputStream()); + + // need to have access to the bufferedInput, so we can reuse it for the possible + // PGPObject chunks after the first one, e.g. files with several consecutive ASCII + // armour blocks + BufferedInputStream bufferedInput = new BufferedInputStream(progressIn); + int newKeys = 0; + int oldKeys = 0; + int badKeys = 0; + try { + + // read all available blocks... (asc files can contain many blocks with BEGIN END) + while (bufferedInput.available() > 0) { + InputStream in = PGPUtil.getDecoderStream(bufferedInput); + PGPObjectFactory objectFactory = new PGPObjectFactory(in); + + // go through all objects in this block + Object obj; + while ((obj = objectFactory.nextObject()) != null) { + Log.d(Constants.TAG, "Found class: " + obj.getClass()); + + if (obj instanceof PGPKeyRing) { + PGPKeyRing keyring = (PGPKeyRing) obj; + + int status = Integer.MIN_VALUE; // out of bounds value + + status = storeKeyRingInCache(context, keyring); + + if (status == Id.return_value.error) { + throw new PgpGeneralException( + context.getString(R.string.error_savingKeys)); + } + + // update the counts to display to the user at the end + if (status == Id.return_value.updated) { + ++oldKeys; + } else if (status == Id.return_value.ok) { + ++newKeys; + } else if (status == Id.return_value.bad) { + ++badKeys; + } + + updateProgress(progress, + (int) (100 * progressIn.position() / data.getSize()), 100); + } else { + Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!"); + } + } + } + } catch (Exception e) { + Log.e(Constants.TAG, "Exception on parsing key file!", e); + } + + returnData.putInt(KeychainIntentService.RESULT_IMPORT_ADDED, newKeys); + returnData.putInt(KeychainIntentService.RESULT_IMPORT_UPDATED, oldKeys); + returnData.putInt(KeychainIntentService.RESULT_IMPORT_BAD, badKeys); + + updateProgress(progress, R.string.progress_done, 100, 100); + + return returnData; + } + + public static Bundle exportKeyRings(Context context, ArrayList<Long> keyRingMasterKeyIds, + int keyType, OutputStream outStream, ProgressDialogUpdater progress) + throws PgpGeneralException, FileNotFoundException, PGPException, IOException { + Bundle returnData = new Bundle(); + + if (keyRingMasterKeyIds.size() == 1) { + updateProgress(progress, R.string.progress_exportingKey, 0, 100); + } else { + updateProgress(progress, R.string.progress_exportingKeys, 0, 100); + } + + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + throw new PgpGeneralException(context.getString(R.string.error_externalStorageNotReady)); + } + + // export public keyrings... + ArmoredOutputStream outPub = new ArmoredOutputStream(outStream); + outPub.setHeader("Version", getFullVersion(context)); + + int numKeys = 0; + for (int i = 0; i < keyRingMasterKeyIds.size(); ++i) { + // double the needed time if exporting both public and secret parts + if (keyType == Id.type.secret_key) { + updateProgress(progress, i * 100 / keyRingMasterKeyIds.size() / 2, 100); + } else { + updateProgress(progress, i * 100 / keyRingMasterKeyIds.size(), 100); + } + + PGPPublicKeyRing publicKeyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId( + context, keyRingMasterKeyIds.get(i)); + + if (publicKeyRing != null) { + publicKeyRing.encode(outPub); + } + ++numKeys; + } + outPub.close(); + + // if we export secret keyrings, append all secret parts after the public parts + if (keyType == Id.type.secret_key) { + ArmoredOutputStream outSec = new ArmoredOutputStream(outStream); + outSec.setHeader("Version", getFullVersion(context)); + + for (int i = 0; i < keyRingMasterKeyIds.size(); ++i) { + updateProgress(progress, i * 100 / keyRingMasterKeyIds.size() / 2, 100); + + PGPSecretKeyRing secretKeyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId( + context, keyRingMasterKeyIds.get(i)); + + if (secretKeyRing != null) { + secretKeyRing.encode(outSec); + } + } + outSec.close(); + } + + returnData.putInt(KeychainIntentService.RESULT_EXPORT, numKeys); + + updateProgress(progress, R.string.progress_done, 100, 100); + + return returnData; + } + + /** + * Encrypt and Sign data + * + * @param context + * @param progress + * @param data + * @param outStream + * @param useAsciiArmor + * @param compression + * @param encryptionKeyIds + * @param symmetricEncryptionAlgorithm + * @param encryptionPassphrase + * @param signatureKeyId + * @param signatureHashAlgorithm + * @param signatureForceV3 + * @param signaturePassphrase + * @throws IOException + * @throws PgpGeneralException + * @throws PGPException + * @throws NoSuchProviderException + * @throws NoSuchAlgorithmException + * @throws SignatureException + */ + public static void encryptAndSign(Context context, ProgressDialogUpdater progress, + InputData data, OutputStream outStream, boolean useAsciiArmor, int compression, + long encryptionKeyIds[], String encryptionPassphrase, int symmetricEncryptionAlgorithm, + long signatureKeyId, int signatureHashAlgorithm, boolean signatureForceV3, + String signaturePassphrase) throws IOException, PgpGeneralException, PGPException, + NoSuchProviderException, NoSuchAlgorithmException, SignatureException { + + if (encryptionKeyIds == null) { + encryptionKeyIds = new long[0]; + } + + ArmoredOutputStream armorOut = null; + OutputStream out = null; + OutputStream encryptOut = null; + if (useAsciiArmor) { + armorOut = new ArmoredOutputStream(outStream); + armorOut.setHeader("Version", getFullVersion(context)); + out = armorOut; + } else { + out = outStream; + } + PGPSecretKey signingKey = null; + PGPSecretKeyRing signingKeyRing = null; + PGPPrivateKey signaturePrivateKey = null; + + if (encryptionKeyIds.length == 0 && encryptionPassphrase == null) { + throw new PgpGeneralException( + context.getString(R.string.error_noEncryptionKeysOrPassPhrase)); + } + + if (signatureKeyId != Id.key.none) { + signingKeyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(context, signatureKeyId); + signingKey = PgpHelper.getSigningKey(context, signatureKeyId); + if (signingKey == null) { + throw new PgpGeneralException(context.getString(R.string.error_signatureFailed)); + } + + if (signaturePassphrase == null) { + throw new PgpGeneralException( + context.getString(R.string.error_noSignaturePassPhrase)); + } + + updateProgress(progress, R.string.progress_extractingSignatureKey, 0, 100); + + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + BOUNCY_CASTLE_PROVIDER_NAME).build(signaturePassphrase.toCharArray()); + signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor); + if (signaturePrivateKey == null) { + throw new PgpGeneralException( + context.getString(R.string.error_couldNotExtractPrivateKey)); + } + } + updateProgress(progress, R.string.progress_preparingStreams, 5, 100); + + // encrypt and compress input file content + JcePGPDataEncryptorBuilder encryptorBuilder = new JcePGPDataEncryptorBuilder( + symmetricEncryptionAlgorithm).setProvider(BOUNCY_CASTLE_PROVIDER_NAME) + .setWithIntegrityPacket(true); + + PGPEncryptedDataGenerator cPk = new PGPEncryptedDataGenerator(encryptorBuilder); + + if (encryptionKeyIds.length == 0) { + // Symmetric encryption + Log.d(Constants.TAG, "encryptionKeyIds length is 0 -> symmetric encryption"); + + JcePBEKeyEncryptionMethodGenerator symmetricEncryptionGenerator = new JcePBEKeyEncryptionMethodGenerator( + encryptionPassphrase.toCharArray()); + cPk.addMethod(symmetricEncryptionGenerator); + } else { + // Asymmetric encryption + for (int i = 0; i < encryptionKeyIds.length; ++i) { + PGPPublicKey key = PgpHelper.getEncryptPublicKey(context, encryptionKeyIds[i]); + if (key != null) { + + JcePublicKeyKeyEncryptionMethodGenerator pubKeyEncryptionGenerator = new JcePublicKeyKeyEncryptionMethodGenerator( + key); + cPk.addMethod(pubKeyEncryptionGenerator); + } + } + } + encryptOut = cPk.open(out, new byte[1 << 16]); + + PGPSignatureGenerator signatureGenerator = null; + PGPV3SignatureGenerator signatureV3Generator = null; + + if (signatureKeyId != Id.key.none) { + updateProgress(progress, R.string.progress_preparingSignature, 10, 100); + + // content signer based on signing key algorithm and choosen hash algorithm + JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder( + signingKey.getPublicKey().getAlgorithm(), signatureHashAlgorithm) + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME); + + if (signatureForceV3) { + signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder); + signatureV3Generator.init(PGPSignature.BINARY_DOCUMENT, signaturePrivateKey); + } else { + signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); + signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, signaturePrivateKey); + + String userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signingKeyRing)); + PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); + spGen.setSignerUserID(false, userId); + signatureGenerator.setHashedSubpackets(spGen.generate()); + } + } + + PGPCompressedDataGenerator compressGen = null; + BCPGOutputStream bcpgOut = null; + if (compression == Id.choice.compression.none) { + bcpgOut = new BCPGOutputStream(encryptOut); + } else { + compressGen = new PGPCompressedDataGenerator(compression); + bcpgOut = new BCPGOutputStream(compressGen.open(encryptOut)); + } + if (signatureKeyId != Id.key.none) { + if (signatureForceV3) { + signatureV3Generator.generateOnePassVersion(false).encode(bcpgOut); + } else { + signatureGenerator.generateOnePassVersion(false).encode(bcpgOut); + } + } + + PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator(); + // file name not needed, so empty string + OutputStream pOut = literalGen.open(bcpgOut, PGPLiteralData.BINARY, "", new Date(), + new byte[1 << 16]); + updateProgress(progress, R.string.progress_encrypting, 20, 100); + + long done = 0; + int n = 0; + byte[] buffer = new byte[1 << 16]; + InputStream in = data.getInputStream(); + while ((n = in.read(buffer)) > 0) { + pOut.write(buffer, 0, n); + if (signatureKeyId != Id.key.none) { + if (signatureForceV3) { + signatureV3Generator.update(buffer, 0, n); + } else { + signatureGenerator.update(buffer, 0, n); + } + } + done += n; + if (data.getSize() != 0) { + updateProgress(progress, (int) (20 + (95 - 20) * done / data.getSize()), 100); + } + } + + literalGen.close(); + + if (signatureKeyId != Id.key.none) { + updateProgress(progress, R.string.progress_generatingSignature, 95, 100); + if (signatureForceV3) { + signatureV3Generator.generate().encode(pOut); + } else { + signatureGenerator.generate().encode(pOut); + } + } + if (compressGen != null) { + compressGen.close(); + } + encryptOut.close(); + if (useAsciiArmor) { + armorOut.close(); + } + + updateProgress(progress, R.string.progress_done, 100, 100); + } + + public static void signText(Context context, ProgressDialogUpdater progress, InputData data, + OutputStream outStream, long signatureKeyId, String signaturePassphrase, + int signatureHashAlgorithm, boolean forceV3Signature) throws PgpGeneralException, + PGPException, IOException, NoSuchAlgorithmException, SignatureException { + + ArmoredOutputStream armorOut = new ArmoredOutputStream(outStream); + armorOut.setHeader("Version", getFullVersion(context)); + + PGPSecretKey signingKey = null; + PGPSecretKeyRing signingKeyRing = null; + PGPPrivateKey signaturePrivateKey = null; + + if (signatureKeyId == 0) { + armorOut.close(); + throw new PgpGeneralException(context.getString(R.string.error_noSignatureKey)); + } + + signingKeyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(context, signatureKeyId); + signingKey = PgpHelper.getSigningKey(context, signatureKeyId); + if (signingKey == null) { + armorOut.close(); + throw new PgpGeneralException(context.getString(R.string.error_signatureFailed)); + } + + if (signaturePassphrase == null) { + armorOut.close(); + throw new PgpGeneralException(context.getString(R.string.error_noSignaturePassPhrase)); + } + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + BOUNCY_CASTLE_PROVIDER_NAME).build(signaturePassphrase.toCharArray()); + signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor); + if (signaturePrivateKey == null) { + armorOut.close(); + throw new PgpGeneralException( + context.getString(R.string.error_couldNotExtractPrivateKey)); + } + updateProgress(progress, R.string.progress_preparingStreams, 0, 100); + + updateProgress(progress, R.string.progress_preparingSignature, 30, 100); + + PGPSignatureGenerator signatureGenerator = null; + PGPV3SignatureGenerator signatureV3Generator = null; + + // content signer based on signing key algorithm and choosen hash algorithm + JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(signingKey + .getPublicKey().getAlgorithm(), signatureHashAlgorithm) + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME); + + if (forceV3Signature) { + signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder); + signatureV3Generator.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey); + } else { + signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); + signatureGenerator.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey); + + PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); + String userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signingKeyRing)); + spGen.setSignerUserID(false, userId); + signatureGenerator.setHashedSubpackets(spGen.generate()); + } + + updateProgress(progress, R.string.progress_signing, 40, 100); + + armorOut.beginClearText(signatureHashAlgorithm); + + InputStream inStream = data.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(inStream)); + + final byte[] newline = "\r\n".getBytes("UTF-8"); + + if (forceV3Signature) { + processLine(reader.readLine(), armorOut, signatureV3Generator); + } else { + processLine(reader.readLine(), armorOut, signatureGenerator); + } + + while (true) { + final String line = reader.readLine(); + + if (line == null) { + armorOut.write(newline); + break; + } + + armorOut.write(newline); + if (forceV3Signature) { + signatureV3Generator.update(newline); + processLine(line, armorOut, signatureV3Generator); + } else { + signatureGenerator.update(newline); + processLine(line, armorOut, signatureGenerator); + } + } + + armorOut.endClearText(); + + BCPGOutputStream bOut = new BCPGOutputStream(armorOut); + if (forceV3Signature) { + signatureV3Generator.generate().encode(bOut); + } else { + signatureGenerator.generate().encode(bOut); + } + armorOut.close(); + + updateProgress(progress, R.string.progress_done, 100, 100); + } + + public static void generateSignature(Context context, ProgressDialogUpdater progress, + InputData data, OutputStream outStream, boolean armored, boolean binary, + long signatureKeyId, String signaturePassPhrase, int hashAlgorithm, + boolean forceV3Signature) throws PgpGeneralException, PGPException, IOException, + NoSuchAlgorithmException, SignatureException { + + OutputStream out = null; + + // Ascii Armor (Base64) + ArmoredOutputStream armorOut = null; + if (armored) { + armorOut = new ArmoredOutputStream(outStream); + armorOut.setHeader("Version", getFullVersion(context)); + out = armorOut; + } else { + out = outStream; + } + + PGPSecretKey signingKey = null; + PGPSecretKeyRing signingKeyRing = null; + PGPPrivateKey signaturePrivateKey = null; + + if (signatureKeyId == 0) { + throw new PgpGeneralException(context.getString(R.string.error_noSignatureKey)); + } + + signingKeyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(context, signatureKeyId); + signingKey = PgpHelper.getSigningKey(context, signatureKeyId); + if (signingKey == null) { + throw new PgpGeneralException(context.getString(R.string.error_signatureFailed)); + } + + if (signaturePassPhrase == null) { + throw new PgpGeneralException(context.getString(R.string.error_noSignaturePassPhrase)); + } + + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + BOUNCY_CASTLE_PROVIDER_NAME).build(signaturePassPhrase.toCharArray()); + signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor); + if (signaturePrivateKey == null) { + throw new PgpGeneralException( + context.getString(R.string.error_couldNotExtractPrivateKey)); + } + updateProgress(progress, R.string.progress_preparingStreams, 0, 100); + + updateProgress(progress, R.string.progress_preparingSignature, 30, 100); + + PGPSignatureGenerator signatureGenerator = null; + PGPV3SignatureGenerator signatureV3Generator = null; + + int type = PGPSignature.CANONICAL_TEXT_DOCUMENT; + if (binary) { + type = PGPSignature.BINARY_DOCUMENT; + } + + // content signer based on signing key algorithm and choosen hash algorithm + JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(signingKey + .getPublicKey().getAlgorithm(), hashAlgorithm) + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME); + + if (forceV3Signature) { + signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder); + signatureV3Generator.init(type, signaturePrivateKey); + } else { + signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); + signatureGenerator.init(type, signaturePrivateKey); + + PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); + String userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signingKeyRing)); + spGen.setSignerUserID(false, userId); + signatureGenerator.setHashedSubpackets(spGen.generate()); + } + + updateProgress(progress, R.string.progress_signing, 40, 100); + + InputStream inStream = data.getInputStream(); + if (binary) { + byte[] buffer = new byte[1 << 16]; + int n = 0; + while ((n = inStream.read(buffer)) > 0) { + if (forceV3Signature) { + signatureV3Generator.update(buffer, 0, n); + } else { + signatureGenerator.update(buffer, 0, n); + } + } + } else { + final BufferedReader reader = new BufferedReader(new InputStreamReader(inStream)); + final byte[] newline = "\r\n".getBytes("UTF-8"); + + while (true) { + final String line = reader.readLine(); + + if (line == null) { + break; + } + + if (forceV3Signature) { + processLine(line, null, signatureV3Generator); + signatureV3Generator.update(newline); + } else { + processLine(line, null, signatureGenerator); + signatureGenerator.update(newline); + } + } + } + + BCPGOutputStream bOut = new BCPGOutputStream(out); + if (forceV3Signature) { + signatureV3Generator.generate().encode(bOut); + } else { + signatureGenerator.generate().encode(bOut); + } + out.close(); + outStream.close(); + + if (progress != null) + progress.setProgress(R.string.progress_done, 100, 100); + } + + public static PGPPublicKeyRing signKey(Context context, long masterKeyId, long pubKeyId, + String passphrase) throws PgpGeneralException, NoSuchAlgorithmException, + NoSuchProviderException, PGPException, SignatureException { + if (passphrase == null || passphrase.length() <= 0) { + throw new PgpGeneralException("Unable to obtain passphrase"); + } else { + PGPPublicKeyRing pubring = ProviderHelper.getPGPPublicKeyRingByKeyId(context, pubKeyId); + + PGPSecretKey signingKey = PgpHelper.getSigningKey(context, masterKeyId); + if (signingKey == null) { + throw new PgpGeneralException(context.getString(R.string.error_signatureFailed)); + } + + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray()); + PGPPrivateKey signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor); + if (signaturePrivateKey == null) { + throw new PgpGeneralException( + context.getString(R.string.error_couldNotExtractPrivateKey)); + } + + // TODO: SHA256 fixed? + JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder( + signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256) + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME); + + PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( + contentSignerBuilder); + + signatureGenerator.init(PGPSignature.DIRECT_KEY, signaturePrivateKey); + + PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); + + PGPSignatureSubpacketVector packetVector = spGen.generate(); + signatureGenerator.setHashedSubpackets(packetVector); + + PGPPublicKey signedKey = PGPPublicKey.addCertification(pubring.getPublicKey(pubKeyId), + signatureGenerator.generate()); + pubring = PGPPublicKeyRing.insertPublicKey(pubring, signedKey); + + return pubring; + } + } + + public static long getDecryptionKeyId(Context context, InputStream inputStream) + throws PgpGeneralException, NoAsymmetricEncryptionException, IOException { + InputStream in = PGPUtil.getDecoderStream(inputStream); + PGPObjectFactory pgpF = new PGPObjectFactory(in); + PGPEncryptedDataList enc; + Object o = pgpF.nextObject(); + + // the first object might be a PGP marker packet. + if (o instanceof PGPEncryptedDataList) { + enc = (PGPEncryptedDataList) o; + } else { + enc = (PGPEncryptedDataList) pgpF.nextObject(); + } + + if (enc == null) { + throw new PgpGeneralException(context.getString(R.string.error_invalidData)); + } + + // TODO: currently we always only look at the first known key + // find the secret key + PGPSecretKey secretKey = null; + Iterator<?> it = enc.getEncryptedDataObjects(); + boolean gotAsymmetricEncryption = false; + while (it.hasNext()) { + Object obj = it.next(); + if (obj instanceof PGPPublicKeyEncryptedData) { + gotAsymmetricEncryption = true; + PGPPublicKeyEncryptedData pbe = (PGPPublicKeyEncryptedData) obj; + secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, pbe.getKeyID()); + if (secretKey != null) { + break; + } + } + } + + if (!gotAsymmetricEncryption) { + throw new NoAsymmetricEncryptionException(); + } + + if (secretKey == null) { + return Id.key.none; + } + + return secretKey.getKeyID(); + } + + public static boolean hasSymmetricEncryption(Context context, InputStream inputStream) + throws PgpGeneralException, IOException { + InputStream in = PGPUtil.getDecoderStream(inputStream); + PGPObjectFactory pgpF = new PGPObjectFactory(in); + PGPEncryptedDataList enc; + Object o = pgpF.nextObject(); + + // the first object might be a PGP marker packet. + if (o instanceof PGPEncryptedDataList) { + enc = (PGPEncryptedDataList) o; + } else { + enc = (PGPEncryptedDataList) pgpF.nextObject(); + } + + if (enc == null) { + throw new PgpGeneralException(context.getString(R.string.error_invalidData)); + } + + Iterator<?> it = enc.getEncryptedDataObjects(); + while (it.hasNext()) { + Object obj = it.next(); + if (obj instanceof PGPPBEEncryptedData) { + return true; + } + } + + return false; + } + + public static Bundle decryptAndVerify(Context context, ProgressDialogUpdater progress, + InputData data, OutputStream outStream, String passphrase, boolean assumeSymmetric) + throws IOException, PgpGeneralException, PGPException, SignatureException { + if (passphrase == null) { + passphrase = ""; + } + + Bundle returnData = new Bundle(); + InputStream in = PGPUtil.getDecoderStream(data.getInputStream()); + PGPObjectFactory pgpF = new PGPObjectFactory(in); + PGPEncryptedDataList enc; + Object o = pgpF.nextObject(); + long signatureKeyId = 0; + + int currentProgress = 0; + if (progress != null) + progress.setProgress(R.string.progress_readingData, currentProgress, 100); + + if (o instanceof PGPEncryptedDataList) { + enc = (PGPEncryptedDataList) o; + } else { + enc = (PGPEncryptedDataList) pgpF.nextObject(); + } + + if (enc == null) { + throw new PgpGeneralException(context.getString(R.string.error_invalidData)); + } + + InputStream clear = null; + PGPEncryptedData encryptedData = null; + + currentProgress += 5; + + // TODO: currently we always only look at the first known key or symmetric encryption, + // there might be more... + if (assumeSymmetric) { + PGPPBEEncryptedData pbe = null; + Iterator<?> it = enc.getEncryptedDataObjects(); + // find secret key + while (it.hasNext()) { + Object obj = it.next(); + if (obj instanceof PGPPBEEncryptedData) { + pbe = (PGPPBEEncryptedData) obj; + break; + } + } + + if (pbe == null) { + throw new PgpGeneralException( + context.getString(R.string.error_noSymmetricEncryptionPacket)); + } + + updateProgress(progress, R.string.progress_preparingStreams, currentProgress, 100); + + PGPDigestCalculatorProvider digestCalcProvider = new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(); + PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder( + digestCalcProvider).setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build( + passphrase.toCharArray()); + + clear = pbe.getDataStream(decryptorFactory); + + encryptedData = pbe; + currentProgress += 5; + } else { + if (progress != null) + progress.setProgress(R.string.progress_findingKey, currentProgress, 100); + PGPPublicKeyEncryptedData pbe = null; + PGPSecretKey secretKey = null; + Iterator<?> it = enc.getEncryptedDataObjects(); + // find secret key + while (it.hasNext()) { + Object obj = it.next(); + if (obj instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj; + secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, encData.getKeyID()); + if (secretKey != null) { + pbe = encData; + break; + } + } + } + + if (secretKey == null) { + throw new PgpGeneralException(context.getString(R.string.error_noSecretKeyFound)); + } + + currentProgress += 5; + updateProgress(progress, R.string.progress_extractingKey, currentProgress, 100); + PGPPrivateKey privateKey = null; + try { + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder() + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray()); + privateKey = secretKey.extractPrivateKey(keyDecryptor); + } catch (PGPException e) { + throw new PGPException(context.getString(R.string.error_wrongPassPhrase)); + } + if (privateKey == null) { + throw new PgpGeneralException( + context.getString(R.string.error_couldNotExtractPrivateKey)); + } + currentProgress += 5; + updateProgress(progress, R.string.progress_preparingStreams, currentProgress, 100); + + PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(privateKey); + + clear = pbe.getDataStream(decryptorFactory); + + encryptedData = pbe; + currentProgress += 5; + } + + PGPObjectFactory plainFact = new PGPObjectFactory(clear); + Object dataChunk = plainFact.nextObject(); + PGPOnePassSignature signature = null; + PGPPublicKey signatureKey = null; + int signatureIndex = -1; + + if (dataChunk instanceof PGPCompressedData) { + if (progress != null) + progress.setProgress(R.string.progress_decompressingData, currentProgress, 100); + PGPObjectFactory fact = new PGPObjectFactory( + ((PGPCompressedData) dataChunk).getDataStream()); + dataChunk = fact.nextObject(); + plainFact = fact; + currentProgress += 10; + } + + if (dataChunk instanceof PGPOnePassSignatureList) { + if (progress != null) + progress.setProgress(R.string.progress_processingSignature, currentProgress, 100); + returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE, true); + PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk; + for (int i = 0; i < sigList.size(); ++i) { + signature = sigList.get(i); + signatureKey = ProviderHelper.getPGPPublicKeyByKeyId(context, signature.getKeyID()); + if (signatureKeyId == 0) { + signatureKeyId = signature.getKeyID(); + } + if (signatureKey == null) { + signature = null; + } else { + signatureIndex = i; + signatureKeyId = signature.getKeyID(); + String userId = null; + PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingByKeyId( + context, signatureKeyId); + if (signKeyRing != null) { + userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signKeyRing)); + } + returnData.putString(KeychainIntentService.RESULT_SIGNATURE_USER_ID, userId); + break; + } + } + + returnData.putLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID, signatureKeyId); + + if (signature != null) { + JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = new JcaPGPContentVerifierBuilderProvider() + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME); + + signature.init(contentVerifierBuilderProvider, signatureKey); + } else { + returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN, true); + } + + dataChunk = plainFact.nextObject(); + currentProgress += 10; + } + + if (dataChunk instanceof PGPSignatureList) { + dataChunk = plainFact.nextObject(); + } + + if (dataChunk instanceof PGPLiteralData) { + if (progress != null) + progress.setProgress(R.string.progress_decrypting, currentProgress, 100); + PGPLiteralData literalData = (PGPLiteralData) dataChunk; + OutputStream out = outStream; + + byte[] buffer = new byte[1 << 16]; + InputStream dataIn = literalData.getInputStream(); + + int startProgress = currentProgress; + int endProgress = 100; + if (signature != null) { + endProgress = 90; + } else if (encryptedData.isIntegrityProtected()) { + endProgress = 95; + } + int n = 0; + int done = 0; + long startPos = data.getStreamPosition(); + while ((n = dataIn.read(buffer)) > 0) { + out.write(buffer, 0, n); + done += n; + if (signature != null) { + try { + signature.update(buffer, 0, n); + } catch (SignatureException e) { + returnData + .putBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS, false); + signature = null; + } + } + // unknown size, but try to at least have a moving, slowing down progress bar + currentProgress = startProgress + (endProgress - startProgress) * done + / (done + 100000); + if (data.getSize() - startPos == 0) { + currentProgress = endProgress; + } else { + currentProgress = (int) (startProgress + (endProgress - startProgress) + * (data.getStreamPosition() - startPos) / (data.getSize() - startPos)); + } + updateProgress(progress, currentProgress, 100); + } + + if (signature != null) { + if (progress != null) + progress.setProgress(R.string.progress_verifyingSignature, 90, 100); + PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject(); + PGPSignature messageSignature = signatureList.get(signatureIndex); + if (signature.verify(messageSignature)) { + returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS, true); + } else { + returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS, false); + } + } + } + + // TODO: add integrity somewhere + if (encryptedData.isIntegrityProtected()) { + if (progress != null) + progress.setProgress(R.string.progress_verifyingIntegrity, 95, 100); + if (encryptedData.verify()) { + // passed + } else { + // failed + } + } else { + // no integrity check + } + + updateProgress(progress, R.string.progress_done, 100, 100); + return returnData; + } + + public static Bundle verifyText(Context context, ProgressDialogUpdater progress, + InputData data, OutputStream outStream, boolean lookupUnknownKey) throws IOException, + PgpGeneralException, PGPException, SignatureException { + Bundle returnData = new Bundle(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredInputStream aIn = new ArmoredInputStream(data.getInputStream()); + + updateProgress(progress, R.string.progress_done, 0, 100); + + // mostly taken from ClearSignedFileProcessor + ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); + int lookAhead = readInputLine(lineOut, aIn); + byte[] lineSep = getLineSeparator(); + + byte[] line = lineOut.toByteArray(); + out.write(line, 0, getLengthWithoutSeparator(line)); + out.write(lineSep); + + while (lookAhead != -1 && aIn.isClearText()) { + lookAhead = readInputLine(lineOut, lookAhead, aIn); + line = lineOut.toByteArray(); + out.write(line, 0, getLengthWithoutSeparator(line)); + out.write(lineSep); + } + + out.close(); + + byte[] clearText = out.toByteArray(); + outStream.write(clearText); + + returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE, true); + + updateProgress(progress, R.string.progress_processingSignature, 60, 100); + PGPObjectFactory pgpFact = new PGPObjectFactory(aIn); + + PGPSignatureList sigList = (PGPSignatureList) pgpFact.nextObject(); + if (sigList == null) { + throw new PgpGeneralException(context.getString(R.string.error_corruptData)); + } + PGPSignature signature = null; + long signatureKeyId = 0; + PGPPublicKey signatureKey = null; + for (int i = 0; i < sigList.size(); ++i) { + signature = sigList.get(i); + signatureKey = ProviderHelper.getPGPPublicKeyByKeyId(context, signature.getKeyID()); + if (signatureKeyId == 0) { + signatureKeyId = signature.getKeyID(); + } + // if key is not known and we want to lookup unknown ones... + if (signatureKey == null && lookupUnknownKey) { + + returnData = new Bundle(); + returnData.putLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID, signatureKeyId); + returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_LOOKUP_KEY, true); + + // return directly now, decrypt will be done again after importing unknown key + return returnData; + } + + if (signatureKey == null) { + signature = null; + } else { + signatureKeyId = signature.getKeyID(); + String userId = null; + PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingByKeyId(context, + signatureKeyId); + if (signKeyRing != null) { + userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signKeyRing)); + } + returnData.putString(KeychainIntentService.RESULT_SIGNATURE_USER_ID, userId); + break; + } + } + + returnData.putLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID, signatureKeyId); + + if (signature == null) { + returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN, true); + if (progress != null) + progress.setProgress(R.string.progress_done, 100, 100); + return returnData; + } + + JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = new JcaPGPContentVerifierBuilderProvider() + .setProvider(BOUNCY_CASTLE_PROVIDER_NAME); + + signature.init(contentVerifierBuilderProvider, signatureKey); + + InputStream sigIn = new BufferedInputStream(new ByteArrayInputStream(clearText)); + + lookAhead = readInputLine(lineOut, sigIn); + + processLine(signature, lineOut.toByteArray()); + + if (lookAhead != -1) { + do { + lookAhead = readInputLine(lineOut, lookAhead, sigIn); + + signature.update((byte) '\r'); + signature.update((byte) '\n'); + + processLine(signature, lineOut.toByteArray()); + } while (lookAhead != -1); + } + + returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS, signature.verify()); + + updateProgress(progress, R.string.progress_done, 100, 100); + return returnData; + } + + public static int getStreamContent(Context context, InputStream inStream) throws IOException { + InputStream in = PGPUtil.getDecoderStream(inStream); + PGPObjectFactory pgpF = new PGPObjectFactory(in); + Object object = pgpF.nextObject(); + while (object != null) { + if (object instanceof PGPPublicKeyRing || object instanceof PGPSecretKeyRing) { + return Id.content.keys; + } else if (object instanceof PGPEncryptedDataList) { + return Id.content.encrypted_data; + } + object = pgpF.nextObject(); + } + + return Id.content.unknown; + } + + private static void processLine(final String pLine, final ArmoredOutputStream pArmoredOutput, + final PGPSignatureGenerator pSignatureGenerator) throws IOException, SignatureException { + + if (pLine == null) { + return; + } + + final char[] chars = pLine.toCharArray(); + int len = chars.length; + + while (len > 0) { + if (!Character.isWhitespace(chars[len - 1])) { + break; + } + len--; + } + + final byte[] data = pLine.substring(0, len).getBytes("UTF-8"); + + if (pArmoredOutput != null) { + pArmoredOutput.write(data); + } + pSignatureGenerator.update(data); + } + + private static void processLine(final String pLine, final ArmoredOutputStream pArmoredOutput, + final PGPV3SignatureGenerator pSignatureGenerator) throws IOException, + SignatureException { + + if (pLine == null) { + return; + } + + final char[] chars = pLine.toCharArray(); + int len = chars.length; + + while (len > 0) { + if (!Character.isWhitespace(chars[len - 1])) { + break; + } + len--; + } + + final byte[] data = pLine.substring(0, len).getBytes("UTF-8"); + + if (pArmoredOutput != null) { + pArmoredOutput.write(data); + } + pSignatureGenerator.update(data); + } + + // taken from ClearSignedFileProcessor in BC + private static void processLine(PGPSignature sig, byte[] line) throws SignatureException, + IOException { + int length = getLengthWithoutWhiteSpace(line); + if (length > 0) { + sig.update(line, 0, length); + } + } + + private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn) + throws IOException { + bOut.reset(); + + int lookAhead = -1; + int ch; + + while ((ch = fIn.read()) >= 0) { + bOut.write(ch); + if (ch == '\r' || ch == '\n') { + lookAhead = readPassedEOL(bOut, ch, fIn); + break; + } + } + + return lookAhead; + } + + private static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn) + throws IOException { + bOut.reset(); + + int ch = lookAhead; + + do { + bOut.write(ch); + if (ch == '\r' || ch == '\n') { + lookAhead = readPassedEOL(bOut, ch, fIn); + break; + } + } while ((ch = fIn.read()) >= 0); + + if (ch < 0) { + lookAhead = -1; + } + + return lookAhead; + } + + private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn) + throws IOException { + int lookAhead = fIn.read(); + + if (lastCh == '\r' && lookAhead == '\n') { + bOut.write(lookAhead); + lookAhead = fIn.read(); + } + + return lookAhead; + } + + private static int getLengthWithoutSeparator(byte[] line) { + int end = line.length - 1; + + while (end >= 0 && isLineEnding(line[end])) { + end--; + } + + return end + 1; + } + + private static boolean isLineEnding(byte b) { + return b == '\r' || b == '\n'; + } + + private static int getLengthWithoutWhiteSpace(byte[] line) { + int end = line.length - 1; + + while (end >= 0 && isWhiteSpace(line[end])) { + end--; + } + + return end + 1; + } + + private static boolean isWhiteSpace(byte b) { + return b == '\r' || b == '\n' || b == '\t' || b == ' '; + } + + 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; + } + + public static boolean isReleaseVersion(Context context) { + try { + PackageInfo pi = context.getPackageManager().getPackageInfo(Constants.PACKAGE_NAME, 0); + if (pi.versionCode % 100 == 99) { + return true; + } else { + return false; + } + } catch (NameNotFoundException e) { + // impossible! + return false; + } + } + + public static String getVersion(Context context) { + String version = null; + try { + PackageInfo pi = context.getPackageManager().getPackageInfo(Constants.PACKAGE_NAME, 0); + version = pi.versionName; + return version; + } catch (NameNotFoundException e) { + Log.e(Constants.TAG, "Version could not be retrieved!", e); + return "0.0.0"; + } + } + + public static String getFullVersion(Context context) { + return "APG v" + getVersion(context); + } + + /** + * Generate a random filename + * + * @param length + * @return + */ + public static String generateRandomFilename(int length) { + SecureRandom random = new SecureRandom(); + + byte bytes[] = new byte[length]; + random.nextBytes(bytes); + String result = ""; + for (int i = 0; i < length; ++i) { + int v = (bytes[i] + 256) % 64; + if (v < 10) { + result += (char) ('0' + v); + } else if (v < 36) { + result += (char) ('A' + v - 10); + } else if (v < 62) { + result += (char) ('a' + v - 36); + } else if (v == 62) { + result += '_'; + } else if (v == 63) { + result += '.'; + } + } + return result; + } + + /** + * Go once through stream to get length of stream. The length is later used to display progress + * when encrypting/decrypting + * + * @param in + * @return + * @throws IOException + */ + public static long getLengthOfStream(InputStream in) throws IOException { + long size = 0; + long n = 0; + byte dummy[] = new byte[0x10000]; + while ((n = in.read(dummy)) > 0) { + size += n; + } + return size; + } + + /** + * Deletes file securely by overwriting it with random data before deleting it. + * + * TODO: Does this really help on flash storage? + * + * @param context + * @param progress + * @param file + * @throws FileNotFoundException + * @throws IOException + */ + public static void deleteFileSecurely(Context context, ProgressDialogUpdater progress, File file) + throws FileNotFoundException, IOException { + long length = file.length(); + SecureRandom random = new SecureRandom(); + RandomAccessFile raf = new RandomAccessFile(file, "rws"); + raf.seek(0); + raf.getFilePointer(); + byte[] data = new byte[1 << 16]; + int pos = 0; + String msg = context.getString(R.string.progress_deletingSecurely, file.getName()); + while (pos < length) { + if (progress != null) + progress.setProgress(msg, (int) (100 * pos / length), 100); + random.nextBytes(data); + raf.write(data); + pos += data.length; + } + raf.close(); + file.delete(); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/Preferences.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/Preferences.java new file mode 100644 index 000000000..121bae6ef --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/Preferences.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.helper; + +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.Vector; + +/** + * Singleton Implementation of a Preference Helper + */ +public class Preferences { + private static Preferences mPreferences; + private SharedPreferences mSharedPreferences; + + public static synchronized Preferences getPreferences(Context context) { + return getPreferences(context, false); + } + + public static synchronized Preferences getPreferences(Context context, boolean force_new) { + if (mPreferences == null || force_new) { + mPreferences = new Preferences(context); + } + return mPreferences; + } + + private Preferences(Context context) { + mSharedPreferences = context.getSharedPreferences("APG.main", Context.MODE_PRIVATE); + } + + public String getLanguage() { + return mSharedPreferences.getString(Constants.pref.LANGUAGE, ""); + } + + public void setLanguage(String value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putString(Constants.pref.LANGUAGE, value); + editor.commit(); + } + + public long getPassPhraseCacheTtl() { + int ttl = mSharedPreferences.getInt(Constants.pref.PASS_PHRASE_CACHE_TTL, 180); + // fix the value if it was set to "never" in previous versions, which currently is not + // supported + if (ttl == 0) { + ttl = 180; + } + return (long) ttl; + } + + public void setPassPhraseCacheTtl(int value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putInt(Constants.pref.PASS_PHRASE_CACHE_TTL, value); + editor.commit(); + } + + public int getDefaultEncryptionAlgorithm() { + return mSharedPreferences.getInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM, + PGPEncryptedData.AES_256); + } + + public void setDefaultEncryptionAlgorithm(int value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM, value); + editor.commit(); + } + + public int getDefaultHashAlgorithm() { + return mSharedPreferences.getInt(Constants.pref.DEFAULT_HASH_ALGORITHM, + HashAlgorithmTags.SHA256); + } + + public void setDefaultHashAlgorithm(int value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putInt(Constants.pref.DEFAULT_HASH_ALGORITHM, value); + editor.commit(); + } + + public int getDefaultMessageCompression() { + return mSharedPreferences.getInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION, + Id.choice.compression.zlib); + } + + public void setDefaultMessageCompression(int value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION, value); + editor.commit(); + } + + public int getDefaultFileCompression() { + return mSharedPreferences.getInt(Constants.pref.DEFAULT_FILE_COMPRESSION, + Id.choice.compression.none); + } + + public void setDefaultFileCompression(int value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putInt(Constants.pref.DEFAULT_FILE_COMPRESSION, value); + editor.commit(); + } + + public boolean getDefaultAsciiArmour() { + return mSharedPreferences.getBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, false); + } + + public void setDefaultAsciiArmour(boolean value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, value); + editor.commit(); + } + + public boolean getForceV3Signatures() { + return mSharedPreferences.getBoolean(Constants.pref.FORCE_V3_SIGNATURES, false); + } + + public void setForceV3Signatures(boolean value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Constants.pref.FORCE_V3_SIGNATURES, value); + editor.commit(); + } + + public String[] getKeyServers() { + String rawData = mSharedPreferences.getString(Constants.pref.KEY_SERVERS, + Constants.defaults.KEY_SERVERS); + Vector<String> servers = new Vector<String>(); + String chunks[] = rawData.split(","); + for (int i = 0; i < chunks.length; ++i) { + String tmp = chunks[i].trim(); + if (tmp.length() > 0) { + servers.add(tmp); + } + } + return servers.toArray(chunks); + } + + public void setKeyServers(String[] value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + String rawData = ""; + for (int i = 0; i < value.length; ++i) { + String tmp = value[i].trim(); + if (tmp.length() == 0) { + continue; + } + if (!"".equals(rawData)) { + rawData += ","; + } + rawData += tmp; + } + editor.putString(Constants.pref.KEY_SERVERS, rawData); + editor.commit(); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java new file mode 100644 index 000000000..3d4dddea5 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.provider; + +import org.sufficientlysecure.keychain.Constants; + +import android.net.Uri; +import android.provider.BaseColumns; + +public class KeychainContract { + + interface KeyRingsColumns { + String MASTER_KEY_ID = "master_key_id"; // not a database id + String TYPE = "type"; // see KeyTypes + String KEY_RING_DATA = "key_ring_data"; // PGPPublicKeyRing / PGPSecretKeyRing blob + } + + interface KeysColumns { + String KEY_ID = "key_id"; // not a database id + String TYPE = "type"; // see KeyTypes + String IS_MASTER_KEY = "is_master_key"; + String ALGORITHM = "algorithm"; + String KEY_SIZE = "key_size"; + String CAN_SIGN = "can_sign"; + String CAN_ENCRYPT = "can_encrypt"; + String IS_REVOKED = "is_revoked"; + String CREATION = "creation"; + String EXPIRY = "expiry"; + String KEY_RING_ROW_ID = "key_ring_row_id"; // foreign key to key_rings._ID + String KEY_DATA = "key_data"; // PGPPublicKey / PGPSecretKey blob + String RANK = "rank"; + } + + interface UserIdsColumns { + String KEY_RING_ROW_ID = "key_ring_row_id"; // foreign key to key_rings._ID + String USER_ID = "user_id"; // not a database id + String RANK = "rank"; + } + + public static final class KeyTypes { + public static final int PUBLIC = 0; + public static final int SECRET = 1; + } + + public static final String CONTENT_AUTHORITY_EXTERNAL = Constants.PACKAGE_NAME; + public static final String CONTENT_AUTHORITY_INTERNAL = Constants.PACKAGE_NAME + ".internal"; + + private static final Uri BASE_CONTENT_URI_INTERNAL = Uri.parse("content://" + + CONTENT_AUTHORITY_INTERNAL); + + public static final String BASE_KEY_RINGS = "key_rings"; + public static final String BASE_DATA = "data"; + + public static final String PATH_PUBLIC = "public"; + public static final String PATH_SECRET = "secret"; + + public static final String PATH_BY_MASTER_KEY_ID = "master_key_id"; + public static final String PATH_BY_KEY_ID = "key_id"; + public static final String PATH_BY_EMAILS = "emails"; + public static final String PATH_BY_LIKE_EMAIL = "like_email"; + + public static final String PATH_USER_IDS = "user_ids"; + public static final String PATH_KEYS = "keys"; + + public static class KeyRings implements KeyRingsColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_KEY_RINGS).build(); + + /** Use if multiple items get returned */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.key_ring"; + + /** Use if a single item is returned */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.key_ring"; + + public static Uri buildPublicKeyRingsUri() { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).build(); + } + + public static Uri buildPublicKeyRingsUri(String keyRingRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId).build(); + } + + public static Uri buildPublicKeyRingsByMasterKeyIdUri(String masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC) + .appendPath(PATH_BY_MASTER_KEY_ID).appendPath(masterKeyId).build(); + } + + public static Uri buildPublicKeyRingsByKeyIdUri(String keyId) { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_KEY_ID) + .appendPath(keyId).build(); + } + + public static Uri buildPublicKeyRingsByEmailsUri(String emails) { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_EMAILS) + .appendPath(emails).build(); + } + + public static Uri buildPublicKeyRingsByLikeEmailUri(String emails) { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_LIKE_EMAIL) + .appendPath(emails).build(); + } + + public static Uri buildSecretKeyRingsUri() { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).build(); + } + + public static Uri buildSecretKeyRingsUri(String keyRingRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId).build(); + } + + public static Uri buildSecretKeyRingsByMasterKeyIdUri(String masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET) + .appendPath(PATH_BY_MASTER_KEY_ID).appendPath(masterKeyId).build(); + } + + public static Uri buildSecretKeyRingsByKeyIdUri(String keyId) { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_KEY_ID) + .appendPath(keyId).build(); + } + + public static Uri buildSecretKeyRingsByEmailsUri(String emails) { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_EMAILS) + .appendPath(emails).build(); + } + + public static Uri buildSecretKeyRingsByLikeEmails(String emails) { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_LIKE_EMAIL) + .appendPath(emails).build(); + } + } + + public static class Keys implements KeysColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_KEY_RINGS).build(); + + /** Use if multiple items get returned */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.key"; + + /** Use if a single item is returned */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.key"; + + public static Uri buildPublicKeysUri(String keyRingRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId) + .appendPath(PATH_KEYS).build(); + } + + public static Uri buildPublicKeysUri(String keyRingRowId, String keyRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId) + .appendPath(PATH_KEYS).appendPath(keyRowId).build(); + } + + public static Uri buildSecretKeysUri(String keyRingRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId) + .appendPath(PATH_KEYS).build(); + } + + public static Uri buildSecretKeysUri(String keyRingRowId, String keyRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId) + .appendPath(PATH_KEYS).appendPath(keyRowId).build(); + } + } + + public static class UserIds implements UserIdsColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_KEY_RINGS).build(); + + /** Use if multiple items get returned */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.user_id"; + + /** Use if a single item is returned */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.user_id"; + + public static Uri buildPublicUserIdsUri(String keyRingRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId) + .appendPath(PATH_USER_IDS).build(); + } + + public static Uri buildPublicUserIdsUri(String keyRingRowId, String userIdRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId) + .appendPath(PATH_USER_IDS).appendPath(userIdRowId).build(); + } + + public static Uri buildSecretUserIdsUri(String keyRingRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId) + .appendPath(PATH_USER_IDS).build(); + } + + public static Uri buildSecretUserIdsUri(String keyRingRowId, String userIdRowId) { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId) + .appendPath(PATH_USER_IDS).appendPath(userIdRowId).build(); + } + } + + public static class DataStream { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_DATA).build(); + + public static Uri buildDataStreamUri(String streamFilename) { + return CONTENT_URI.buildUpon().appendPath(streamFilename).build(); + } + } + + private KeychainContract() { + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainDatabase.java new file mode 100644 index 000000000..4a2aeeb80 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.provider; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIdsColumns; +import org.sufficientlysecure.keychain.util.Log; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; + + +public class KeychainDatabase extends SQLiteOpenHelper { + private static final String DATABASE_NAME = "apg.db"; + private static final int DATABASE_VERSION = 3; + + public interface Tables { + String KEY_RINGS = "key_rings"; + String KEYS = "keys"; + String USER_IDS = "user_ids"; + } + + private static final String CREATE_KEY_RINGS = "CREATE TABLE IF NOT EXISTS " + Tables.KEY_RINGS + + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + KeyRingsColumns.MASTER_KEY_ID + " INT64, " + KeyRingsColumns.TYPE + " INTEGER, " + + KeyRingsColumns.KEY_RING_DATA + " BLOB)"; + + private static final String CREATE_KEYS = "CREATE TABLE IF NOT EXISTS " + Tables.KEYS + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + KeysColumns.KEY_ID + + " INT64, " + KeysColumns.TYPE + " INTEGER, " + KeysColumns.IS_MASTER_KEY + + " INTEGER, " + KeysColumns.ALGORITHM + " INTEGER, " + KeysColumns.KEY_SIZE + + " INTEGER, " + KeysColumns.CAN_SIGN + " INTEGER, " + KeysColumns.CAN_ENCRYPT + + " INTEGER, " + KeysColumns.IS_REVOKED + " INTEGER, " + KeysColumns.CREATION + + " INTEGER, " + KeysColumns.EXPIRY + " INTEGER, " + KeysColumns.KEY_DATA + " BLOB," + + KeysColumns.RANK + " INTEGER, " + KeysColumns.KEY_RING_ROW_ID + + " INTEGER NOT NULL, FOREIGN KEY(" + KeysColumns.KEY_RING_ROW_ID + ") REFERENCES " + + Tables.KEY_RINGS + "(" + BaseColumns._ID + ") ON DELETE CASCADE)"; + + private static final String CREATE_USER_IDS = "CREATE TABLE IF NOT EXISTS " + Tables.USER_IDS + + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + UserIdsColumns.USER_ID + " TEXT, " + UserIdsColumns.RANK + " INTEGER, " + + UserIdsColumns.KEY_RING_ROW_ID + " INTEGER NOT NULL, FOREIGN KEY(" + + UserIdsColumns.KEY_RING_ROW_ID + ") REFERENCES " + Tables.KEY_RINGS + "(" + + BaseColumns._ID + ") ON DELETE CASCADE)"; + + KeychainDatabase(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.w(Constants.TAG, "Creating database..."); + + db.execSQL(CREATE_KEY_RINGS); + db.execSQL(CREATE_KEYS); + db.execSQL(CREATE_USER_IDS); + } + + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + if (!db.isReadOnly()) { + // Enable foreign key constraints + db.execSQL("PRAGMA foreign_keys=ON;"); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(Constants.TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); + + // Upgrade from oldVersion through all methods to newest one + for (int version = oldVersion; version < newVersion; ++version) { + Log.w(Constants.TAG, "Upgrading database to version " + version); + + switch (version) { + + default: + break; + + } + } + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProvider.java new file mode 100644 index 000000000..9d531dceb --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -0,0 +1,852 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.provider; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.HashMap; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyTypes; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIdsColumns; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.util.Log; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteConstraintException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.BaseColumns; +import android.text.TextUtils; + +public class KeychainProvider extends ContentProvider { + private static final int PUBLIC_KEY_RING = 101; + private static final int PUBLIC_KEY_RING_BY_ROW_ID = 102; + private static final int PUBLIC_KEY_RING_BY_MASTER_KEY_ID = 103; + private static final int PUBLIC_KEY_RING_BY_KEY_ID = 104; + private static final int PUBLIC_KEY_RING_BY_EMAILS = 105; + private static final int PUBLIC_KEY_RING_BY_LIKE_EMAIL = 106; + + private static final int PUBLIC_KEY_RING_KEY = 111; + private static final int PUBLIC_KEY_RING_KEY_BY_ROW_ID = 112; + + private static final int PUBLIC_KEY_RING_USER_ID = 121; + private static final int PUBLIC_KEY_RING_USER_ID_BY_ROW_ID = 122; + + private static final int SECRET_KEY_RING = 201; + private static final int SECRET_KEY_RING_BY_ROW_ID = 202; + private static final int SECRET_KEY_RING_BY_MASTER_KEY_ID = 203; + private static final int SECRET_KEY_RING_BY_KEY_ID = 204; + private static final int SECRET_KEY_RING_BY_EMAILS = 205; + private static final int SECRET_KEY_RING_BY_LIKE_EMAIL = 206; + + private static final int SECRET_KEY_RING_KEY = 211; + private static final int SECRET_KEY_RING_KEY_BY_ROW_ID = 212; + + private static final int SECRET_KEY_RING_USER_ID = 221; + private static final int SECRET_KEY_RING_USER_ID_BY_ROW_ID = 222; + + private static final int DATA_STREAM = 301; + + protected boolean mInternalProvider; + protected UriMatcher mUriMatcher; + + /** + * Build and return a {@link UriMatcher} that catches all {@link Uri} variations supported by + * this {@link ContentProvider}. + */ + protected UriMatcher buildUriMatcher(boolean internalProvider) { + final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); + + String authority; + if (internalProvider) { + authority = KeychainContract.CONTENT_AUTHORITY_INTERNAL; + } else { + authority = KeychainContract.CONTENT_AUTHORITY_EXTERNAL; + } + + /** + * public key rings + * + * <pre> + * key_rings/public + * key_rings/public/# + * key_rings/public/master_key_id/_ + * key_rings/public/key_id/_ + * key_rings/public/emails/_ + * key_rings/public/like_email/_ + * </pre> + */ + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC, + PUBLIC_KEY_RING); + matcher.addURI(authority, + KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/#", + PUBLIC_KEY_RING_BY_ROW_ID); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/" + + KeychainContract.PATH_BY_MASTER_KEY_ID + "/*", PUBLIC_KEY_RING_BY_MASTER_KEY_ID); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/" + + KeychainContract.PATH_BY_KEY_ID + "/*", PUBLIC_KEY_RING_BY_KEY_ID); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/" + + KeychainContract.PATH_BY_EMAILS + "/*", PUBLIC_KEY_RING_BY_EMAILS); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/" + + KeychainContract.PATH_BY_EMAILS, PUBLIC_KEY_RING_BY_EMAILS); // without emails + // specified + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/" + + KeychainContract.PATH_BY_LIKE_EMAIL + "/*", PUBLIC_KEY_RING_BY_LIKE_EMAIL); + + /** + * public keys + * + * <pre> + * key_rings/public/#/keys + * key_rings/public/#/keys/# + * </pre> + */ + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + + "/#/" + KeychainContract.PATH_KEYS, PUBLIC_KEY_RING_KEY); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + + "/#/" + KeychainContract.PATH_KEYS + "/#", PUBLIC_KEY_RING_KEY_BY_ROW_ID); + + /** + * public user ids + * + * <pre> + * key_rings/public/#/user_ids + * key_rings/public/#/user_ids/# + * </pre> + */ + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + + "/#/" + KeychainContract.PATH_USER_IDS, PUBLIC_KEY_RING_USER_ID); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + + "/#/" + KeychainContract.PATH_USER_IDS + "/#", PUBLIC_KEY_RING_USER_ID_BY_ROW_ID); + + /** + * secret key rings + * + * <pre> + * key_rings/secret + * key_rings/secret/# + * key_rings/secret/master_key_id/_ + * key_rings/secret/key_id/_ + * key_rings/secret/emails/_ + * key_rings/secret/like_email/_ + * </pre> + */ + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET, + SECRET_KEY_RING); + matcher.addURI(authority, + KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/#", + SECRET_KEY_RING_BY_ROW_ID); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/" + + KeychainContract.PATH_BY_MASTER_KEY_ID + "/*", SECRET_KEY_RING_BY_MASTER_KEY_ID); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/" + + KeychainContract.PATH_BY_KEY_ID + "/*", SECRET_KEY_RING_BY_KEY_ID); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/" + + KeychainContract.PATH_BY_EMAILS + "/*", SECRET_KEY_RING_BY_EMAILS); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/" + + KeychainContract.PATH_BY_EMAILS, SECRET_KEY_RING_BY_EMAILS); // without emails + // specified + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/" + + KeychainContract.PATH_BY_LIKE_EMAIL + "/*", SECRET_KEY_RING_BY_LIKE_EMAIL); + + /** + * secret keys + * + * <pre> + * key_rings/secret/#/keys + * key_rings/secret/#/keys/# + * </pre> + */ + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + + "/#/" + KeychainContract.PATH_KEYS, SECRET_KEY_RING_KEY); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + + "/#/" + KeychainContract.PATH_KEYS + "/#", SECRET_KEY_RING_KEY_BY_ROW_ID); + + /** + * secret user ids + * + * <pre> + * key_rings/secret/#/user_ids + * key_rings/secret/#/user_ids/# + * </pre> + */ + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + + "/#/" + KeychainContract.PATH_USER_IDS, SECRET_KEY_RING_USER_ID); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + + "/#/" + KeychainContract.PATH_USER_IDS + "/#", SECRET_KEY_RING_USER_ID_BY_ROW_ID); + + /** + * data stream + * + * <pre> + * data / _ + * </pre> + */ + matcher.addURI(authority, KeychainContract.BASE_DATA + "/*", DATA_STREAM); + + return matcher; + } + + private KeychainDatabase mApgDatabase; + + /** {@inheritDoc} */ + @Override + public boolean onCreate() { + mUriMatcher = buildUriMatcher(mInternalProvider); + mApgDatabase = new KeychainDatabase(getContext()); + return true; + } + + /** {@inheritDoc} */ + @Override + public String getType(Uri uri) { + final int match = mUriMatcher.match(uri); + switch (match) { + case PUBLIC_KEY_RING: + case PUBLIC_KEY_RING_BY_EMAILS: + case PUBLIC_KEY_RING_BY_LIKE_EMAIL: + case SECRET_KEY_RING: + case SECRET_KEY_RING_BY_EMAILS: + case SECRET_KEY_RING_BY_LIKE_EMAIL: + return KeyRings.CONTENT_TYPE; + + case PUBLIC_KEY_RING_BY_ROW_ID: + case PUBLIC_KEY_RING_BY_MASTER_KEY_ID: + case PUBLIC_KEY_RING_BY_KEY_ID: + case SECRET_KEY_RING_BY_ROW_ID: + case SECRET_KEY_RING_BY_MASTER_KEY_ID: + case SECRET_KEY_RING_BY_KEY_ID: + return KeyRings.CONTENT_ITEM_TYPE; + + case PUBLIC_KEY_RING_KEY: + case SECRET_KEY_RING_KEY: + return Keys.CONTENT_TYPE; + + case PUBLIC_KEY_RING_KEY_BY_ROW_ID: + case SECRET_KEY_RING_KEY_BY_ROW_ID: + return Keys.CONTENT_ITEM_TYPE; + + case PUBLIC_KEY_RING_USER_ID: + case SECRET_KEY_RING_USER_ID: + return UserIds.CONTENT_TYPE; + + case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID: + case SECRET_KEY_RING_USER_ID_BY_ROW_ID: + return UserIds.CONTENT_ITEM_TYPE; + + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + + /** + * Returns type of the query (secret/public) + * + * @param uri + * @return + */ + private int getKeyType(int match) { + int type; + switch (match) { + case PUBLIC_KEY_RING: + case PUBLIC_KEY_RING_BY_ROW_ID: + case PUBLIC_KEY_RING_BY_MASTER_KEY_ID: + case PUBLIC_KEY_RING_BY_KEY_ID: + case PUBLIC_KEY_RING_BY_EMAILS: + case PUBLIC_KEY_RING_BY_LIKE_EMAIL: + case PUBLIC_KEY_RING_KEY: + case PUBLIC_KEY_RING_KEY_BY_ROW_ID: + case PUBLIC_KEY_RING_USER_ID: + case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID: + type = KeyTypes.PUBLIC; + break; + + case SECRET_KEY_RING: + case SECRET_KEY_RING_BY_ROW_ID: + case SECRET_KEY_RING_BY_MASTER_KEY_ID: + case SECRET_KEY_RING_BY_KEY_ID: + case SECRET_KEY_RING_BY_EMAILS: + case SECRET_KEY_RING_BY_LIKE_EMAIL: + case SECRET_KEY_RING_KEY: + case SECRET_KEY_RING_KEY_BY_ROW_ID: + case SECRET_KEY_RING_USER_ID: + case SECRET_KEY_RING_USER_ID_BY_ROW_ID: + type = KeyTypes.SECRET; + break; + + default: + throw new IllegalArgumentException("Unknown match " + match); + + } + + return type; + } + + /** + * Set result of query to specific columns, don't show blob column for external content provider + * + * @return + */ + private HashMap<String, String> getProjectionMapForKeyRings() { + HashMap<String, String> projectionMap = new HashMap<String, String>(); + + projectionMap.put(BaseColumns._ID, Tables.KEY_RINGS + "." + BaseColumns._ID); + projectionMap.put(KeyRingsColumns.MASTER_KEY_ID, Tables.KEY_RINGS + "." + + KeyRingsColumns.MASTER_KEY_ID); + // only give out keyRing blob when we are using the internal content provider + if (mInternalProvider) { + projectionMap.put(KeyRingsColumns.KEY_RING_DATA, Tables.KEY_RINGS + "." + + KeyRingsColumns.KEY_RING_DATA); + } + projectionMap.put(UserIdsColumns.USER_ID, Tables.USER_IDS + "." + UserIdsColumns.USER_ID); + + return projectionMap; + } + + /** + * Set result of query to specific columns, don't show blob column for external content provider + * + * @return + */ + private HashMap<String, String> getProjectionMapForKeys() { + HashMap<String, String> projectionMap = new HashMap<String, String>(); + + projectionMap.put(BaseColumns._ID, BaseColumns._ID); + projectionMap.put(KeysColumns.KEY_ID, KeysColumns.KEY_ID); + projectionMap.put(KeysColumns.IS_MASTER_KEY, KeysColumns.IS_MASTER_KEY); + projectionMap.put(KeysColumns.ALGORITHM, KeysColumns.ALGORITHM); + projectionMap.put(KeysColumns.KEY_SIZE, KeysColumns.KEY_SIZE); + projectionMap.put(KeysColumns.CAN_SIGN, KeysColumns.CAN_SIGN); + projectionMap.put(KeysColumns.CAN_ENCRYPT, KeysColumns.CAN_ENCRYPT); + projectionMap.put(KeysColumns.IS_REVOKED, KeysColumns.IS_REVOKED); + projectionMap.put(KeysColumns.CREATION, KeysColumns.CREATION); + projectionMap.put(KeysColumns.EXPIRY, KeysColumns.EXPIRY); + projectionMap.put(KeysColumns.KEY_RING_ROW_ID, KeysColumns.KEY_RING_ROW_ID); + // only give out keyRing blob when we are using the internal content provider + if (mInternalProvider) { + projectionMap.put(KeysColumns.KEY_DATA, KeysColumns.KEY_DATA); + } + projectionMap.put(KeysColumns.RANK, KeysColumns.RANK); + + return projectionMap; + } + + /** + * Builds default query for keyRings: KeyRings table is joined with UserIds + * + * @param qb + * @param match + * @param isMasterKey + * @param sortOrder + * @return + */ + private SQLiteQueryBuilder buildKeyRingQuery(SQLiteQueryBuilder qb, int match, String sortOrder) { + // public or secret keyring + qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " = "); + qb.appendWhereEscapeString(Integer.toString(getKeyType(match))); + + // join keyrings with userIds to every keyring + qb.setTables(Tables.KEY_RINGS + " INNER JOIN " + Tables.USER_IDS + " ON " + "(" + + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.USER_IDS + "." + + UserIdsColumns.KEY_RING_ROW_ID + " AND " + Tables.USER_IDS + "." + + UserIdsColumns.RANK + " = '0')"); + + qb.setProjectionMap(getProjectionMapForKeyRings()); + + return qb; + } + + /** + * Builds default query for keyRings: KeyRings table is joined with Keys and UserIds + * + * @param qb + * @param match + * @param isMasterKey + * @param sortOrder + * @return + */ + private SQLiteQueryBuilder buildKeyRingQueryWithKeys(SQLiteQueryBuilder qb, int match, + String sortOrder) { + // public or secret keyring + qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " = "); + qb.appendWhereEscapeString(Integer.toString(getKeyType(match))); + + // join keyrings with keys and userIds to every keyring + qb.setTables(Tables.KEY_RINGS + " INNER JOIN " + Tables.KEYS + " ON " + "(" + + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.KEYS + "." + + KeysColumns.KEY_RING_ROW_ID + ") " + " INNER JOIN " + Tables.USER_IDS + " ON " + + "(" + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.USER_IDS + "." + + UserIdsColumns.KEY_RING_ROW_ID + " AND " + Tables.USER_IDS + "." + + UserIdsColumns.RANK + " = '0')"); + + qb.setProjectionMap(getProjectionMapForKeyRings()); + + return qb; + } + + /** {@inheritDoc} */ + @SuppressWarnings("deprecation") + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + Log.v(Constants.TAG, "query(uri=" + uri + ", proj=" + Arrays.toString(projection) + ")"); + + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + SQLiteDatabase db = mApgDatabase.getReadableDatabase(); + + int match = mUriMatcher.match(uri); + + switch (match) { + case PUBLIC_KEY_RING: + case SECRET_KEY_RING: + qb = buildKeyRingQuery(qb, match, sortOrder); + + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC"; + } + + break; + + case PUBLIC_KEY_RING_BY_ROW_ID: + case SECRET_KEY_RING_BY_ROW_ID: + qb = buildKeyRingQuery(qb, match, sortOrder); + + qb.appendWhere(" AND " + Tables.KEY_RINGS + "." + BaseColumns._ID + " = "); + qb.appendWhereEscapeString(uri.getLastPathSegment()); + + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC"; + } + + break; + + case PUBLIC_KEY_RING_BY_MASTER_KEY_ID: + case SECRET_KEY_RING_BY_MASTER_KEY_ID: + qb = buildKeyRingQuery(qb, match, sortOrder); + + qb.appendWhere(" AND " + Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getLastPathSegment()); + + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC"; + } + + break; + + case SECRET_KEY_RING_BY_KEY_ID: + case PUBLIC_KEY_RING_BY_KEY_ID: + qb = buildKeyRingQueryWithKeys(qb, match, sortOrder); + + qb.appendWhere(" AND " + Tables.KEYS + "." + KeysColumns.KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getLastPathSegment()); + + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC"; + } + + break; + + case SECRET_KEY_RING_BY_EMAILS: + case PUBLIC_KEY_RING_BY_EMAILS: + qb = buildKeyRingQuery(qb, match, sortOrder); + + String emails = uri.getLastPathSegment(); + String chunks[] = emails.split(" *, *"); + boolean gotCondition = false; + String emailWhere = ""; + for (int i = 0; i < chunks.length; ++i) { + if (chunks[i].length() == 0) { + continue; + } + if (i != 0) { + emailWhere += " OR "; + } + emailWhere += "tmp." + UserIdsColumns.USER_ID + " LIKE "; + // match '*<email>', so it has to be at the *end* of the user id + emailWhere += DatabaseUtils.sqlEscapeString("%<" + chunks[i] + ">"); + gotCondition = true; + } + + if (gotCondition) { + qb.appendWhere(" AND EXISTS (SELECT tmp." + BaseColumns._ID + " FROM " + + Tables.USER_IDS + " AS tmp WHERE tmp." + UserIdsColumns.KEY_RING_ROW_ID + + " = " + Tables.KEY_RINGS + "." + BaseColumns._ID + " AND (" + emailWhere + + "))"); + } + + break; + + case SECRET_KEY_RING_BY_LIKE_EMAIL: + case PUBLIC_KEY_RING_BY_LIKE_EMAIL: + qb = buildKeyRingQuery(qb, match, sortOrder); + + String likeEmail = uri.getLastPathSegment(); + + String likeEmailWhere = "tmp." + UserIdsColumns.USER_ID + " LIKE " + + DatabaseUtils.sqlEscapeString("%<%" + likeEmail + "%>"); + + qb.appendWhere(" AND EXISTS (SELECT tmp." + BaseColumns._ID + " FROM " + + Tables.USER_IDS + " AS tmp WHERE tmp." + UserIdsColumns.KEY_RING_ROW_ID + + " = " + Tables.KEY_RINGS + "." + BaseColumns._ID + " AND (" + likeEmailWhere + + "))"); + + break; + + case PUBLIC_KEY_RING_KEY: + case SECRET_KEY_RING_KEY: + qb.setTables(Tables.KEYS); + qb.appendWhere(KeysColumns.TYPE + " = "); + qb.appendWhereEscapeString(Integer.toString(getKeyType(match))); + + qb.appendWhere(" AND " + KeysColumns.KEY_RING_ROW_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(2)); + + qb.setProjectionMap(getProjectionMapForKeys()); + + break; + + case PUBLIC_KEY_RING_KEY_BY_ROW_ID: + case SECRET_KEY_RING_KEY_BY_ROW_ID: + qb.setTables(Tables.KEYS); + qb.appendWhere(KeysColumns.TYPE + " = "); + qb.appendWhereEscapeString(Integer.toString(getKeyType(match))); + + qb.appendWhere(" AND " + KeysColumns.KEY_RING_ROW_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(2)); + + qb.appendWhere(" AND " + BaseColumns._ID + " = "); + qb.appendWhereEscapeString(uri.getLastPathSegment()); + + qb.setProjectionMap(getProjectionMapForKeys()); + + break; + + case PUBLIC_KEY_RING_USER_ID: + case SECRET_KEY_RING_USER_ID: + qb.setTables(Tables.USER_IDS); + qb.appendWhere(UserIdsColumns.KEY_RING_ROW_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(2)); + + break; + + case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID: + case SECRET_KEY_RING_USER_ID_BY_ROW_ID: + qb.setTables(Tables.USER_IDS); + qb.appendWhere(UserIdsColumns.KEY_RING_ROW_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(2)); + + qb.appendWhere(" AND " + BaseColumns._ID + " = "); + qb.appendWhereEscapeString(uri.getLastPathSegment()); + + break; + + default: + throw new IllegalArgumentException("Unknown URI " + uri); + + } + + // If no sort order is specified use the default + String orderBy; + if (TextUtils.isEmpty(sortOrder)) { + orderBy = null; + } else { + orderBy = sortOrder; + } + + Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy); + + // Tell the cursor what uri to watch, so it knows when its source data changes + c.setNotificationUri(getContext().getContentResolver(), uri); + + if (Constants.DEBUG) { + Log.d(Constants.TAG, + "Query: " + + qb.buildQuery(projection, selection, selectionArgs, null, null, + orderBy, null)); + Log.d(Constants.TAG, "Cursor: " + DatabaseUtils.dumpCursorToString(c)); + } + + return c; + } + + /** {@inheritDoc} */ + @Override + public Uri insert(Uri uri, ContentValues values) { + Log.d(Constants.TAG, "insert(uri=" + uri + ", values=" + values.toString() + ")"); + + final SQLiteDatabase db = mApgDatabase.getWritableDatabase(); + + Uri rowUri = null; + long rowId = -1; + try { + final int match = mUriMatcher.match(uri); + + switch (match) { + case PUBLIC_KEY_RING: + values.put(KeyRings.TYPE, KeyTypes.PUBLIC); + + rowId = db.insertOrThrow(Tables.KEY_RINGS, null, values); + rowUri = KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)); + + break; + case PUBLIC_KEY_RING_KEY: + values.put(Keys.TYPE, KeyTypes.PUBLIC); + + rowId = db.insertOrThrow(Tables.KEYS, null, values); + rowUri = Keys.buildPublicKeysUri(Long.toString(rowId)); + + break; + case PUBLIC_KEY_RING_USER_ID: + rowId = db.insertOrThrow(Tables.USER_IDS, null, values); + rowUri = UserIds.buildPublicUserIdsUri(Long.toString(rowId)); + + break; + case SECRET_KEY_RING: + values.put(KeyRings.TYPE, KeyTypes.SECRET); + + rowId = db.insertOrThrow(Tables.KEY_RINGS, null, values); + rowUri = KeyRings.buildSecretKeyRingsUri(Long.toString(rowId)); + + break; + case SECRET_KEY_RING_KEY: + values.put(Keys.TYPE, KeyTypes.SECRET); + + rowId = db.insertOrThrow(Tables.KEYS, null, values); + rowUri = Keys.buildSecretKeysUri(Long.toString(rowId)); + + break; + case SECRET_KEY_RING_USER_ID: + rowId = db.insertOrThrow(Tables.USER_IDS, null, values); + rowUri = UserIds.buildSecretUserIdsUri(Long.toString(rowId)); + + break; + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } catch (SQLiteConstraintException e) { + Log.e(Constants.TAG, "Constraint exception on insert! Entry already existing?"); + } + + // notify of changes in db + getContext().getContentResolver().notifyChange(uri, null); + + return rowUri; + } + + /** {@inheritDoc} */ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + Log.v(Constants.TAG, "delete(uri=" + uri + ")"); + + final SQLiteDatabase db = mApgDatabase.getWritableDatabase(); + + int count; + final int match = mUriMatcher.match(uri); + + String defaultSelection = null; + switch (match) { + case PUBLIC_KEY_RING_BY_ROW_ID: + case SECRET_KEY_RING_BY_ROW_ID: + defaultSelection = BaseColumns._ID + "=" + uri.getLastPathSegment(); + // corresponding keys and userIds are deleted by ON DELETE CASCADE + count = db.delete(Tables.KEY_RINGS, + buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), selection), + selectionArgs); + break; + case PUBLIC_KEY_RING_BY_MASTER_KEY_ID: + case SECRET_KEY_RING_BY_MASTER_KEY_ID: + defaultSelection = KeyRings.MASTER_KEY_ID + "=" + uri.getLastPathSegment(); + // corresponding keys and userIds are deleted by ON DELETE CASCADE + count = db.delete(Tables.KEY_RINGS, + buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), selection), + selectionArgs); + break; + case PUBLIC_KEY_RING_KEY_BY_ROW_ID: + case SECRET_KEY_RING_KEY_BY_ROW_ID: + count = db.delete(Tables.KEYS, + buildDefaultKeysSelection(uri, getKeyType(match), selection), selectionArgs); + break; + case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID: + case SECRET_KEY_RING_USER_ID_BY_ROW_ID: + count = db.delete(Tables.KEYS, buildDefaultUserIdsSelection(uri, selection), + selectionArgs); + break; + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + + // notify of changes in db + getContext().getContentResolver().notifyChange(uri, null); + + return count; + } + + /** {@inheritDoc} */ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + Log.v(Constants.TAG, "update(uri=" + uri + ", values=" + values.toString() + ")"); + + final SQLiteDatabase db = mApgDatabase.getWritableDatabase(); + + String defaultSelection = null; + int count = 0; + try { + final int match = mUriMatcher.match(uri); + switch (match) { + case PUBLIC_KEY_RING_BY_ROW_ID: + case SECRET_KEY_RING_BY_ROW_ID: + defaultSelection = BaseColumns._ID + "=" + uri.getLastPathSegment(); + + count = db.update( + Tables.KEY_RINGS, + values, + buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), + selection), selectionArgs); + break; + case PUBLIC_KEY_RING_BY_MASTER_KEY_ID: + case SECRET_KEY_RING_BY_MASTER_KEY_ID: + defaultSelection = KeyRings.MASTER_KEY_ID + "=" + uri.getLastPathSegment(); + + count = db.update( + Tables.KEY_RINGS, + values, + buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), + selection), selectionArgs); + break; + case PUBLIC_KEY_RING_KEY_BY_ROW_ID: + case SECRET_KEY_RING_KEY_BY_ROW_ID: + count = db + .update(Tables.KEYS, values, + buildDefaultKeysSelection(uri, getKeyType(match), selection), + selectionArgs); + break; + case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID: + case SECRET_KEY_RING_USER_ID_BY_ROW_ID: + count = db.update(Tables.USER_IDS, values, + buildDefaultUserIdsSelection(uri, selection), selectionArgs); + break; + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } catch (SQLiteConstraintException e) { + Log.e(Constants.TAG, "Constraint exception on update! Entry already existing?"); + } + + // notify of changes in db + getContext().getContentResolver().notifyChange(uri, null); + + return count; + } + + /** + * Build default selection statement for KeyRings. If no extra selection is specified only build + * where clause with rowId + * + * @param uri + * @param selection + * @return + */ + private String buildDefaultKeyRingsSelection(String defaultSelection, Integer keyType, + String selection) { + String andType = ""; + if (keyType != null) { + andType = " AND " + KeyRingsColumns.TYPE + "=" + keyType; + } + + String andSelection = ""; + if (!TextUtils.isEmpty(selection)) { + andSelection = " AND (" + selection + ")"; + } + + return defaultSelection + andType + andSelection; + } + + /** + * Build default selection statement for Keys. If no extra selection is specified only build + * where clause with rowId + * + * @param uri + * @param selection + * @return + */ + private String buildDefaultKeysSelection(Uri uri, Integer keyType, String selection) { + String rowId = uri.getLastPathSegment(); + + String foreignKeyRingRowId = uri.getPathSegments().get(2); + String andForeignKeyRing = " AND " + KeysColumns.KEY_RING_ROW_ID + " = " + + foreignKeyRingRowId; + + String andType = ""; + if (keyType != null) { + andType = " AND " + KeysColumns.TYPE + "=" + keyType; + } + + String andSelection = ""; + if (!TextUtils.isEmpty(selection)) { + andSelection = " AND (" + selection + ")"; + } + + return BaseColumns._ID + "=" + rowId + andForeignKeyRing + andType + andSelection; + } + + /** + * Build default selection statement for UserIds. If no extra selection is specified only build + * where clause with rowId + * + * @param uri + * @param selection + * @return + */ + private String buildDefaultUserIdsSelection(Uri uri, String selection) { + String rowId = uri.getLastPathSegment(); + + String foreignKeyRingRowId = uri.getPathSegments().get(2); + String andForeignKeyRing = " AND " + KeysColumns.KEY_RING_ROW_ID + " = " + + foreignKeyRingRowId; + + String andSelection = ""; + if (!TextUtils.isEmpty(selection)) { + andSelection = " AND (" + selection + ")"; + } + + return BaseColumns._ID + "=" + rowId + andForeignKeyRing + andSelection; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + int match = mUriMatcher.match(uri); + if (match != DATA_STREAM) { + throw new FileNotFoundException(); + } + String fileName = uri.getLastPathSegment(); + File file = new File(getContext().getFilesDir().getAbsolutePath(), fileName); + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderExternal.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderExternal.java new file mode 100644 index 000000000..a62b85621 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderExternal.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.provider; + +/** + * The same content provider as ApgProviderInternal except that it does not give out keyRing and key + * blob data when querying. + * + * This provider is exported with a readPermission in AndroidManifest.xml + */ +public class KeychainProviderExternal extends KeychainProvider { + + public KeychainProviderExternal() { + mInternalProvider = false; + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderInternal.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderInternal.java new file mode 100644 index 000000000..ffac2f7cc --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderInternal.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.provider; + +/** + * This provider is NOT exported in AndroidManifest.xml as it also return the actual secret keys + * from the database + */ +public class KeychainProviderInternal extends KeychainProvider { + + public KeychainProviderInternal() { + mInternalProvider = true; + } + +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java new file mode 100644 index 000000000..340d3c788 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.provider; + +import org.sufficientlysecure.keychain.Constants; + +import android.net.Uri; +import android.provider.BaseColumns; + +public class KeychainServiceBlobContract { + + interface BlobsColumns { + String KEY = "key"; + } + + public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".blobs"; + + private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY); + + public static class Blobs implements BlobsColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI; + } + + private KeychainServiceBlobContract() { + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java new file mode 100644 index 000000000..386349c0e --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2011 Markus Doits <markus.doits@googlemail.com> + * + * 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.provider; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; + +import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns; + +public class KeychainServiceBlobDatabase extends SQLiteOpenHelper { + private static final String DATABASE_NAME = "apg_blob.db"; + private static final int DATABASE_VERSION = 2; + + public static final String TABLE = "data"; + + public KeychainServiceBlobDatabase(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE + " ( " + BaseColumns._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT, " + BlobsColumns.KEY + " TEXT NOT NULL)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // no upgrade necessary yet + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java new file mode 100644 index 000000000..761bfb2fe --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2011 Markus Doits <markus.doits@googlemail.com> + * + * 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.provider; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.Blobs; +import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns; +import org.sufficientlysecure.keychain.util.Log; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.BaseColumns; + + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +public class KeychainServiceBlobProvider extends ContentProvider { + private static final String STORE_PATH = Constants.path.APP_DIR + "/ApgBlobs"; + + private KeychainServiceBlobDatabase mBlobDatabase = null; + + public KeychainServiceBlobProvider() { + File dir = new File(STORE_PATH); + dir.mkdirs(); + } + + @Override + public boolean onCreate() { + mBlobDatabase = new KeychainServiceBlobDatabase(getContext()); + return true; + } + + /** {@inheritDoc} */ + @Override + public Uri insert(Uri uri, ContentValues ignored) { + // ContentValues are actually ignored, because we want to store a blob with no more + // information but have to create an record with the password generated here first + ContentValues vals = new ContentValues(); + + // Insert a random key in the database. This has to provided by the caller when updating or + // getting the blob + String password = UUID.randomUUID().toString(); + vals.put(BlobsColumns.KEY, password); + + SQLiteDatabase db = mBlobDatabase.getWritableDatabase(); + long newRowId = db.insert(KeychainServiceBlobDatabase.TABLE, null, vals); + Uri insertedUri = ContentUris.withAppendedId(Blobs.CONTENT_URI, newRowId); + + return Uri.withAppendedPath(insertedUri, password); + } + + /** {@inheritDoc} */ + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws SecurityException, + FileNotFoundException { + Log.d(Constants.TAG, "openFile() called with uri: " + uri.toString() + " and mode: " + mode); + + List<String> segments = uri.getPathSegments(); + if (segments.size() < 2) { + throw new SecurityException("Password not found in URI"); + } + String id = segments.get(0); + String key = segments.get(1); + + Log.d(Constants.TAG, "Got id: " + id + " and key: " + key); + + // get the data + SQLiteDatabase db = mBlobDatabase.getReadableDatabase(); + Cursor result = db.query(KeychainServiceBlobDatabase.TABLE, new String[] { BaseColumns._ID }, + BaseColumns._ID + " = ? and " + BlobsColumns.KEY + " = ?", + new String[] { id, key }, null, null, null); + + if (result.getCount() == 0) { + // either the key is wrong or no id exists + throw new FileNotFoundException("No file found with that ID and/or password"); + } + + File targetFile = new File(STORE_PATH, id); + if (mode.equals("w")) { + Log.d(Constants.TAG, "Try to open file w"); + if (!targetFile.exists()) { + try { + targetFile.createNewFile(); + } catch (IOException e) { + Log.e(Constants.TAG, "Got IEOException on creating new file", e); + throw new FileNotFoundException("Could not create file to write to"); + } + } + return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_TRUNCATE); + } else if (mode.equals("r")) { + Log.d(Constants.TAG, "Try to open file r"); + if (!targetFile.exists()) { + throw new FileNotFoundException("Error: Could not find the file requested"); + } + return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_READ_ONLY); + } + + return null; + } + + /** {@inheritDoc} */ + @Override + public String getType(Uri uri) { + return null; + } + + /** {@inheritDoc} */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + return null; + } + + /** {@inheritDoc} */ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java new file mode 100644 index 000000000..36049a64c --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -0,0 +1,663 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.provider; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; + +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.helper.PgpConversionHelper; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.helper.PgpMain; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.Log; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.os.RemoteException; + +public class ProviderHelper { + + /** + * Private helper method to get PGPKeyRing from database + * + * @param context + * @param queryUri + * @return + */ + private static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) { + Cursor cursor = context.getContentResolver().query(queryUri, + new String[] { KeyRings._ID, KeyRings.KEY_RING_DATA }, null, null, null); + + PGPKeyRing keyRing = null; + if (cursor != null && cursor.moveToFirst()) { + int keyRingDataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA); + + byte[] data = cursor.getBlob(keyRingDataCol); + if (data != null) { + keyRing = PgpConversionHelper.BytesToPGPKeyRing(data); + } + } + + if (cursor != null) { + cursor.close(); + } + + return keyRing; + } + + /** + * Retrieves the actual PGPPublicKeyRing object from the database blob based on the rowId + * + * @param context + * @param rowId + * @return + */ + public static PGPPublicKeyRing getPGPPublicKeyRingByRowId(Context context, long rowId) { + Uri queryUri = KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)); + return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri); + } + + /** + * Retrieves the actual PGPPublicKeyRing object from the database blob based on the maserKeyId + * + * @param context + * @param masterKeyId + * @return + */ + public static PGPPublicKeyRing getPGPPublicKeyRingByMasterKeyId(Context context, + long masterKeyId) { + Uri queryUri = KeyRings.buildPublicKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId)); + return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri); + } + + /** + * Retrieves the actual PGPPublicKeyRing object from the database blob associated with a key + * with this keyId + * + * @param context + * @param keyId + * @return + */ + public static PGPPublicKeyRing getPGPPublicKeyRingByKeyId(Context context, long keyId) { + Uri queryUri = KeyRings.buildPublicKeyRingsByKeyIdUri(Long.toString(keyId)); + return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri); + } + + /** + * Retrieves the actual PGPPublicKey object from the database blob associated with a key with + * this keyId + * + * @param context + * @param keyId + * @return + */ + public static PGPPublicKey getPGPPublicKeyByKeyId(Context context, long keyId) { + PGPPublicKeyRing keyRing = getPGPPublicKeyRingByKeyId(context, keyId); + if (keyRing == null) { + return null; + } + + return keyRing.getPublicKey(keyId); + } + + /** + * Retrieves the actual PGPSecretKeyRing object from the database blob based on the rowId + * + * @param context + * @param rowId + * @return + */ + public static PGPSecretKeyRing getPGPSecretKeyRingByRowId(Context context, long rowId) { + Uri queryUri = KeyRings.buildSecretKeyRingsUri(Long.toString(rowId)); + return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri); + } + + /** + * Retrieves the actual PGPSecretKeyRing object from the database blob based on the maserKeyId + * + * @param context + * @param masterKeyId + * @return + */ + public static PGPSecretKeyRing getPGPSecretKeyRingByMasterKeyId(Context context, + long masterKeyId) { + Uri queryUri = KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId)); + return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri); + } + + /** + * Retrieves the actual PGPSecretKeyRing object from the database blob associated with a key + * with this keyId + * + * @param context + * @param keyId + * @return + */ + public static PGPSecretKeyRing getPGPSecretKeyRingByKeyId(Context context, long keyId) { + Uri queryUri = KeyRings.buildSecretKeyRingsByKeyIdUri(Long.toString(keyId)); + return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri); + } + + /** + * Retrieves the actual PGPSecretKey object from the database blob associated with a key with + * this keyId + * + * @param context + * @param keyId + * @return + */ + public static PGPSecretKey getPGPSecretKeyByKeyId(Context context, long keyId) { + PGPSecretKeyRing keyRing = getPGPSecretKeyRingByKeyId(context, keyId); + if (keyRing == null) { + return null; + } + + return keyRing.getSecretKey(keyId); + } + + /** + * Saves PGPPublicKeyRing with its keys and userIds in DB + * + * @param context + * @param keyRing + * @return + * @throws IOException + * @throws GeneralException + */ + @SuppressWarnings("unchecked") + public static void saveKeyRing(Context context, PGPPublicKeyRing keyRing) throws IOException { + PGPPublicKey masterKey = keyRing.getPublicKey(); + long masterKeyId = masterKey.getKeyID(); + + // delete old version of this keyRing, which also deletes all keys and userIds on cascade + Uri deleteUri = KeyRings.buildPublicKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId)); + + try { + context.getContentResolver().delete(deleteUri, null, null); + } catch (UnsupportedOperationException e) { + Log.e(Constants.TAG, "Key could not be deleted! Maybe we are creating a new one!", e); + } + + ContentValues values = new ContentValues(); + values.put(KeyRings.MASTER_KEY_ID, masterKeyId); + values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded()); + + // insert new version of this keyRing + Uri uri = KeyRings.buildPublicKeyRingsUri(); + Uri insertedUri = context.getContentResolver().insert(uri, values); + long keyRingRowId = Long.valueOf(insertedUri.getLastPathSegment()); + + // save all keys and userIds included in keyRing object in database + ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); + + int rank = 0; + for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) { + operations.add(buildPublicKeyOperations(context, keyRingRowId, key, rank)); + ++rank; + } + + int userIdRank = 0; + for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) { + operations.add(buildPublicUserIdOperations(context, keyRingRowId, userId, userIdRank)); + ++userIdRank; + } + + try { + context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY_INTERNAL, + operations); + } catch (RemoteException e) { + Log.e(Constants.TAG, "applyBatch failed!", e); + } catch (OperationApplicationException e) { + Log.e(Constants.TAG, "applyBatch failed!", e); + } + } + + /** + * Saves PGPSecretKeyRing with its keys and userIds in DB + * + * @param context + * @param keyRing + * @return + * @throws IOException + * @throws GeneralException + */ + @SuppressWarnings("unchecked") + public static void saveKeyRing(Context context, PGPSecretKeyRing keyRing) throws IOException { + PGPSecretKey masterKey = keyRing.getSecretKey(); + long masterKeyId = masterKey.getKeyID(); + + // delete old version of this keyRing, which also deletes all keys and userIds on cascade + Uri deleteUri = KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId)); + + try { + context.getContentResolver().delete(deleteUri, null, null); + } catch (UnsupportedOperationException e) { + Log.e(Constants.TAG, "Key could not be deleted! Maybe we are creating a new one!", e); + } + + ContentValues values = new ContentValues(); + values.put(KeyRings.MASTER_KEY_ID, masterKeyId); + values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded()); + + // insert new version of this keyRing + Uri uri = KeyRings.buildSecretKeyRingsUri(); + Uri insertedUri = context.getContentResolver().insert(uri, values); + long keyRingRowId = Long.valueOf(insertedUri.getLastPathSegment()); + + // save all keys and userIds included in keyRing object in database + ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); + + int rank = 0; + for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) { + operations.add(buildSecretKeyOperations(context, keyRingRowId, key, rank)); + ++rank; + } + + int userIdRank = 0; + for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) { + operations.add(buildSecretUserIdOperations(context, keyRingRowId, userId, userIdRank)); + ++userIdRank; + } + + try { + context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY_INTERNAL, + operations); + } catch (RemoteException e) { + Log.e(Constants.TAG, "applyBatch failed!", e); + } catch (OperationApplicationException e) { + Log.e(Constants.TAG, "applyBatch failed!", e); + } + } + + /** + * Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing + * + * @param context + * @param keyRingRowId + * @param key + * @param rank + * @return + * @throws IOException + */ + private static ContentProviderOperation buildPublicKeyOperations(Context context, + long keyRingRowId, PGPPublicKey key, int rank) throws IOException { + ContentValues values = new ContentValues(); + values.put(Keys.KEY_ID, key.getKeyID()); + values.put(Keys.IS_MASTER_KEY, key.isMasterKey()); + values.put(Keys.ALGORITHM, key.getAlgorithm()); + values.put(Keys.KEY_SIZE, key.getBitStrength()); + values.put(Keys.CAN_SIGN, PgpHelper.isSigningKey(key)); + values.put(Keys.CAN_ENCRYPT, PgpHelper.isEncryptionKey(key)); + values.put(Keys.IS_REVOKED, key.isRevoked()); + values.put(Keys.CREATION, PgpHelper.getCreationDate(key).getTime() / 1000); + Date expiryDate = PgpHelper.getExpiryDate(key); + if (expiryDate != null) { + values.put(Keys.EXPIRY, expiryDate.getTime() / 1000); + } + values.put(Keys.KEY_RING_ROW_ID, keyRingRowId); + values.put(Keys.KEY_DATA, key.getEncoded()); + values.put(Keys.RANK, rank); + + Uri uri = Keys.buildPublicKeysUri(Long.toString(keyRingRowId)); + + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + /** + * Build ContentProviderOperation to add PublicUserIds to database corresponding to a keyRing + * + * @param context + * @param keyRingRowId + * @param key + * @param rank + * @return + * @throws IOException + */ + private static ContentProviderOperation buildPublicUserIdOperations(Context context, + long keyRingRowId, String userId, int rank) { + ContentValues values = new ContentValues(); + values.put(UserIds.KEY_RING_ROW_ID, keyRingRowId); + values.put(UserIds.USER_ID, userId); + values.put(UserIds.RANK, rank); + + Uri uri = UserIds.buildPublicUserIdsUri(Long.toString(keyRingRowId)); + + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + /** + * Build ContentProviderOperation to add PGPSecretKey to database corresponding to a keyRing + * + * @param context + * @param keyRingRowId + * @param key + * @param rank + * @return + * @throws IOException + */ + private static ContentProviderOperation buildSecretKeyOperations(Context context, + long keyRingRowId, PGPSecretKey key, int rank) throws IOException { + ContentValues values = new ContentValues(); + values.put(Keys.KEY_ID, key.getKeyID()); + values.put(Keys.IS_MASTER_KEY, key.isMasterKey()); + values.put(Keys.ALGORITHM, key.getPublicKey().getAlgorithm()); + values.put(Keys.KEY_SIZE, key.getPublicKey().getBitStrength()); + values.put(Keys.CAN_SIGN, PgpHelper.isSigningKey(key)); + values.put(Keys.CAN_ENCRYPT, PgpHelper.isEncryptionKey(key)); + values.put(Keys.IS_REVOKED, key.getPublicKey().isRevoked()); + values.put(Keys.CREATION, PgpHelper.getCreationDate(key).getTime() / 1000); + Date expiryDate = PgpHelper.getExpiryDate(key); + if (expiryDate != null) { + values.put(Keys.EXPIRY, expiryDate.getTime() / 1000); + } + values.put(Keys.KEY_RING_ROW_ID, keyRingRowId); + values.put(Keys.KEY_DATA, key.getEncoded()); + values.put(Keys.RANK, rank); + + Uri uri = Keys.buildSecretKeysUri(Long.toString(keyRingRowId)); + + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + /** + * Build ContentProviderOperation to add SecretUserIds to database corresponding to a keyRing + * + * @param context + * @param keyRingRowId + * @param key + * @param rank + * @return + * @throws IOException + */ + private static ContentProviderOperation buildSecretUserIdOperations(Context context, + long keyRingRowId, String userId, int rank) { + ContentValues values = new ContentValues(); + values.put(UserIds.KEY_RING_ROW_ID, keyRingRowId); + values.put(UserIds.USER_ID, userId); + values.put(UserIds.RANK, rank); + + Uri uri = UserIds.buildSecretUserIdsUri(Long.toString(keyRingRowId)); + + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + /** + * Private helper method + * + * @param context + * @param queryUri + * @return + */ + private static ArrayList<Long> getKeyRingsMasterKeyIds(Context context, Uri queryUri) { + Cursor cursor = context.getContentResolver().query(queryUri, + new String[] { KeyRings.MASTER_KEY_ID }, null, null, null); + + ArrayList<Long> masterKeyIds = new ArrayList<Long>(); + if (cursor != null) { + int masterKeyIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID); + if (cursor.moveToFirst()) { + do { + masterKeyIds.add(cursor.getLong(masterKeyIdCol)); + } while (cursor.moveToNext()); + } + } + + if (cursor != null) { + cursor.close(); + } + + return masterKeyIds; + } + + /** + * Retrieves ids of all SecretKeyRings + * + * @param context + * @return + */ + public static ArrayList<Long> getSecretKeyRingsMasterKeyIds(Context context) { + Uri queryUri = KeyRings.buildSecretKeyRingsUri(); + return getKeyRingsMasterKeyIds(context, queryUri); + } + + /** + * Retrieves ids of all PublicKeyRings + * + * @param context + * @return + */ + public static ArrayList<Long> getPublicKeyRingsMasterKeyIds(Context context) { + Uri queryUri = KeyRings.buildPublicKeyRingsUri(); + return getKeyRingsMasterKeyIds(context, queryUri); + } + + public static void deletePublicKeyRing(Context context, long rowId) { + ContentResolver cr = context.getContentResolver(); + cr.delete(KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)), null, null); + } + + public static void deleteSecretKeyRing(Context context, long rowId) { + ContentResolver cr = context.getContentResolver(); + cr.delete(KeyRings.buildSecretKeyRingsUri(Long.toString(rowId)), null, null); + } + + /** + * Get master key id of keyring by its row id + * + * @param context + * @param keyRingRowId + * @return + */ + public static long getPublicMasterKeyId(Context context, long keyRingRowId) { + Uri queryUri = KeyRings.buildPublicKeyRingsUri(String.valueOf(keyRingRowId)); + return getMasterKeyId(context, queryUri, keyRingRowId); + } + + /** + * Get master key id of keyring by its row id + * + * @param context + * @param keyRingRowId + * @return + */ + public static long getSecretMasterKeyId(Context context, long keyRingRowId) { + Uri queryUri = KeyRings.buildSecretKeyRingsUri(String.valueOf(keyRingRowId)); + return getMasterKeyId(context, queryUri, keyRingRowId); + } + + /** + * Private helper method to get master key id of keyring by its row id + * + * @param context + * @param queryUri + * @param keyRingRowId + * @return + */ + private static long getMasterKeyId(Context context, Uri queryUri, long keyRingRowId) { + String[] projection = new String[] { KeyRings.MASTER_KEY_ID }; + + ContentResolver cr = context.getContentResolver(); + Cursor cursor = cr.query(queryUri, projection, null, null, null); + + long masterKeyId = -1; + if (cursor != null && cursor.moveToFirst()) { + int masterKeyIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID); + + masterKeyId = cursor.getLong(masterKeyIdCol); + } + + if (cursor != null) { + cursor.close(); + } + + return masterKeyId; + } + + public static ArrayList<String> getPublicKeyRingsAsArmoredString(Context context, + long[] masterKeyIds) { + return getKeyRingsAsArmoredString(context, KeyRings.buildPublicKeyRingsUri(), masterKeyIds); + } + + public static ArrayList<String> getSecretKeyRingsAsArmoredString(Context context, + long[] masterKeyIds) { + return getKeyRingsAsArmoredString(context, KeyRings.buildSecretKeyRingsUri(), masterKeyIds); + } + + private static ArrayList<String> getKeyRingsAsArmoredString(Context context, Uri uri, + long[] masterKeyIds) { + ArrayList<String> output = new ArrayList<String>(); + + if (masterKeyIds != null && masterKeyIds.length > 0) { + + Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, uri, masterKeyIds); + + if (cursor != null) { + int masterIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID); + int dataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA); + if (cursor.moveToFirst()) { + do { + Log.d(Constants.TAG, "masterKeyId: " + cursor.getLong(masterIdCol)); + + // get actual keyring data blob and write it to ByteArrayOutputStream + try { + Object keyRing = null; + byte[] data = cursor.getBlob(dataCol); + if (data != null) { + keyRing = PgpConversionHelper.BytesToPGPKeyRing(data); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ArmoredOutputStream aos = new ArmoredOutputStream(bos); + aos.setHeader("Version", PgpMain.getFullVersion(context)); + + if (keyRing instanceof PGPSecretKeyRing) { + aos.write(((PGPSecretKeyRing) keyRing).getEncoded()); + } else if (keyRing instanceof PGPPublicKeyRing) { + aos.write(((PGPPublicKeyRing) keyRing).getEncoded()); + } + aos.close(); + + String armoredKey = bos.toString("UTF-8"); + + Log.d(Constants.TAG, "armouredKey:" + armoredKey); + + output.add(armoredKey); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException", e); + } + } while (cursor.moveToNext()); + } + } + + if (cursor != null) { + cursor.close(); + } + + } else { + Log.e(Constants.TAG, "No master keys given!"); + } + + if (output.size() > 0) { + return output; + } else { + return null; + } + } + + public static byte[] getPublicKeyRingsAsByteArray(Context context, long[] masterKeyIds) { + return getKeyRingsAsByteArray(context, KeyRings.buildPublicKeyRingsUri(), masterKeyIds); + } + + public static byte[] getSecretKeyRingsAsByteArray(Context context, long[] masterKeyIds) { + return getKeyRingsAsByteArray(context, KeyRings.buildSecretKeyRingsUri(), masterKeyIds); + } + + private static byte[] getKeyRingsAsByteArray(Context context, Uri uri, long[] masterKeyIds) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + if (masterKeyIds != null && masterKeyIds.length > 0) { + + Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, uri, masterKeyIds); + + if (cursor != null) { + int masterIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID); + int dataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA); + if (cursor.moveToFirst()) { + do { + Log.d(Constants.TAG, "masterKeyId: " + cursor.getLong(masterIdCol)); + + // get actual keyring data blob and write it to ByteArrayOutputStream + try { + bos.write(cursor.getBlob(dataCol)); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException", e); + } + } while (cursor.moveToNext()); + } + } + + if (cursor != null) { + cursor.close(); + } + + } else { + Log.e(Constants.TAG, "No master keys given!"); + } + + return bos.toByteArray(); + } + + private static Cursor getCursorWithSelectedKeyringMasterKeyIds(Context context, Uri baseUri, + long[] masterKeyIds) { + Cursor cursor = null; + if (masterKeyIds != null && masterKeyIds.length > 0) { + + String inMasterKeyList = KeyRings.MASTER_KEY_ID + " IN ("; + for (int i = 0; i < masterKeyIds.length; ++i) { + if (i != 0) { + inMasterKeyList += ", "; + } + inMasterKeyList += DatabaseUtils.sqlEscapeString("" + masterKeyIds[i]); + } + inMasterKeyList += ")"; + + cursor = context.getContentResolver().query(baseUri, + new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.KEY_RING_DATA }, + inMasterKeyList, null, null); + } + + return cursor; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainApiService.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainApiService.aidl new file mode 100644 index 000000000..ac8327b8e --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainApiService.aidl @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service; + +import org.sufficientlysecure.keychain.service.handler.IKeychainEncryptHandler; +import org.sufficientlysecure.keychain.service.handler.IKeychainDecryptHandler; +import org.sufficientlysecure.keychain.service.handler.IKeychainGetDecryptionKeyIdHandler; + +/** + * All methods are oneway, which means they are asynchronous and non-blocking. + * Results are returned into given Handler, which has to be implemented on client side. + */ +interface IKeychainApiService { + + /** + * Encrypt + * + * Either inputBytes or inputUri is given, the other should be null. + * + * @param inputBytes + * Byte array you want to encrypt + * @param inputUri + * Blob in ContentProvider you want to encrypt + * @param useAsciiArmor + * Convert bytes to ascii armored text to guard against encoding problems + * @param compression + * Compression: 0x21070001: none, 1: Zip, 2: Zlib, 3: BZip2 + * @param encryptionKeyIds + * Ids of public keys used for encryption + * @param symmetricEncryptionAlgorithm + * 7: AES-128, 8: AES-192, 9: AES-256, 4: Blowfish, 10: Twofish, 3: CAST5, + * 6: DES, 2: Triple DES, 1: IDEA + * @param handler + * Results are returned to this IKeychainEncryptDecryptHandler Handler + * to onSuccessEncrypt(in byte[] output), after successful encryption + */ + oneway void encryptAsymmetric(in byte[] inputBytes, in String inputUri, in boolean useAsciiArmor, + in int compression, in long[] encryptionKeyIds, in int symmetricEncryptionAlgorithm, + in IKeychainEncryptHandler handler); + + /** + * Same as encryptAsymmetric but using a passphrase for symmetric encryption + * + * @param encryptionPassphrase + * Passphrase for direct symmetric encryption using symmetricEncryptionAlgorithm + */ + oneway void encryptSymmetric(in byte[] inputBytes, in String inputUri, in boolean useAsciiArmor, + in int compression, in String encryptionPassphrase, in int symmetricEncryptionAlgorithm, + in IKeychainEncryptHandler handler); + + /** + * Encrypt and sign + * + * Either inputBytes or inputUri is given, the other should be null. + * + * @param inputBytes + * Byte array you want to encrypt + * @param inputUri + * Blob in ContentProvider you want to encrypt + * @param useAsciiArmor + * Convert bytes to ascii armored text to guard against encoding problems + * @param compression + * Compression: 0x21070001: none, 1: Zip, 2: Zlib, 3: BZip2 + * @param encryptionKeyIds + * Ids of public keys used for encryption + * @param symmetricEncryptionAlgorithm + * 7: AES-128, 8: AES-192, 9: AES-256, 4: Blowfish, 10: Twofish, 3: CAST5, + * 6: DES, 2: Triple DES, 1: IDEA + * @param signatureKeyId + * Key id of key to sign with + * @param signatureHashAlgorithm + * 1: MD5, 3: RIPEMD-160, 2: SHA-1, 11: SHA-224, 8: SHA-256, 9: SHA-384, + * 10: SHA-512 + * @param signatureForceV3 + * Force V3 signatures + * @param signaturePassphrase + * Passphrase to unlock signature key + * @param handler + * Results are returned to this IKeychainEncryptDecryptHandler Handler + * to onSuccessEncrypt(in byte[] output), after successful encryption and signing + */ + oneway void encryptAndSignAsymmetric(in byte[] inputBytes, in String inputUri, + in boolean useAsciiArmor, in int compression, in long[] encryptionKeyIds, + in int symmetricEncryptionAlgorithm, in long signatureKeyId, in int signatureHashAlgorithm, + in boolean signatureForceV3, in String signaturePassphrase, + in IKeychainEncryptHandler handler); + + /** + * Same as encryptAndSignAsymmetric but using a passphrase for symmetric encryption + * + * @param encryptionPassphrase + * Passphrase for direct symmetric encryption using symmetricEncryptionAlgorithm + */ + oneway void encryptAndSignSymmetric(in byte[] inputBytes, in String inputUri, + in boolean useAsciiArmor, in int compression, in String encryptionPassphrase, + in int symmetricEncryptionAlgorithm, in long signatureKeyId, in int signatureHashAlgorithm, + in boolean signatureForceV3, in String signaturePassphrase, + in IKeychainEncryptHandler handler); + + /** + * Decrypts and verifies given input bytes. If no signature is present this method + * will only decrypt. + * + * @param inputBytes + * Byte array you want to decrypt and verify + * @param inputUri + * Blob in ContentProvider you want to decrypt and verify + * @param keyPassphrase + * Passphrase to unlock secret key for decryption. + * @param handler + * Handler where to return results to after successful encryption + */ + oneway void decryptAndVerifyAsymmetric(in byte[] inputBytes, in String inputUri, + in String keyPassphrase, in IKeychainDecryptHandler handler); + + /** + * Same as decryptAndVerifyAsymmetric but for symmetric decryption. + * + * @param encryptionPassphrase + * Passphrase to decrypt + */ + oneway void decryptAndVerifySymmetric(in byte[] inputBytes, in String inputUri, + in String encryptionPassphrase, in IKeychainDecryptHandler handler); + + /** + * + */ + oneway void getDecryptionKeyId(in byte[] inputBytes, in String inputUri, + in IKeychainGetDecryptionKeyIdHandler handler); + + +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainKeyService.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainKeyService.aidl new file mode 100644 index 000000000..ecea2b8ff --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainKeyService.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service; + +import org.sufficientlysecure.keychain.service.handler.IKeychainGetKeyringsHandler; + +/** + * All methods are oneway, which means they are asynchronous and non-blocking. + * Results are returned into given Handler, which has to be implemented on client side. + */ +interface IKeychainKeyService { + + oneway void getPublicKeyRings(in long[] masterKeyIds, in boolean asAsciiArmoredStringArray, + in IKeychainGetKeyringsHandler handler); + + oneway void getSecretKeyRings(in long[] masterKeyIds, in boolean asAsciiArmoredStringArray, + in IKeychainGetKeyringsHandler handler); +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainApiService.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainApiService.java new file mode 100644 index 000000000..7c70c3c68 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainApiService.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; + +import org.spongycastle.openpgp.PGPException; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpMain; +import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.service.IKeychainApiService; +import org.sufficientlysecure.keychain.service.handler.IKeychainDecryptHandler; +import org.sufficientlysecure.keychain.service.handler.IKeychainEncryptHandler; +import org.sufficientlysecure.keychain.service.handler.IKeychainGetDecryptionKeyIdHandler; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; + +public class KeychainApiService extends Service { + Context mContext; + + @Override + public void onCreate() { + super.onCreate(); + mContext = this; + Log.d(Constants.TAG, "KeychainApiService, onCreate()"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(Constants.TAG, "KeychainApiService, onDestroy()"); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + // private static void writeToOutputStream(InputStream is, OutputStream os) throws IOException { + // byte[] buffer = new byte[8]; + // int len = 0; + // while ((len = is.read(buffer)) != -1) { + // os.write(buffer, 0, len); + // } + // } + + private synchronized void encryptAndSignSafe(byte[] inputBytes, String inputUri, + boolean useAsciiArmor, int compression, long[] encryptionKeyIds, + String encryptionPassphrase, int symmetricEncryptionAlgorithm, long signatureKeyId, + int signatureHashAlgorithm, boolean signatureForceV3, String signaturePassphrase, + IKeychainEncryptHandler handler) throws RemoteException { + + try { + // TODO use inputUri + + // InputStream inStream = null; + // if (isBlob) { + // ContentResolver cr = getContentResolver(); + // try { + // inStream = cr.openInputStream(Uri.parse(pArgs.getString(arg.BLOB.name()))); + // } catch (Exception e) { + // Log.e(TAG, "... exception on opening blob", e); + // } + // } else { + // inStream = new ByteArrayInputStream(pArgs.getString(arg.MESSAGE.name()).getBytes()); + // } + // InputData in = new InputData(inStream, 0); // XXX Size second param? + + // build InputData and write into OutputStream + InputStream inputStream = new ByteArrayInputStream(inputBytes); + long inputLength = inputBytes.length; + InputData input = new InputData(inputStream, inputLength); + + OutputStream output = new ByteArrayOutputStream(); + + PgpMain.encryptAndSign(mContext, null, input, output, useAsciiArmor, compression, + encryptionKeyIds, encryptionPassphrase, symmetricEncryptionAlgorithm, + signatureKeyId, signatureHashAlgorithm, signatureForceV3, signaturePassphrase); + + output.close(); + + // if (isBlob) { + // ContentResolver cr = getContentResolver(); + // try { + // OutputStream outStream = cr.openOutputStream(Uri.parse(pArgs.getString(arg.BLOB + // .name()))); + // writeToOutputStream(new ByteArrayInputStream(out.toString().getBytes()), outStream); + // outStream.close(); + // } catch (Exception e) { + // Log.e(TAG, "... exception on writing blob", e); + // } + // } else { + // pReturn.putString(ret.RESULT.name(), out.toString()); + // } + + byte[] outputBytes = ((ByteArrayOutputStream) output).toByteArray(); + + // return over handler on client side + handler.onSuccess(outputBytes, null); + } catch (Exception e) { + Log.e(Constants.TAG, "KeychainService, Exception!", e); + + try { + handler.onException(getExceptionId(e), e.getMessage()); + } catch (Exception t) { + Log.e(Constants.TAG, "Error returning exception to client", t); + } + } + } + + private synchronized void decryptAndVerifySafe(byte[] inputBytes, String inputUri, + String passphrase, boolean assumeSymmetric, IKeychainDecryptHandler handler) + throws RemoteException { + + try { + // build InputData and write into OutputStream + InputStream inputStream = new ByteArrayInputStream(inputBytes); + long inputLength = inputBytes.length; + InputData inputData = new InputData(inputStream, inputLength); + + OutputStream outputStream = new ByteArrayOutputStream(); + + Bundle outputBundle = PgpMain.decryptAndVerify(mContext, null, inputData, outputStream, + passphrase, assumeSymmetric); + + outputStream.close(); + + byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray(); + + // get signature informations from bundle + boolean signature = outputBundle.getBoolean(KeychainIntentService.RESULT_SIGNATURE); + long signatureKeyId = outputBundle + .getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID); + String signatureUserId = outputBundle + .getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID); + boolean signatureSuccess = outputBundle + .getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS); + boolean signatureUnknown = outputBundle + .getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN); + + // return over handler on client side + handler.onSuccess(outputBytes, null, signature, signatureKeyId, signatureUserId, + signatureSuccess, signatureUnknown); + } catch (Exception e) { + Log.e(Constants.TAG, "KeychainService, Exception!", e); + + try { + handler.onException(getExceptionId(e), e.getMessage()); + } catch (Exception t) { + Log.e(Constants.TAG, "Error returning exception to client", t); + } + } + } + + private synchronized void getDecryptionKeySafe(byte[] inputBytes, String inputUri, + IKeychainGetDecryptionKeyIdHandler handler) { + + // TODO: implement inputUri + + try { + InputStream inputStream = new ByteArrayInputStream(inputBytes); + + long secretKeyId = Id.key.none; + boolean symmetric; + + try { + secretKeyId = PgpMain.getDecryptionKeyId(KeychainApiService.this, inputStream); + if (secretKeyId == Id.key.none) { + throw new PgpGeneralException(getString(R.string.error_noSecretKeyFound)); + } + symmetric = false; + } catch (PgpMain.NoAsymmetricEncryptionException e) { + secretKeyId = Id.key.symmetric; + if (!PgpMain.hasSymmetricEncryption(KeychainApiService.this, inputStream)) { + throw new PgpGeneralException(getString(R.string.error_noKnownEncryptionFound)); + } + symmetric = true; + } + + handler.onSuccess(secretKeyId, symmetric); + + } catch (Exception e) { + Log.e(Constants.TAG, "KeychainService, Exception!", e); + + try { + handler.onException(getExceptionId(e), e.getMessage()); + } catch (Exception t) { + Log.e(Constants.TAG, "Error returning exception to client", t); + } + } + } + + /** + * This is the implementation of the interface IKeychainService. All methods are oneway, meaning + * asynchronous and return to the client using IKeychainHandler. + * + * The real PGP code is located in PGPMain. + */ + private final IKeychainApiService.Stub mBinder = new IKeychainApiService.Stub() { + + @Override + public void encryptAsymmetric(byte[] inputBytes, String inputUri, boolean useAsciiArmor, + int compression, long[] encryptionKeyIds, int symmetricEncryptionAlgorithm, + IKeychainEncryptHandler handler) throws RemoteException { + + encryptAndSignSafe(inputBytes, inputUri, useAsciiArmor, compression, encryptionKeyIds, + null, symmetricEncryptionAlgorithm, Id.key.none, 0, false, null, handler); + } + + @Override + public void encryptSymmetric(byte[] inputBytes, String inputUri, boolean useAsciiArmor, + int compression, String encryptionPassphrase, int symmetricEncryptionAlgorithm, + IKeychainEncryptHandler handler) throws RemoteException { + + encryptAndSignSafe(inputBytes, inputUri, useAsciiArmor, compression, null, + encryptionPassphrase, symmetricEncryptionAlgorithm, Id.key.none, 0, false, + null, handler); + } + + @Override + public void encryptAndSignAsymmetric(byte[] inputBytes, String inputUri, + boolean useAsciiArmor, int compression, long[] encryptionKeyIds, + int symmetricEncryptionAlgorithm, long signatureKeyId, int signatureHashAlgorithm, + boolean signatureForceV3, String signaturePassphrase, + IKeychainEncryptHandler handler) throws RemoteException { + + encryptAndSignSafe(inputBytes, inputUri, useAsciiArmor, compression, encryptionKeyIds, + null, symmetricEncryptionAlgorithm, signatureKeyId, signatureHashAlgorithm, + signatureForceV3, signaturePassphrase, handler); + } + + @Override + public void encryptAndSignSymmetric(byte[] inputBytes, String inputUri, + boolean useAsciiArmor, int compression, String encryptionPassphrase, + int symmetricEncryptionAlgorithm, long signatureKeyId, int signatureHashAlgorithm, + boolean signatureForceV3, String signaturePassphrase, + IKeychainEncryptHandler handler) throws RemoteException { + + encryptAndSignSafe(inputBytes, inputUri, useAsciiArmor, compression, null, + encryptionPassphrase, symmetricEncryptionAlgorithm, signatureKeyId, + signatureHashAlgorithm, signatureForceV3, signaturePassphrase, handler); + } + + @Override + public void decryptAndVerifyAsymmetric(byte[] inputBytes, String inputUri, + String keyPassphrase, IKeychainDecryptHandler handler) throws RemoteException { + + decryptAndVerifySafe(inputBytes, inputUri, keyPassphrase, false, handler); + } + + @Override + public void decryptAndVerifySymmetric(byte[] inputBytes, String inputUri, + String encryptionPassphrase, IKeychainDecryptHandler handler) + throws RemoteException { + + decryptAndVerifySafe(inputBytes, inputUri, encryptionPassphrase, true, handler); + } + + @Override + public void getDecryptionKeyId(byte[] inputBytes, String inputUri, + IKeychainGetDecryptionKeyIdHandler handler) throws RemoteException { + + getDecryptionKeySafe(inputBytes, inputUri, handler); + } + + }; + + /** + * As we can not throw an exception through Android RPC, we assign identifiers to the exception + * types. + * + * @param e + * @return + */ + private int getExceptionId(Exception e) { + if (e instanceof NoSuchProviderException) { + return 0; + } else if (e instanceof NoSuchAlgorithmException) { + return 1; + } else if (e instanceof SignatureException) { + return 2; + } else if (e instanceof IOException) { + return 3; + } else if (e instanceof PgpGeneralException) { + return 4; + } else if (e instanceof PGPException) { + return 5; + } else { + return -1; + } + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentService.java new file mode 100644 index 000000000..e525fe96e --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -0,0 +1,884 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; + +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.helper.PgpConversionHelper; +import org.sufficientlysecure.keychain.helper.PgpMain; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.DataStream; +import org.sufficientlysecure.keychain.util.HkpKeyServer; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ProgressDialogUpdater; +import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo; +import org.sufficientlysecure.keychain.R; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; + + +/** + * This Service contains all important long lasting operations for APG. 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 ProgressDialogUpdater { + + /* extras that can be given by intent */ + public static final String EXTRA_MESSENGER = "messenger"; + public static final String EXTRA_ACTION = "action"; + public static final String EXTRA_DATA = "data"; + + /* possible EXTRA_ACTIONs */ + public static final int ACTION_ENCRYPT_SIGN = 10; + + public static final int ACTION_DECRYPT_VERIFY = 20; + + public static final int ACTION_SAVE_KEYRING = 30; + public static final int ACTION_GENERATE_KEY = 31; + public static final int ACTION_GENERATE_DEFAULT_RSA_KEYS = 32; + + public static final int ACTION_DELETE_FILE_SECURELY = 40; + + public static final int ACTION_IMPORT_KEYRING = 50; + public static final int ACTION_EXPORT_KEYRING = 51; + + public static final int ACTION_UPLOAD_KEYRING = 60; + public static final int ACTION_QUERY_KEYRING = 61; + + public static final int ACTION_SIGN_KEYRING = 70; + + /* keys for data bundle */ + + // encrypt, decrypt, import export + public static final String TARGET = "target"; + // possible targets: + public static final int TARGET_BYTES = 1; + public static final int TARGET_FILE = 2; + public static final int TARGET_STREAM = 3; + + // encrypt + public static final String ENCRYPT_SECRET_KEY_ID = "secretKeyId"; + public static final String ENCRYPT_USE_ASCII_AMOR = "useAsciiAmor"; + public static final String ENCRYPT_ENCRYPTION_KEYS_IDS = "encryptionKeysIds"; + public static final String ENCRYPT_COMPRESSION_ID = "compressionId"; + public static final String ENCRYPT_GENERATE_SIGNATURE = "generateSignature"; + public static final String ENCRYPT_SIGN_ONLY = "signOnly"; + public static final String ENCRYPT_MESSAGE_BYTES = "messageBytes"; + public static final String ENCRYPT_INPUT_FILE = "inputFile"; + public static final String ENCRYPT_OUTPUT_FILE = "outputFile"; + public static final String ENCRYPT_PROVIDER_URI = "providerUri"; + + // decrypt/verify + public static final String DECRYPT_SIGNED_ONLY = "signedOnly"; + public static final String DECRYPT_RETURN_BYTES = "returnBinary"; + public static final String DECRYPT_CIPHERTEXT_BYTES = "ciphertextBytes"; + public static final String DECRYPT_ASSUME_SYMMETRIC = "assumeSymmetric"; + public static final String DECRYPT_LOOKUP_UNKNOWN_KEY = "lookupUnknownKey"; + + // save keyring + public static final String SAVE_KEYRING_NEW_PASSPHRASE = "newPassphrase"; + public static final String SAVE_KEYRING_CURRENT_PASSPHRASE = "currentPassphrase"; + public static final String SAVE_KEYRING_USER_IDS = "userIds"; + public static final String SAVE_KEYRING_KEYS = "keys"; + public static final String SAVE_KEYRING_KEYS_USAGES = "keysUsages"; + public static final String SAVE_KEYRING_MASTER_KEY_ID = "masterKeyId"; + + // generate key + public static final String GENERATE_KEY_ALGORITHM = "algorithm"; + public static final String GENERATE_KEY_KEY_SIZE = "keySize"; + public static final String GENERATE_KEY_SYMMETRIC_PASSPHRASE = "passphrase"; + public static final String GENERATE_KEY_MASTER_KEY = "masterKey"; + + // delete file securely + public static final String DELETE_FILE = "deleteFile"; + + // import key + public static final String IMPORT_INPUT_STREAM = "importInputStream"; + public static final String IMPORT_FILENAME = "importFilename"; + public static final String IMPORT_BYTES = "importBytes"; + // public static final String IMPORT_KEY_TYPE = "importKeyType"; + + // export key + public static final String EXPORT_OUTPUT_STREAM = "exportOutputStream"; + public static final String EXPORT_FILENAME = "exportFilename"; + public static final String EXPORT_KEY_TYPE = "exportKeyType"; + public static final String EXPORT_ALL = "exportAll"; + public static final String EXPORT_KEY_RING_MASTER_KEY_ID = "exportKeyRingId"; + + // upload key + public static final String UPLOAD_KEY_SERVER = "uploadKeyServer"; + public static final String UPLOAD_KEY_KEYRING_ROW_ID = "uploadKeyRingId"; + + // query key + public static final String QUERY_KEY_SERVER = "queryKeyServer"; + public static final String QUERY_KEY_TYPE = "queryKeyType"; + public static final String QUERY_KEY_STRING = "queryKeyString"; + public static final String QUERY_KEY_ID = "queryKeyId"; + + // sign key + public static final String SIGN_KEY_MASTER_KEY_ID = "signKeyMasterKeyId"; + public static final String SIGN_KEY_PUB_KEY_ID = "signKeyPubKeyId"; + + /* + * possible data keys as result send over messenger + */ + // keys + public static final String RESULT_NEW_KEY = "newKey"; + public static final String RESULT_NEW_KEY2 = "newKey2"; + + // encrypt + public static final String RESULT_SIGNATURE_BYTES = "signatureData"; + public static final String RESULT_SIGNATURE_STRING = "signatureText"; + public static final String RESULT_ENCRYPTED_STRING = "encryptedMessage"; + public static final String RESULT_ENCRYPTED_BYTES = "encryptedData"; + public static final String RESULT_URI = "resultUri"; + + // decrypt/verify + public static final String RESULT_DECRYPTED_STRING = "decryptedMessage"; + public static final String RESULT_DECRYPTED_BYTES = "decryptedData"; + public static final String RESULT_SIGNATURE = "signature"; + public static final String RESULT_SIGNATURE_KEY_ID = "signatureKeyId"; + public static final String RESULT_SIGNATURE_USER_ID = "signatureUserId"; + + public static final String RESULT_SIGNATURE_SUCCESS = "signatureSuccess"; + public static final String RESULT_SIGNATURE_UNKNOWN = "signatureUnknown"; + public static final String RESULT_SIGNATURE_LOOKUP_KEY = "lookupKey"; + + // import + public static final String RESULT_IMPORT_ADDED = "added"; + public static final String RESULT_IMPORT_UPDATED = "updated"; + public static final String RESULT_IMPORT_BAD = "bad"; + + // export + public static final String RESULT_EXPORT = "exported"; + + // query + public static final String RESULT_QUERY_KEY_KEY_DATA = "queryKeyKeyData"; + public static final String RESULT_QUERY_KEY_SEARCH_RESULT = "queryKeySearchResult"; + + Messenger mMessenger; + + public KeychainIntentService() { + super("ApgService"); + } + + /** + * 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) { + 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) || extras + .containsKey(EXTRA_ACTION))) { + Log.e(Constants.TAG, + "Extra bundle must contain a messenger, a data bundle, and an action!"); + return; + } + + mMessenger = (Messenger) extras.get(EXTRA_MESSENGER); + Bundle data = extras.getBundle(EXTRA_DATA); + + OtherHelper.logDebugBundle(data, "EXTRA_DATA"); + + int action = extras.getInt(EXTRA_ACTION); + + // execute action from extra bundle + switch (action) { + case ACTION_ENCRYPT_SIGN: + + try { + /* Input */ + int target = data.getInt(TARGET); + + long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID); + String encryptionPassphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE); + + boolean useAsciiArmor = data.getBoolean(ENCRYPT_USE_ASCII_AMOR); + long encryptionKeyIds[] = data.getLongArray(ENCRYPT_ENCRYPTION_KEYS_IDS); + int compressionId = data.getInt(ENCRYPT_COMPRESSION_ID); + boolean generateSignature = data.getBoolean(ENCRYPT_GENERATE_SIGNATURE); + boolean signOnly = data.getBoolean(ENCRYPT_SIGN_ONLY); + + InputStream inStream = null; + long inLength = -1; + InputData inputData = null; + OutputStream outStream = null; + String streamFilename = null; + switch (target) { + case TARGET_BYTES: /* encrypting bytes directly */ + byte[] bytes = data.getByteArray(ENCRYPT_MESSAGE_BYTES); + + inStream = new ByteArrayInputStream(bytes); + inLength = bytes.length; + + inputData = new InputData(inStream, inLength); + outStream = new ByteArrayOutputStream(); + + break; + case TARGET_FILE: /* encrypting file */ + String inputFile = data.getString(ENCRYPT_INPUT_FILE); + String outputFile = data.getString(ENCRYPT_OUTPUT_FILE); + + // check if storage is ready + if (!FileHelper.isStorageMounted(inputFile) + || !FileHelper.isStorageMounted(outputFile)) { + throw new PgpGeneralException( + getString(R.string.error_externalStorageNotReady)); + } + + inStream = new FileInputStream(inputFile); + File file = new File(inputFile); + inLength = file.length(); + inputData = new InputData(inStream, inLength); + + outStream = new FileOutputStream(outputFile); + + break; + + case TARGET_STREAM: /* Encrypting stream from content uri */ + Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI); + + // InputStream + InputStream in = getContentResolver().openInputStream(providerUri); + inLength = PgpMain.getLengthOfStream(in); + inputData = new InputData(in, inLength); + + // OutputStream + try { + while (true) { + streamFilename = PgpMain.generateRandomFilename(32); + if (streamFilename == null) { + throw new PgpMain.PgpGeneralException( + "couldn't generate random file name"); + } + openFileInput(streamFilename).close(); + } + } catch (FileNotFoundException e) { + // found a name that isn't used yet + } + outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE); + + break; + + default: + throw new PgpMain.PgpGeneralException("No target choosen!"); + + } + + /* Operation */ + + if (generateSignature) { + Log.d(Constants.TAG, "generating signature..."); + PgpMain.generateSignature(this, this, inputData, outStream, useAsciiArmor, + false, secretKeyId, PassphraseCacheService.getCachedPassphrase(this, + secretKeyId), Preferences.getPreferences(this) + .getDefaultHashAlgorithm(), Preferences.getPreferences(this) + .getForceV3Signatures()); + } else if (signOnly) { + Log.d(Constants.TAG, "sign only..."); + PgpMain.signText(this, this, inputData, outStream, secretKeyId, + PassphraseCacheService.getCachedPassphrase(this, secretKeyId), + Preferences.getPreferences(this).getDefaultHashAlgorithm(), Preferences + .getPreferences(this).getForceV3Signatures()); + } else { + Log.d(Constants.TAG, "encrypt..."); + PgpMain.encryptAndSign(this, this, inputData, outStream, useAsciiArmor, + compressionId, encryptionKeyIds, encryptionPassphrase, Preferences + .getPreferences(this).getDefaultEncryptionAlgorithm(), + secretKeyId, + Preferences.getPreferences(this).getDefaultHashAlgorithm(), Preferences + .getPreferences(this).getForceV3Signatures(), + PassphraseCacheService.getCachedPassphrase(this, secretKeyId)); + } + + outStream.close(); + + /* Output */ + + Bundle resultData = new Bundle(); + + switch (target) { + case TARGET_BYTES: + if (useAsciiArmor) { + String output = new String( + ((ByteArrayOutputStream) outStream).toByteArray()); + if (generateSignature) { + resultData.putString(RESULT_SIGNATURE_STRING, output); + } else { + resultData.putString(RESULT_ENCRYPTED_STRING, output); + } + } else { + byte output[] = ((ByteArrayOutputStream) outStream).toByteArray(); + if (generateSignature) { + resultData.putByteArray(RESULT_SIGNATURE_BYTES, output); + } else { + resultData.putByteArray(RESULT_ENCRYPTED_BYTES, output); + } + } + + break; + case TARGET_FILE: + // nothing, file was written, just send okay + + break; + case TARGET_STREAM: + String uri = DataStream.buildDataStreamUri(streamFilename).toString(); + resultData.putString(RESULT_URI, uri); + + break; + } + + OtherHelper.logDebugBundle(resultData, "resultData"); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_DECRYPT_VERIFY: + try { + /* Input */ + int target = data.getInt(TARGET); + + long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID); + byte[] bytes = data.getByteArray(DECRYPT_CIPHERTEXT_BYTES); + boolean signedOnly = data.getBoolean(DECRYPT_SIGNED_ONLY); + boolean returnBytes = data.getBoolean(DECRYPT_RETURN_BYTES); + boolean assumeSymmetricEncryption = data.getBoolean(DECRYPT_ASSUME_SYMMETRIC); + + boolean lookupUnknownKey = data.getBoolean(DECRYPT_LOOKUP_UNKNOWN_KEY); + + InputStream inStream = null; + long inLength = -1; + InputData inputData = null; + OutputStream outStream = null; + String streamFilename = null; + switch (target) { + case TARGET_BYTES: /* decrypting bytes directly */ + inStream = new ByteArrayInputStream(bytes); + inLength = bytes.length; + + inputData = new InputData(inStream, inLength); + outStream = new ByteArrayOutputStream(); + + break; + + case TARGET_FILE: /* decrypting file */ + String inputFile = data.getString(ENCRYPT_INPUT_FILE); + String outputFile = data.getString(ENCRYPT_OUTPUT_FILE); + + // check if storage is ready + if (!FileHelper.isStorageMounted(inputFile) + || !FileHelper.isStorageMounted(outputFile)) { + throw new PgpGeneralException( + getString(R.string.error_externalStorageNotReady)); + } + + // InputStream + inLength = -1; + inStream = new FileInputStream(inputFile); + File file = new File(inputFile); + inLength = file.length(); + inputData = new InputData(inStream, inLength); + + // OutputStream + outStream = new FileOutputStream(outputFile); + + break; + + case TARGET_STREAM: /* decrypting stream from content uri */ + Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI); + + // InputStream + InputStream in = getContentResolver().openInputStream(providerUri); + inLength = PgpMain.getLengthOfStream(in); + inputData = new InputData(in, inLength); + + // OutputStream + try { + while (true) { + streamFilename = PgpMain.generateRandomFilename(32); + if (streamFilename == null) { + throw new PgpMain.PgpGeneralException( + "couldn't generate random file name"); + } + openFileInput(streamFilename).close(); + } + } catch (FileNotFoundException e) { + // found a name that isn't used yet + } + outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE); + + break; + + default: + throw new PgpMain.PgpGeneralException("No target choosen!"); + + } + + /* Operation */ + + Bundle resultData = new Bundle(); + + // verifyText and decrypt returning additional resultData values for the + // verification of signatures + if (signedOnly) { + resultData = PgpMain.verifyText(this, this, inputData, outStream, + lookupUnknownKey); + } else { + resultData = PgpMain.decryptAndVerify(this, this, inputData, outStream, + PassphraseCacheService.getCachedPassphrase(this, secretKeyId), + assumeSymmetricEncryption); + } + + outStream.close(); + + /* Output */ + + switch (target) { + case TARGET_BYTES: + if (returnBytes) { + byte output[] = ((ByteArrayOutputStream) outStream).toByteArray(); + resultData.putByteArray(RESULT_DECRYPTED_BYTES, output); + } else { + String output = new String( + ((ByteArrayOutputStream) outStream).toByteArray()); + resultData.putString(RESULT_DECRYPTED_STRING, output); + } + + break; + case TARGET_FILE: + // nothing, file was written, just send okay and verification bundle + + break; + case TARGET_STREAM: + String uri = DataStream.buildDataStreamUri(streamFilename).toString(); + resultData.putString(RESULT_URI, uri); + + break; + } + + OtherHelper.logDebugBundle(resultData, "resultData"); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_SAVE_KEYRING: + + try { + /* Input */ + String oldPassPhrase = data.getString(SAVE_KEYRING_CURRENT_PASSPHRASE); + String newPassPhrase = data.getString(SAVE_KEYRING_NEW_PASSPHRASE); + if (newPassPhrase == null) { + newPassPhrase = oldPassPhrase; + } + ArrayList<String> userIds = data.getStringArrayList(SAVE_KEYRING_USER_IDS); + ArrayList<PGPSecretKey> keys = PgpConversionHelper.BytesToPGPSecretKeyList(data + .getByteArray(SAVE_KEYRING_KEYS)); + ArrayList<Integer> keysUsages = data.getIntegerArrayList(SAVE_KEYRING_KEYS_USAGES); + long masterKeyId = data.getLong(SAVE_KEYRING_MASTER_KEY_ID); + + /* Operation */ + PgpMain.buildSecretKey(this, userIds, keys, keysUsages, masterKeyId, oldPassPhrase, + newPassPhrase, this); + PassphraseCacheService.addCachedPassphrase(this, masterKeyId, newPassPhrase); + + /* Output */ + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_GENERATE_KEY: + + try { + /* Input */ + int algorithm = data.getInt(GENERATE_KEY_ALGORITHM); + String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE); + int keysize = data.getInt(GENERATE_KEY_KEY_SIZE); + PGPSecretKey masterKey = null; + if (data.containsKey(GENERATE_KEY_MASTER_KEY)) { + masterKey = PgpConversionHelper.BytesToPGPSecretKey(data + .getByteArray(GENERATE_KEY_MASTER_KEY)); + } + + /* Operation */ + PGPSecretKeyRing newKeyRing = PgpMain.createKey(this, algorithm, keysize, + passphrase, masterKey); + + /* Output */ + Bundle resultData = new Bundle(); + resultData.putByteArray(RESULT_NEW_KEY, + PgpConversionHelper.PGPSecretKeyRingToBytes(newKeyRing)); + + OtherHelper.logDebugBundle(resultData, "resultData"); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_GENERATE_DEFAULT_RSA_KEYS: + // generate one RSA 2048 key for signing and one subkey for encrypting! + try { + /* Input */ + String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE); + + /* Operation */ + PGPSecretKeyRing masterKeyRing = PgpMain.createKey(this, Id.choice.algorithm.rsa, + 2048, passphrase, null); + + PGPSecretKeyRing subKeyRing = PgpMain.createKey(this, Id.choice.algorithm.rsa, + 2048, passphrase, masterKeyRing.getSecretKey()); + + /* Output */ + Bundle resultData = new Bundle(); + resultData.putByteArray(RESULT_NEW_KEY, + PgpConversionHelper.PGPSecretKeyRingToBytes(masterKeyRing)); + resultData.putByteArray(RESULT_NEW_KEY2, + PgpConversionHelper.PGPSecretKeyRingToBytes(subKeyRing)); + + OtherHelper.logDebugBundle(resultData, "resultData"); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_DELETE_FILE_SECURELY: + try { + /* Input */ + String deleteFile = data.getString(DELETE_FILE); + + /* Operation */ + try { + PgpMain.deleteFileSecurely(this, this, new File(deleteFile)); + } catch (FileNotFoundException e) { + throw new PgpMain.PgpGeneralException(getString(R.string.error_fileNotFound, + deleteFile)); + } catch (IOException e) { + throw new PgpMain.PgpGeneralException(getString( + R.string.error_fileDeleteFailed, deleteFile)); + } + + /* Output */ + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_IMPORT_KEYRING: + try { + + /* Input */ + int target = data.getInt(TARGET); + + // int keyType = Id.type.public_key; + // if (data.containsKey(IMPORT_KEY_TYPE)) { + // keyType = data.getInt(IMPORT_KEY_TYPE); + // } + + /* Operation */ + InputStream inStream = null; + long inLength = -1; + InputData inputData = null; + switch (target) { + case TARGET_BYTES: /* import key from bytes directly */ + byte[] bytes = data.getByteArray(IMPORT_BYTES); + + inStream = new ByteArrayInputStream(bytes); + inLength = bytes.length; + + inputData = new InputData(inStream, inLength); + + break; + case TARGET_FILE: /* import key from file */ + String inputFile = data.getString(IMPORT_FILENAME); + + inStream = new FileInputStream(inputFile); + File file = new File(inputFile); + inLength = file.length(); + inputData = new InputData(inStream, inLength); + + break; + + case TARGET_STREAM: + // TODO: not implemented + break; + } + + Bundle resultData = new Bundle(); + resultData = PgpMain.importKeyRings(this, inputData, this); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_EXPORT_KEYRING: + try { + + /* Input */ + int keyType = Id.type.public_key; + if (data.containsKey(EXPORT_KEY_TYPE)) { + keyType = data.getInt(EXPORT_KEY_TYPE); + } + + String outputFile = data.getString(EXPORT_FILENAME); + + boolean exportAll = data.getBoolean(EXPORT_ALL); + long keyRingMasterKeyId = -1; + if (!exportAll) { + keyRingMasterKeyId = data.getLong(EXPORT_KEY_RING_MASTER_KEY_ID); + } + + /* Operation */ + + // check if storage is ready + if (!FileHelper.isStorageMounted(outputFile)) { + throw new PgpGeneralException(getString(R.string.error_externalStorageNotReady)); + } + + // OutputStream + FileOutputStream outStream = new FileOutputStream(outputFile); + + ArrayList<Long> keyRingMasterKeyIds = new ArrayList<Long>(); + if (exportAll) { + // get all key ring row ids based on export type + + if (keyType == Id.type.public_key) { + keyRingMasterKeyIds = ProviderHelper.getPublicKeyRingsMasterKeyIds(this); + } else { + keyRingMasterKeyIds = ProviderHelper.getSecretKeyRingsMasterKeyIds(this); + } + } else { + keyRingMasterKeyIds.add(keyRingMasterKeyId); + } + + Bundle resultData = new Bundle(); + resultData = PgpMain.exportKeyRings(this, keyRingMasterKeyIds, keyType, outStream, + this); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_UPLOAD_KEYRING: + try { + + /* Input */ + int keyRingRowId = data.getInt(UPLOAD_KEY_KEYRING_ROW_ID); + String keyServer = data.getString(UPLOAD_KEY_SERVER); + + /* Operation */ + HkpKeyServer server = new HkpKeyServer(keyServer); + + PGPPublicKeyRing keyring = ProviderHelper.getPGPPublicKeyRingByRowId(this, + keyRingRowId); + if (keyring != null) { + boolean uploaded = PgpMain.uploadKeyRingToServer(server, + (PGPPublicKeyRing) keyring); + if (!uploaded) { + throw new PgpGeneralException("Unable to export key to selected server"); + } + } + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_QUERY_KEYRING: + try { + + /* Input */ + int queryType = data.getInt(QUERY_KEY_TYPE); + String keyServer = data.getString(QUERY_KEY_SERVER); + + String queryString = data.getString(QUERY_KEY_STRING); + long keyId = data.getLong(QUERY_KEY_ID); + + /* Operation */ + Bundle resultData = new Bundle(); + + HkpKeyServer server = new HkpKeyServer(keyServer); + if (queryType == Id.keyserver.search) { + ArrayList<KeyInfo> searchResult = server.search(queryString); + + resultData.putParcelableArrayList(RESULT_QUERY_KEY_SEARCH_RESULT, searchResult); + } else if (queryType == Id.keyserver.get) { + String keyData = server.get(keyId); + + resultData.putString(RESULT_QUERY_KEY_KEY_DATA, keyData); + } + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + case ACTION_SIGN_KEYRING: + try { + + /* Input */ + long masterKeyId = data.getLong(SIGN_KEY_MASTER_KEY_ID); + long pubKeyId = data.getLong(SIGN_KEY_PUB_KEY_ID); + + /* Operation */ + String signaturePassPhrase = PassphraseCacheService.getCachedPassphrase(this, + masterKeyId); + + PGPPublicKeyRing signedPubKeyRing = PgpMain.signKey(this, masterKeyId, pubKeyId, + signaturePassPhrase); + + // store the signed key in our local cache + int retval = PgpMain.storeKeyRingInCache(this, signedPubKeyRing); + if (retval != Id.return_value.ok && retval != Id.return_value.updated) { + throw new PgpGeneralException("Failed to store signed key in local cache"); + } + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY); + } catch (Exception e) { + sendErrorToHandler(e); + } + + break; + + default: + break; + } + + } + + private void sendErrorToHandler(Exception e) { + Log.e(Constants.TAG, "ApgService Exception: ", e); + e.printStackTrace(); + + Bundle data = new Bundle(); + data.putString(KeychainIntentServiceHandler.DATA_ERROR, e.getMessage()); + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_EXCEPTION, null, data); + } + + private void sendMessageToHandler(Integer arg1, Integer arg2, Bundle data) { + Message msg = Message.obtain(); + msg.arg1 = arg1; + 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(Integer arg1, Bundle data) { + sendMessageToHandler(arg1, null, data); + } + + private void sendMessageToHandler(Integer arg1) { + sendMessageToHandler(arg1, 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(KeychainIntentServiceHandler.DATA_MESSAGE, message); + } + data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS, progress); + data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS_MAX, max); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_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); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java new file mode 100644 index 000000000..4e43f2b3e --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service; + +import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.R; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.FragmentActivity; +import android.widget.Toast; + +public class KeychainIntentServiceHandler extends Handler { + + // possible messages send from this service to handler on ui + public static final int MESSAGE_OKAY = 1; + public static final int MESSAGE_EXCEPTION = 2; + public static final int MESSAGE_UPDATE_PROGRESS = 3; + + // possible data keys for messages + public static final String DATA_ERROR = "error"; + public static final String DATA_PROGRESS = "progress"; + public static final String DATA_PROGRESS_MAX = "max"; + public static final String DATA_MESSAGE = "message"; + public static final String DATA_MESSAGE_ID = "message_id"; + + Activity mActivity; + ProgressDialogFragment mProgressDialogFragment; + + public KeychainIntentServiceHandler(Activity activity) { + this.mActivity = activity; + } + + public KeychainIntentServiceHandler(Activity activity, ProgressDialogFragment progressDialogFragment) { + this.mActivity = activity; + this.mProgressDialogFragment = progressDialogFragment; + } + + public KeychainIntentServiceHandler(Activity activity, int progressDialogMessageId, int progressDialogStyle) { + this.mActivity = activity; + this.mProgressDialogFragment = ProgressDialogFragment.newInstance(progressDialogMessageId, + progressDialogStyle); + } + + public void showProgressDialog(FragmentActivity activity) { + mProgressDialogFragment.show(activity.getSupportFragmentManager(), "progressDialog"); + } + + @Override + public void handleMessage(Message message) { + Bundle data = message.getData(); + + switch (message.arg1) { + case MESSAGE_OKAY: + mProgressDialogFragment.dismiss(); + + break; + + case MESSAGE_EXCEPTION: + mProgressDialogFragment.dismiss(); + + // show error from service + if (data.containsKey(DATA_ERROR)) { + Toast.makeText(mActivity, + mActivity.getString(R.string.errorMessage, data.getString(DATA_ERROR)), + Toast.LENGTH_SHORT).show(); + } + + break; + + case MESSAGE_UPDATE_PROGRESS: + if (data.containsKey(DATA_PROGRESS) && data.containsKey(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)); + } 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)); + } + } + + break; + + default: + break; + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainKeyService.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainKeyService.java new file mode 100644 index 000000000..c0be8040f --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainKeyService.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service; + +import java.util.ArrayList; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.service.IKeychainKeyService; +import org.sufficientlysecure.keychain.service.handler.IKeychainGetKeyringsHandler; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; + +public class KeychainKeyService extends Service { + Context mContext; + + @Override + public void onCreate() { + super.onCreate(); + mContext = this; + Log.d(Constants.TAG, "ApgKeyService, onCreate()"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(Constants.TAG, "ApgKeyService, onDestroy()"); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + /** + * Synchronized implementation of getPublicKeyRings + */ + private synchronized void getPublicKeyRingsSafe(long[] masterKeyIds, + boolean asAsciiArmoredStringArray, IKeychainGetKeyringsHandler handler) + throws RemoteException { + if (asAsciiArmoredStringArray) { + ArrayList<String> output = ProviderHelper.getPublicKeyRingsAsArmoredString(mContext, + masterKeyIds); + + handler.onSuccess(null, output); + } else { + byte[] outputBytes = ProviderHelper + .getPublicKeyRingsAsByteArray(mContext, masterKeyIds); + handler.onSuccess(outputBytes, null); + } + } + + /** + * Synchronized implementation of getSecretKeyRings + */ + private synchronized void getSecretKeyRingsSafe(long[] masterKeyIds, + boolean asAsciiArmoredStringArray, IKeychainGetKeyringsHandler handler) + throws RemoteException { + if (asAsciiArmoredStringArray) { + ArrayList<String> output = ProviderHelper.getSecretKeyRingsAsArmoredString(mContext, + masterKeyIds); + + handler.onSuccess(null, output); + } else { + byte[] outputBytes = ProviderHelper + .getSecretKeyRingsAsByteArray(mContext, masterKeyIds); + handler.onSuccess(outputBytes, null); + } + + } + + /** + * This is the implementation of the interface IApgKeyService. All methods are oneway, meaning + * asynchronous and return to the client using handlers. + * + * The real PGP code is located in PGPMain. + */ + private final IKeychainKeyService.Stub mBinder = new IKeychainKeyService.Stub() { + + @Override + public void getPublicKeyRings(long[] masterKeyIds, boolean asAsciiArmoredStringArray, + IKeychainGetKeyringsHandler handler) throws RemoteException { + getPublicKeyRingsSafe(masterKeyIds, asAsciiArmoredStringArray, handler); + } + + @Override + public void getSecretKeyRings(long[] masterKeyIds, boolean asAsciiArmoredStringArray, + IKeychainGetKeyringsHandler handler) throws RemoteException { + getSecretKeyRingsSafe(masterKeyIds, asAsciiArmoredStringArray, handler); + } + + }; + + /** + * As we can not throw an exception through Android RPC, we assign identifiers to the exception + * types. + * + * @param e + * @return + */ + // private int getExceptionId(Exception e) { + // if (e instanceof NoSuchProviderException) { + // return 0; + // } else if (e instanceof NoSuchAlgorithmException) { + // return 1; + // } else if (e instanceof SignatureException) { + // return 2; + // } else if (e instanceof IOException) { + // return 3; + // } else if (e instanceof ApgGeneralException) { + // return 4; + // } else if (e instanceof PGPException) { + // return 5; + // } else { + // return -1; + // } + // } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/PassphraseCacheService.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/PassphraseCacheService.java new file mode 100644 index 000000000..eb1232769 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/PassphraseCacheService.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service; + +import java.util.Date; +import java.util.HashMap; + +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.provider.ProviderHelper; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; + +public class PassphraseCacheService extends Service { + public static final String TAG = Constants.TAG + ": PassphraseCacheService"; + + public static final String BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE = Constants.INTENT_PREFIX + + "PASSPHRASE_CACHE_SERVICE"; + + public static final String EXTRA_TTL = "ttl"; + public static final String EXTRA_KEY_ID = "keyId"; + public static final String EXTRA_PASSPHRASE = "passphrase"; + + private static final int REQUEST_ID = 0; + private static final long DEFAULT_TTL = 15; + + private BroadcastReceiver mIntentReceiver; + + // This is static to be easily retrieved by getCachedPassphrase() without the need of callback + // functions + private static HashMap<Long, String> mPassphraseCache = new HashMap<Long, String>(); + + /** + * This caches a new passphrase by sending a new command to the service. An android service is + * only run once. Thus, when the service is already started, new commands just add new events to + * the alarm manager for new passphrases to let them timeout in the future. + * + * @param context + * @param keyId + * @param passphrase + */ + public static void addCachedPassphrase(Context context, long keyId, String passphrase) { + Log.d(TAG, "cacheNewPassphrase() for " + keyId); + + Intent intent = new Intent(context, PassphraseCacheService.class); + intent.putExtra(EXTRA_TTL, Preferences.getPreferences(context).getPassPhraseCacheTtl()); + intent.putExtra(EXTRA_PASSPHRASE, passphrase); + intent.putExtra(EXTRA_KEY_ID, keyId); + + context.startService(intent); + } + + /** + * Gets a cached passphrase from memory + * + * @param context + * @param keyId + * @return + */ + public static String getCachedPassphrase(Context context, long keyId) { + // try to get master key id which is used as an identifier for cached passphrases + long masterKeyId = keyId; + if (masterKeyId != Id.key.symmetric) { + PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(context, keyId); + if (keyRing == null) { + return null; + } + PGPSecretKey masterKey = PgpHelper.getMasterKey(keyRing); + if (masterKey == null) { + return null; + } + masterKeyId = masterKey.getKeyID(); + } + + // get cached passphrase + String cachedPassphrase = mPassphraseCache.get(masterKeyId); + if (cachedPassphrase == null) { + return null; + } + // set it again to reset the cache life cycle + Log.d(TAG, "Cache passphrase again when getting it!"); + addCachedPassphrase(context, masterKeyId, cachedPassphrase); + + return cachedPassphrase; + } + + /** + * Register BroadcastReceiver that is unregistered when service is destroyed. This + * BroadcastReceiver hears on intents with ACTION_PASSPHRASE_CACHE_SERVICE to then timeout + * specific passphrases in memory. + */ + private void registerReceiver() { + if (mIntentReceiver == null) { + mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + Log.d(TAG, "Received broadcast..."); + + if (action.equals(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE)) { + long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1); + timeout(context, keyId); + } + } + }; + + IntentFilter filter = new IntentFilter(); + filter.addAction(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE); + registerReceiver(mIntentReceiver, filter); + } + } + + /** + * Build pending intent that is executed by alarm manager to time out a specific passphrase + * + * @param context + * @param keyId + * @return + */ + private static PendingIntent buildIntent(Context context, long keyId) { + Intent intent = new Intent(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE); + intent.putExtra(EXTRA_KEY_ID, keyId); + PendingIntent sender = PendingIntent.getBroadcast(context, REQUEST_ID, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + return sender; + } + + @Override + public void onCreate() { + Log.d(TAG, "onCreate()"); + } + + /** + * Executed when service is started by intent + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "onStartCommand()"); + + // register broadcastreceiver + registerReceiver(); + + if (intent != null) { + long ttl = intent.getLongExtra(EXTRA_TTL, DEFAULT_TTL); + long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1); + String passphrase = intent.getStringExtra(EXTRA_PASSPHRASE); + + Log.d(TAG, "Received intent in onStartCommand() with keyId: " + keyId + ", ttl: " + ttl); + + // add keyId and passphrase to memory + mPassphraseCache.put(keyId, passphrase); + + // register new alarm with keyId for this passphrase + long triggerTime = new Date().getTime() + (ttl * 1000); + AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); + am.set(AlarmManager.RTC_WAKEUP, triggerTime, buildIntent(this, keyId)); + } + + return START_STICKY; + } + + /** + * Called when one specific passphrase for keyId timed out + * + * @param context + * @param keyId + */ + private void timeout(Context context, long keyId) { + // remove passphrase corresponding to keyId from memory + mPassphraseCache.remove(keyId); + + Log.d(TAG, "Timeout of " + keyId + ", removed from memory!"); + + // stop whole service if no cached passphrases remaining + if (mPassphraseCache.isEmpty()) { + Log.d(TAG, "No passphrases remaining in memory, stopping service!"); + stopSelf(); + } + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy()"); + + unregisterReceiver(mIntentReceiver); + } + + public class PassphraseCacheBinder extends Binder { + public PassphraseCacheService getService() { + return PassphraseCacheService.this; + } + } + + private final IBinder mBinder = new PassphraseCacheBinder(); + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainDecryptHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainDecryptHandler.aidl new file mode 100644 index 000000000..31ead701d --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainDecryptHandler.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service.handler; + +interface IKeychainDecryptHandler { + + oneway void onSuccess(in byte[] outputBytes, in String outputUri, in boolean signature, + in long signatureKeyId, in String signatureUserId, in boolean signatureSuccess, + in boolean signatureUnknown); + + + oneway void onException(in int exceptionNumber, in String message); +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainEncryptHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainEncryptHandler.aidl new file mode 100644 index 000000000..5b21a0613 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainEncryptHandler.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service.handler; + +interface IKeychainEncryptHandler { + /** + * Either output or streamUri is given. One of them is null + * + */ + oneway void onSuccess(in byte[] outputBytes, in String outputUri); + + + oneway void onException(in int exceptionNumber, in String message); +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetDecryptionKeyIdHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetDecryptionKeyIdHandler.aidl new file mode 100644 index 000000000..2fff74a92 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetDecryptionKeyIdHandler.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service.handler; + +interface IKeychainGetDecryptionKeyIdHandler { + + oneway void onSuccess(in long secretKeyId, in boolean symmetric); + + + oneway void onException(in int exceptionNumber, in String message); +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetKeyringsHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetKeyringsHandler.aidl new file mode 100644 index 000000000..c3a7d1faf --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetKeyringsHandler.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service.handler; + +interface IKeychainGetKeyringsHandler { + /** + * Either outputBytes or outputString is given. One of them is null + * + */ + oneway void onSuccess(in byte[] outputBytes, in List<String> outputString); + + + oneway void onException(in int exceptionNumber, in String message); +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainSignHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainSignHandler.aidl new file mode 100644 index 000000000..69badab4c --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainSignHandler.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service.handler; + +interface IKeychainSignHandler { + /** + * Either output or streamUri is given. One of them is null + * + */ + oneway void onSuccess(in byte[] outputBytes, in String outputUri); + + + oneway void onException(in int exceptionNumber, in String message); +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainVerifyHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainVerifyHandler.aidl new file mode 100644 index 000000000..aaa9a7f6a --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainVerifyHandler.aidl @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.service.handler; + +interface IKeychainVerifyHandler { + + oneway void onSuccess(in boolean signature, in long signatureKeyId, + in String signatureUserId, in boolean signatureSuccess, in boolean signatureUnknown); + + + oneway void onException(in int exceptionNumber, in String message); +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java new file mode 100644 index 000000000..87b1204a4 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java @@ -0,0 +1,946 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.helper.PgpMain; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.LookupUnknownKeyDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.animation.AnimationUtils; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ViewFlipper; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.regex.Matcher; + +public class DecryptActivity extends SherlockFragmentActivity { + + /* Intents */ + // without permission + public static final String ACTION_DECRYPT = Constants.INTENT_PREFIX + "DECRYPT"; + public static final String ACTION_DECRYPT_FILE = Constants.INTENT_PREFIX + "DECRYPT_FILE"; + + // with permission + public static final String ACTION_DECRYPT_AND_RETURN = Constants.INTENT_PREFIX + + "DECRYPT_AND_RETURN"; + public static final String ACTION_DECRYPT_STREAM_AND_RETURN = Constants.INTENT_PREFIX + + "DECRYPT_STREAM_AND_RETURN"; + + /* EXTRA keys for input */ + public static final String EXTRA_TEXT = "text"; + public static final String EXTRA_DATA = "data"; + public static final String EXTRA_REPLY_TO = "replyTo"; + public static final String EXTRA_SUBJECT = "subject"; + public static final String EXTRA_BINARY = "binary"; + + private long mSignatureKeyId = 0; + + private boolean mReturnResult = false; + private String mReplyTo = null; + private String mSubject = null; + private boolean mSignedOnly = false; + private boolean mAssumeSymmetricEncryption = false; + + private EditText mMessage = null; + private LinearLayout mSignatureLayout = null; + private ImageView mSignatureStatusImage = null; + private TextView mUserId = null; + private TextView mUserIdRest = null; + + private ViewFlipper mSource = null; + private TextView mSourceLabel = null; + private ImageView mSourcePrevious = null; + private ImageView mSourceNext = null; + + private boolean mDecryptEnabled = true; + private String mDecryptString = ""; + private boolean mReplyEnabled = true; + private String mReplyString = ""; + + private int mDecryptTarget; + + private EditText mFilename = null; + private CheckBox mDeleteAfter = null; + private ImageButton mBrowse = null; + + private String mInputFilename = null; + private String mOutputFilename = null; + + private Uri mContentUri = null; + private byte[] mDataBytes = null; + private boolean mReturnBinary = false; + + private long mUnknownSignatureKeyId = 0; + + private long mSecretKeyId = Id.key.none; + + private FileDialogFragment mFileDialog; + + private boolean mLookupUnknownKey = true; + + private boolean mDecryptImmediately = false; + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + + if (mDecryptEnabled) { + menu.add(1, Id.menu.option.decrypt, 0, mDecryptString).setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + if (mReplyEnabled) { + menu.add(1, Id.menu.option.reply, 1, mReplyString).setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + case Id.menu.option.decrypt: { + decryptClicked(); + + return true; + } + case Id.menu.option.reply: { + replyClicked(); + + return true; + } + + default: { + return super.onOptionsItemSelected(item); + } + } + } + + private void initView() { + mSource = (ViewFlipper) findViewById(R.id.source); + mSourceLabel = (TextView) findViewById(R.id.sourceLabel); + mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious); + mSourceNext = (ImageView) findViewById(R.id.sourceNext); + + mSourcePrevious.setClickable(true); + mSourcePrevious.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this, + R.anim.push_right_in)); + mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this, + R.anim.push_right_out)); + mSource.showPrevious(); + updateSource(); + } + }); + + mSourceNext.setClickable(true); + OnClickListener nextSourceClickListener = new OnClickListener() { + public void onClick(View v) { + mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this, + R.anim.push_left_in)); + mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this, + R.anim.push_left_out)); + mSource.showNext(); + updateSource(); + } + }; + mSourceNext.setOnClickListener(nextSourceClickListener); + + mSourceLabel.setClickable(true); + mSourceLabel.setOnClickListener(nextSourceClickListener); + + mMessage = (EditText) findViewById(R.id.message); + mSignatureLayout = (LinearLayout) findViewById(R.id.signature); + mSignatureStatusImage = (ImageView) findViewById(R.id.ic_signature_status); + mUserId = (TextView) findViewById(R.id.mainUserId); + mUserIdRest = (TextView) findViewById(R.id.mainUserIdRest); + + // measure the height of the source_file view and set the message view's min height to that, + // so it fills mSource fully... bit of a hack. + View tmp = findViewById(R.id.sourceFile); + tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + int height = tmp.getMeasuredHeight(); + mMessage.setMinimumHeight(height); + + mFilename = (EditText) findViewById(R.id.filename); + mBrowse = (ImageButton) findViewById(R.id.btn_browse); + mBrowse.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + FileHelper.openFile(DecryptActivity.this, mFilename.getText().toString(), "*/*", + Id.request.filename); + } + }); + + mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterDecryption); + + // default: message source + mSource.setInAnimation(null); + mSource.setOutAnimation(null); + while (mSource.getCurrentView().getId() != R.id.sourceMessage) { + mSource.showNext(); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // check permissions for intent actions without user interaction + String[] restrictedActions = new String[] { ACTION_DECRYPT_AND_RETURN }; + OtherHelper.checkPackagePermissionForActions(this, this.getCallingPackage(), + Constants.PERMISSION_ACCESS_API, getIntent().getAction(), restrictedActions); + + setContentView(R.layout.decrypt); + + // set actionbar without home button if called from another app + OtherHelper.setActionBarBackButton(this); + + initView(); + + // Handle intent actions + handleActions(getIntent()); + + if (mSource.getCurrentView().getId() == R.id.sourceMessage + && mMessage.getText().length() == 0) { + + CharSequence clipboardText = ClipboardReflection.getClipboardText(this); + + String data = ""; + if (clipboardText != null) { + Matcher matcher = PgpMain.PGP_MESSAGE.matcher(clipboardText); + if (!matcher.matches()) { + matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(clipboardText); + } + if (matcher.matches()) { + data = matcher.group(1); + mMessage.setText(data); + Toast.makeText(this, R.string.usingClipboardContent, Toast.LENGTH_SHORT).show(); + } + } + } + + mSignatureLayout.setVisibility(View.GONE); + mSignatureLayout.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mSignatureKeyId == 0) { + return; + } + PGPPublicKeyRing key = ProviderHelper.getPGPPublicKeyRingByKeyId( + DecryptActivity.this, mSignatureKeyId); + if (key != null) { + Intent intent = new Intent(DecryptActivity.this, KeyServerQueryActivity.class); + intent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID); + intent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, mSignatureKeyId); + startActivity(intent); + } + } + }); + + mReplyEnabled = false; + + // build new actionbar + invalidateOptionsMenu(); + + if (mReturnResult) { + mSourcePrevious.setClickable(false); + mSourcePrevious.setEnabled(false); + mSourcePrevious.setVisibility(View.INVISIBLE); + + mSourceNext.setClickable(false); + mSourceNext.setEnabled(false); + mSourceNext.setVisibility(View.INVISIBLE); + + mSourceLabel.setClickable(false); + mSourceLabel.setEnabled(false); + } + + updateSource(); + + if (mDecryptImmediately + || (mSource.getCurrentView().getId() == R.id.sourceMessage && (mMessage.getText() + .length() > 0 || mDataBytes != null || mContentUri != null))) { + decryptClicked(); + } + } + + /** + * Handles all actions with this intent + * + * @param intent + */ + private void handleActions(Intent intent) { + String action = intent.getAction(); + Bundle extras = intent.getExtras(); + String type = intent.getType(); + Uri uri = intent.getData(); + + if (extras == null) { + extras = new Bundle(); + } + + /* + * Android's Action + */ + if (Intent.ACTION_SEND.equals(action) && type != null) { + // When sending to APG Encrypt via share menu + if ("text/plain".equals(type)) { + // Plain text + String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); + if (sharedText != null) { + // handle like normal text decryption, override action and extras to later + // execute ACTION_DECRYPT in main actions + extras.putString(EXTRA_TEXT, sharedText); + action = ACTION_DECRYPT; + } + } else { + // Binary via content provider (could also be files) + // override uri to get stream from send + uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); + action = ACTION_DECRYPT_FILE; + } + } else if (Intent.ACTION_VIEW.equals(action)) { + // Android's Action when opening file associated to APG (see AndroidManifest.xml) + + // override action + action = ACTION_DECRYPT_FILE; + + // EVERYTHING ELSE IS OLD CODE + // This gets the Uri, where an inputStream can be opened from + // mContentUri = intent.getData(); + + // TODO: old implementation of ACTION_VIEW. Is this used in K9? + // Uri uri = mIntent.getData(); + // try { + // InputStream attachment = getContentResolver().openInputStream(uri); + // ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + // byte bytes[] = new byte[1 << 16]; + // int length; + // while ((length = attachment.read(bytes)) > 0) { + // byteOut.write(bytes, 0, length); + // } + // byteOut.close(); + // String data = new String(byteOut.toByteArray()); + // mMessage.setText(data); + // } catch (FileNotFoundException e) { + // // ignore, then + // } catch (IOException e) { + // // ignore, then + // } + + // same as ACTION_DECRYPT_FILE but decrypt it immediately + // handleActionDecryptFile(intent); + // mDecryptImmediately = true; + } + + /** + * Main Actions + */ + if (ACTION_DECRYPT.equals(action)) { + mDataBytes = extras.getByteArray(EXTRA_DATA); + String textData = null; + if (mDataBytes == null) { + Log.d(Constants.TAG, "EXTRA_DATA was null"); + textData = extras.getString(EXTRA_TEXT); + } else { + Log.d(Constants.TAG, "Got data from EXTRA_DATA"); + } + if (textData != null) { + Log.d(Constants.TAG, "textData null, matching text ..."); + Matcher matcher = PgpMain.PGP_MESSAGE.matcher(textData); + if (matcher.matches()) { + Log.d(Constants.TAG, "PGP_MESSAGE matched"); + textData = matcher.group(1); + // replace non breakable spaces + textData = textData.replaceAll("\\xa0", " "); + mMessage.setText(textData); + } else { + matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(textData); + if (matcher.matches()) { + Log.d(Constants.TAG, "PGP_SIGNED_MESSAGE matched"); + textData = matcher.group(1); + // replace non breakable spaces + textData = textData.replaceAll("\\xa0", " "); + mMessage.setText(textData); + + mDecryptString = getString(R.string.btn_verify); + // build new action bar + invalidateOptionsMenu(); + } else { + Log.d(Constants.TAG, "Nothing matched!"); + } + } + } + mReplyTo = extras.getString(EXTRA_REPLY_TO); + mSubject = extras.getString(EXTRA_SUBJECT); + } else if (ACTION_DECRYPT_FILE.equals(action)) { + // get file path from uri + String path = FileHelper.getPath(this, uri); + + if (path != null) { + mInputFilename = path; + mFilename.setText(mInputFilename); + guessOutputFilename(); + mSource.setInAnimation(null); + mSource.setOutAnimation(null); + while (mSource.getCurrentView().getId() != R.id.sourceFile) { + mSource.showNext(); + } + } else { + Log.e(Constants.TAG, + "Direct binary data without actual file in filesystem is not supported. This is only supported by ACTION_DECRYPT_STREAM_AND_RETURN."); + Toast.makeText(this, R.string.error_onlyFilesAreSupported, Toast.LENGTH_LONG) + .show(); + // end activity + finish(); + } + } else if (ACTION_DECRYPT_AND_RETURN.equals(action)) { + mReturnBinary = extras.getBoolean(EXTRA_BINARY, false); + + if (mContentUri == null) { + mDataBytes = extras.getByteArray(EXTRA_DATA); + String data = extras.getString(EXTRA_TEXT); + if (data != null) { + Matcher matcher = PgpMain.PGP_MESSAGE.matcher(data); + if (matcher.matches()) { + data = matcher.group(1); + // replace non breakable spaces + data = data.replaceAll("\\xa0", " "); + mMessage.setText(data); + } else { + matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(data); + if (matcher.matches()) { + data = matcher.group(1); + // replace non breakable spaces + data = data.replaceAll("\\xa0", " "); + mMessage.setText(data); + mDecryptString = getString(R.string.btn_verify); + + // build new action bar + invalidateOptionsMenu(); + } + } + } + } + mReturnResult = true; + } else if (ACTION_DECRYPT_STREAM_AND_RETURN.equals(action)) { + // TODO: Implement decrypt stream + } + } + + private void guessOutputFilename() { + mInputFilename = mFilename.getText().toString(); + File file = new File(mInputFilename); + String filename = file.getName(); + if (filename.endsWith(".asc") || filename.endsWith(".gpg") || filename.endsWith(".pgp")) { + filename = filename.substring(0, filename.length() - 4); + } + mOutputFilename = Constants.path.APP_DIR + "/" + filename; + } + + private void updateSource() { + switch (mSource.getCurrentView().getId()) { + case R.id.sourceFile: { + mSourceLabel.setText(R.string.label_file); + mDecryptString = getString(R.string.btn_decrypt); + + // build new action bar + invalidateOptionsMenu(); + break; + } + + case R.id.sourceMessage: { + mSourceLabel.setText(R.string.label_message); + mDecryptString = getString(R.string.btn_decrypt); + + // build new action bar + invalidateOptionsMenu(); + break; + } + + default: { + break; + } + } + } + + private void decryptClicked() { + if (mSource.getCurrentView().getId() == R.id.sourceFile) { + mDecryptTarget = Id.target.file; + } else { + mDecryptTarget = Id.target.message; + } + initiateDecryption(); + } + + private void initiateDecryption() { + if (mDecryptTarget == Id.target.file) { + String currentFilename = mFilename.getText().toString(); + if (mInputFilename == null || !mInputFilename.equals(currentFilename)) { + guessOutputFilename(); + } + + if (mInputFilename.equals("")) { + Toast.makeText(this, R.string.noFileSelected, Toast.LENGTH_SHORT).show(); + return; + } + + if (mInputFilename.startsWith("file")) { + File file = new File(mInputFilename); + if (!file.exists() || !file.isFile()) { + Toast.makeText( + this, + getString(R.string.errorMessage, getString(R.string.error_fileNotFound)), + Toast.LENGTH_SHORT).show(); + return; + } + } + } + + if (mDecryptTarget == Id.target.message) { + String messageData = mMessage.getText().toString(); + Matcher matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(messageData); + if (matcher.matches()) { + mSignedOnly = true; + decryptStart(); + return; + } + } + + // else treat it as an decrypted message/file + mSignedOnly = false; + + getDecryptionKeyFromInputStream(); + + // if we need a symmetric passphrase or a passphrase to use a secret key ask for it + if (mSecretKeyId == Id.key.symmetric + || PassphraseCacheService.getCachedPassphrase(this, mSecretKeyId) == null) { + showPassphraseDialog(); + } else { + if (mDecryptTarget == Id.target.file) { + askForOutputFilename(); + } else { + decryptStart(); + } + } + } + + /** + * 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 + */ + private void showPassphraseDialog() { + // Message is received after passphrase is cached + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + if (mDecryptTarget == Id.target.file) { + askForOutputFilename(); + } else { + decryptStart(); + } + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + try { + PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this, + messenger, mSecretKeyId); + + passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog"); + } catch (PgpMain.PgpGeneralException e) { + Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!"); + // send message to handler to start encryption directly + returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY); + } + } + + /** + * TODO: Rework function, remove global variables + */ + private void getDecryptionKeyFromInputStream() { + InputStream inStream = null; + if (mContentUri != null) { + try { + inStream = getContentResolver().openInputStream(mContentUri); + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "File not found!", e); + Toast.makeText(this, getString(R.string.error_fileNotFound, e.getMessage()), + Toast.LENGTH_SHORT).show(); + } + } else if (mDecryptTarget == Id.target.file) { + // check if storage is ready + if (!FileHelper.isStorageMounted(mInputFilename)) { + Toast.makeText(this, getString(R.string.error_externalStorageNotReady), + Toast.LENGTH_SHORT).show(); + return; + } + + try { + inStream = new FileInputStream(mInputFilename); + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "File not found!", e); + Toast.makeText(this, getString(R.string.error_fileNotFound, e.getMessage()), + Toast.LENGTH_SHORT).show(); + } + } else { + if (mDataBytes != null) { + inStream = new ByteArrayInputStream(mDataBytes); + } else { + inStream = new ByteArrayInputStream(mMessage.getText().toString().getBytes()); + } + } + + // get decryption key for this inStream + try { + try { + mSecretKeyId = PgpMain.getDecryptionKeyId(this, inStream); + if (mSecretKeyId == Id.key.none) { + throw new PgpMain.PgpGeneralException( + getString(R.string.error_noSecretKeyFound)); + } + mAssumeSymmetricEncryption = false; + } catch (PgpMain.NoAsymmetricEncryptionException e) { + mSecretKeyId = Id.key.symmetric; + if (!PgpMain.hasSymmetricEncryption(this, inStream)) { + throw new PgpMain.PgpGeneralException( + getString(R.string.error_noKnownEncryptionFound)); + } + mAssumeSymmetricEncryption = true; + } + } catch (Exception e) { + Toast.makeText(this, getString(R.string.errorMessage, e.getMessage()), + Toast.LENGTH_SHORT).show(); + } + } + + private void replyClicked() { + Intent intent = new Intent(this, EncryptActivity.class); + intent.setAction(EncryptActivity.ACTION_ENCRYPT); + String data = mMessage.getText().toString(); + data = data.replaceAll("(?m)^", "> "); + data = "\n\n" + data; + intent.putExtra(EncryptActivity.EXTRA_TEXT, data); + intent.putExtra(EncryptActivity.EXTRA_SUBJECT, "Re: " + mSubject); + intent.putExtra(EncryptActivity.EXTRA_SEND_TO, mReplyTo); + intent.putExtra(EncryptActivity.EXTRA_SIGNATURE_KEY_ID, mSecretKeyId); + intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, new long[] { mSignatureKeyId }); + startActivity(intent); + } + + private void askForOutputFilename() { + // Message is received after passphrase is cached + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == FileDialogFragment.MESSAGE_OKAY) { + Bundle data = message.getData(); + mOutputFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME); + decryptStart(); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + mFileDialog = FileDialogFragment.newInstance(messenger, + getString(R.string.title_decryptToFile), + getString(R.string.specifyFileToDecryptTo), mOutputFilename, null, + Id.request.output_filename); + + mFileDialog.show(getSupportFragmentManager(), "fileDialog"); + } + + private void lookupUnknownKey(long unknownKeyId) { + // Message is received after passphrase is cached + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_OKAY) { + // the result is handled by onActivityResult() as LookupUnknownKeyDialogFragment + // starts a new Intent which then returns data + } else if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_CANCEL) { + // decrypt again, but don't lookup unknown keys! + mLookupUnknownKey = false; + decryptStart(); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + LookupUnknownKeyDialogFragment lookupKeyDialog = LookupUnknownKeyDialogFragment + .newInstance(messenger, unknownKeyId); + + lookupKeyDialog.show(getSupportFragmentManager(), "unknownKeyDialog"); + } + + private void decryptStart() { + Log.d(Constants.TAG, "decryptStart"); + + // Send all information needed to service to decrypt in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + // fill values for this action + Bundle data = new Bundle(); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_DECRYPT_VERIFY); + + // choose action based on input: decrypt stream, file or bytes + if (mContentUri != null) { + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_STREAM); + + data.putParcelable(KeychainIntentService.ENCRYPT_PROVIDER_URI, mContentUri); + } else if (mDecryptTarget == Id.target.file) { + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE); + + Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename=" + + mOutputFilename); + + data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename); + data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename); + } else { + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES); + + if (mDataBytes != null) { + data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, mDataBytes); + } else { + String message = mMessage.getText().toString(); + data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, message.getBytes()); + } + } + + data.putLong(KeychainIntentService.ENCRYPT_SECRET_KEY_ID, mSecretKeyId); + + data.putBoolean(KeychainIntentService.DECRYPT_SIGNED_ONLY, mSignedOnly); + data.putBoolean(KeychainIntentService.DECRYPT_LOOKUP_UNKNOWN_KEY, mLookupUnknownKey); + data.putBoolean(KeychainIntentService.DECRYPT_RETURN_BYTES, mReturnBinary); + data.putBoolean(KeychainIntentService.DECRYPT_ASSUME_SYMMETRIC, mAssumeSymmetricEncryption); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after encrypting is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + R.string.progress_decrypting, ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + // get returned data bundle + Bundle returnData = message.getData(); + + // if key is unknown show lookup dialog + if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_LOOKUP_KEY) + && mLookupUnknownKey) { + mUnknownSignatureKeyId = returnData + .getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID); + lookupUnknownKey(mUnknownSignatureKeyId); + return; + } + + mSignatureKeyId = 0; + mSignatureLayout.setVisibility(View.GONE); + mReplyEnabled = false; + + // build new action bar + invalidateOptionsMenu(); + + Toast.makeText(DecryptActivity.this, R.string.decryptionSuccessful, + Toast.LENGTH_SHORT).show(); + if (mReturnResult) { + Intent intent = new Intent(); + intent.putExtras(returnData); + setResult(RESULT_OK, intent); + finish(); + return; + } + + switch (mDecryptTarget) { + case Id.target.message: + String decryptedMessage = returnData + .getString(KeychainIntentService.RESULT_DECRYPTED_STRING); + mMessage.setText(decryptedMessage); + mMessage.setHorizontallyScrolling(false); + mReplyEnabled = false; + + // build new action bar + invalidateOptionsMenu(); + break; + + case Id.target.file: + if (mDeleteAfter.isChecked()) { + // Create and show dialog to delete original file + DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment + .newInstance(mInputFilename); + deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog"); + } + break; + + default: + // shouldn't happen + break; + + } + + if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE)) { + String userId = returnData + .getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID); + mSignatureKeyId = returnData + .getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID); + mUserIdRest + .setText("id: " + PgpHelper.getSmallFingerPrint(mSignatureKeyId)); + if (userId == null) { + userId = getResources().getString(R.string.unknownUserId); + } + String chunks[] = userId.split(" <", 2); + userId = chunks[0]; + if (chunks.length > 1) { + mUserIdRest.setText("<" + chunks[1]); + } + mUserId.setText(userId); + + if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS)) { + mSignatureStatusImage.setImageResource(R.drawable.overlay_ok); + } else if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN)) { + mSignatureStatusImage.setImageResource(R.drawable.overlay_error); + Toast.makeText(DecryptActivity.this, + R.string.unknownSignatureKeyTouchToLookUp, Toast.LENGTH_LONG) + .show(); + } else { + mSignatureStatusImage.setImageResource(R.drawable.overlay_error); + } + mSignatureLayout.setVisibility(View.VISIBLE); + } + } + }; + }; + + // 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); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case Id.request.filename: { + if (resultCode == RESULT_OK && data != null) { + try { + String path = FileHelper.getPath(this, data.getData()); + Log.d(Constants.TAG, "path=" + path); + + mFilename.setText(path); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Nullpointer while retrieving path!"); + } + } + return; + } + + case Id.request.output_filename: { + if (resultCode == RESULT_OK && data != null) { + try { + String path = data.getData().getPath(); + Log.d(Constants.TAG, "path=" + path); + + mFileDialog.setFilename(path); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Nullpointer while retrieving path!"); + } + } + return; + } + + // this request is returned after LookupUnknownKeyDialogFragment started + // KeyServerQueryActivity and user looked uo key + case Id.request.look_up_key_id: { + Log.d(Constants.TAG, "Returning from Lookup Key..."); + // decrypt again without lookup + mLookupUnknownKey = false; + decryptStart(); + return; + } + + default: { + break; + } + } + + super.onActivityResult(requestCode, resultCode, data); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java new file mode 100644 index 000000000..73a1234c4 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java @@ -0,0 +1,593 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.helper.PgpConversionHelper; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.helper.PgpMain; +import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; +import org.sufficientlysecure.keychain.ui.widget.KeyEditor; +import org.sufficientlysecure.keychain.ui.widget.SectionView; +import org.sufficientlysecure.keychain.ui.widget.UserIdEditor; +import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +import android.app.ProgressDialog; +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.ViewGroup; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.Toast; +import android.widget.CompoundButton.OnCheckedChangeListener; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Vector; + +public class EditKeyActivity extends SherlockFragmentActivity { + + // possible intent actions for this activity + public static final String ACTION_CREATE_KEY = Constants.INTENT_PREFIX + "CREATE_KEY"; + public static final String ACTION_EDIT_KEY = Constants.INTENT_PREFIX + "EDIT_KEY"; + + // possible extra keys + public static final String EXTRA_USER_IDS = "userIds"; + public static final String EXTRA_NO_PASSPHRASE = "noPassphrase"; + public static final String EXTRA_GENERATE_DEFAULT_KEYS = "generateDefaultKeys"; + public static final String EXTRA_KEY_ID = "keyId"; + + // results when saving key + public static final String RESULT_EXTRA_MASTER_KEY_ID = "masterKeyId"; + public static final String RESULT_EXTRA_USER_ID = "userId"; + + private ActionBar mActionBar; + + private PGPSecretKeyRing mKeyRing = null; + + private SectionView mUserIdsView; + private SectionView mKeysView; + + private String mCurrentPassPhrase = null; + private String mNewPassPhrase = null; + + private Button mChangePassPhrase; + + private CheckBox mNoPassphrase; + + Vector<String> mUserIds; + Vector<PGPSecretKey> mKeys; + Vector<Integer> mKeysUsages; + + // will be set to false to build layout later in handler + private boolean mBuildLayout = true; + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(1, Id.menu.option.cancel, 0, R.string.btn_doNotSave).setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + menu.add(1, Id.menu.option.save, 1, R.string.btn_save).setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, KeyListSecretActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + case Id.menu.option.save: + saveClicked(); + return true; + + case Id.menu.option.cancel: + cancelClicked(); + return true; + + default: + break; + + } + return false; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // check permissions for intent actions without user interaction + String[] restrictedActions = new String[] { ACTION_CREATE_KEY }; + OtherHelper.checkPackagePermissionForActions(this, this.getCallingPackage(), + Constants.PERMISSION_ACCESS_API, getIntent().getAction(), restrictedActions); + + setContentView(R.layout.edit_key); + + mActionBar = getSupportActionBar(); + mActionBar.setDisplayShowTitleEnabled(true); + + // set actionbar without home button if called from another app + OtherHelper.setActionBarBackButton(this); + + // find views + mChangePassPhrase = (Button) findViewById(R.id.edit_key_btn_change_pass_phrase); + mNoPassphrase = (CheckBox) findViewById(R.id.edit_key_no_passphrase); + + mUserIds = new Vector<String>(); + mKeys = new Vector<PGPSecretKey>(); + mKeysUsages = new Vector<Integer>(); + + // Catch Intents opened from other apps + Intent intent = getIntent(); + String action = intent.getAction(); + if (ACTION_CREATE_KEY.equals(action)) { + handleActionCreateKey(intent); + } else if (ACTION_EDIT_KEY.equals(action)) { + handleActionEditKey(intent); + } + + mChangePassPhrase.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + showSetPassphraseDialog(); + } + }); + + // disable passphrase when no passphrase checkobox is checked! + mNoPassphrase.setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + // remove passphrase + mNewPassPhrase = null; + + mChangePassPhrase.setVisibility(View.GONE); + } else { + mChangePassPhrase.setVisibility(View.VISIBLE); + } + } + }); + + if (mBuildLayout) { + buildLayout(); + } + } + + /** + * Handle intent action to create new key + * + * @param intent + */ + private void handleActionCreateKey(Intent intent) { + Bundle extras = intent.getExtras(); + + mActionBar.setTitle(R.string.title_createKey); + + mCurrentPassPhrase = ""; + + if (extras != null) { + // if userId is given, prefill the fields + if (extras.containsKey(EXTRA_USER_IDS)) { + Log.d(Constants.TAG, "UserIds are given!"); + mUserIds.add(extras.getString(EXTRA_USER_IDS)); + } + + // if no passphrase is given + if (extras.containsKey(EXTRA_NO_PASSPHRASE)) { + boolean noPassphrase = extras.getBoolean(EXTRA_NO_PASSPHRASE); + if (noPassphrase) { + // check "no passphrase" checkbox and remove button + mNoPassphrase.setChecked(true); + mChangePassPhrase.setVisibility(View.GONE); + } + } + + // generate key + if (extras.containsKey(EXTRA_GENERATE_DEFAULT_KEYS)) { + boolean generateDefaultKeys = extras.getBoolean(EXTRA_GENERATE_DEFAULT_KEYS); + if (generateDefaultKeys) { + + // build layout in handler after generating keys not directly in onCreate + mBuildLayout = false; + + // Send all information needed to service generate keys in other thread + Intent serviceIntent = new Intent(this, KeychainIntentService.class); + serviceIntent.putExtra(KeychainIntentService.EXTRA_ACTION, + KeychainIntentService.ACTION_GENERATE_DEFAULT_RSA_KEYS); + + // fill values for this action + Bundle data = new Bundle(); + data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE, + mCurrentPassPhrase); + + serviceIntent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after generating is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + R.string.progress_generating, ProgressDialog.STYLE_SPINNER) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + // get new key from data bundle returned from service + Bundle data = message.getData(); + PGPSecretKeyRing masterKeyRing = (PGPSecretKeyRing) PgpConversionHelper + .BytesToPGPKeyRing(data + .getByteArray(KeychainIntentService.RESULT_NEW_KEY)); + PGPSecretKeyRing subKeyRing = (PGPSecretKeyRing) PgpConversionHelper + .BytesToPGPKeyRing(data + .getByteArray(KeychainIntentService.RESULT_NEW_KEY2)); + + // add master key + @SuppressWarnings("unchecked") + Iterator<PGPSecretKey> masterIt = masterKeyRing.getSecretKeys(); + mKeys.add(masterIt.next()); + mKeysUsages.add(Id.choice.usage.sign_only); + + // add sub key + @SuppressWarnings("unchecked") + Iterator<PGPSecretKey> subIt = subKeyRing.getSecretKeys(); + subIt.next(); // masterkey + mKeys.add(subIt.next()); + mKeysUsages.add(Id.choice.usage.encrypt_only); + + buildLayout(); + } + }; + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + serviceIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + saveHandler.showProgressDialog(this); + + // start service with intent + startService(serviceIntent); + } + } + } + } + + /** + * Handle intent action to edit existing key + * + * @param intent + */ + @SuppressWarnings("unchecked") + private void handleActionEditKey(Intent intent) { + Bundle extras = intent.getExtras(); + + mActionBar.setTitle(R.string.title_editKey); + + mCurrentPassPhrase = PgpMain.getEditPassPhrase(); + if (mCurrentPassPhrase == null) { + mCurrentPassPhrase = ""; + } + + if (mCurrentPassPhrase.equals("")) { + // check "no passphrase" checkbox and remove button + mNoPassphrase.setChecked(true); + mChangePassPhrase.setVisibility(View.GONE); + } + + if (extras != null) { + if (extras.containsKey(EXTRA_KEY_ID)) { + long keyId = extras.getLong(EXTRA_KEY_ID); + + if (keyId != 0) { + PGPSecretKey masterKey = null; + mKeyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this, keyId); + if (mKeyRing != null) { + masterKey = PgpHelper.getMasterKey(mKeyRing); + for (PGPSecretKey key : new IterableIterator<PGPSecretKey>( + mKeyRing.getSecretKeys())) { + mKeys.add(key); + mKeysUsages.add(-1); // get usage when view is created + } + } + if (masterKey != null) { + for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) { + Log.d(Constants.TAG, "Added userId " + userId); + mUserIds.add(userId); + } + } + } + } + } + } + + /** + * Shows the dialog to set a new passphrase + */ + private void showSetPassphraseDialog() { + // Message is received after passphrase is cached + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == SetPassphraseDialogFragment.MESSAGE_OKAY) { + Bundle data = message.getData(); + + // set new returned passphrase! + mNewPassPhrase = data + .getString(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE); + + updatePassPhraseButtonText(); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + // set title based on isPassphraseSet() + int title = -1; + if (isPassphraseSet()) { + title = R.string.title_changePassPhrase; + } else { + title = R.string.title_setPassPhrase; + } + + SetPassphraseDialogFragment setPassphraseDialog = SetPassphraseDialogFragment.newInstance( + messenger, title); + + setPassphraseDialog.show(getSupportFragmentManager(), "setPassphraseDialog"); + } + + /** + * Build layout based on mUserId, mKeys and mKeysUsages Vectors. It creates Views for every user + * id and key. + */ + private void buildLayout() { + // Build layout based on given userIds and keys + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + LinearLayout container = (LinearLayout) findViewById(R.id.edit_key_container); + mUserIdsView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false); + mUserIdsView.setType(Id.type.user_id); + mUserIdsView.setUserIds(mUserIds); + container.addView(mUserIdsView); + mKeysView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false); + mKeysView.setType(Id.type.key); + mKeysView.setKeys(mKeys, mKeysUsages); + container.addView(mKeysView); + + updatePassPhraseButtonText(); + } + + private long getMasterKeyId() { + if (mKeysView.getEditors().getChildCount() == 0) { + return 0; + } + return ((KeyEditor) mKeysView.getEditors().getChildAt(0)).getValue().getKeyID(); + } + + public boolean isPassphraseSet() { + if (mNoPassphrase.isChecked()) { + return true; + } else if ((!mCurrentPassPhrase.equals("")) + || (mNewPassPhrase != null && !mNewPassPhrase.equals(""))) { + return true; + } else { + return false; + } + } + + private void saveClicked() { + try { + if (!isPassphraseSet()) { + throw new PgpMain.PgpGeneralException(this.getString(R.string.setAPassPhrase)); + } + + // Send all information needed to service to edit key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_SAVE_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + data.putString(KeychainIntentService.SAVE_KEYRING_CURRENT_PASSPHRASE, mCurrentPassPhrase); + data.putString(KeychainIntentService.SAVE_KEYRING_NEW_PASSPHRASE, mNewPassPhrase); + data.putStringArrayList(KeychainIntentService.SAVE_KEYRING_USER_IDS, + getUserIds(mUserIdsView)); + ArrayList<PGPSecretKey> keys = getKeys(mKeysView); + data.putByteArray(KeychainIntentService.SAVE_KEYRING_KEYS, + PgpConversionHelper.PGPSecretKeyArrayListToBytes(keys)); + data.putIntegerArrayList(KeychainIntentService.SAVE_KEYRING_KEYS_USAGES, + getKeysUsages(mKeysView)); + data.putLong(KeychainIntentService.SAVE_KEYRING_MASTER_KEY_ID, getMasterKeyId()); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after saving is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + R.string.progress_saving, ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + Intent data = new Intent(); + data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, getMasterKeyId()); + ArrayList<String> userIds = null; + try { + userIds = getUserIds(mUserIdsView); + } catch (PgpGeneralException e) { + Log.e(Constants.TAG, "exception while getting user ids", e); + } + data.putExtra(RESULT_EXTRA_USER_ID, userIds.get(0)); + setResult(RESULT_OK, data); + finish(); + } + }; + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + saveHandler.showProgressDialog(this); + + // start service with intent + startService(intent); + } catch (PgpMain.PgpGeneralException e) { + Toast.makeText(this, getString(R.string.errorMessage, e.getMessage()), + Toast.LENGTH_SHORT).show(); + } + } + + private void cancelClicked() { + setResult(RESULT_CANCELED); + finish(); + } + + /** + * Returns user ids from the SectionView + * + * @param userIdsView + * @return + */ + private ArrayList<String> getUserIds(SectionView userIdsView) + throws PgpMain.PgpGeneralException { + ArrayList<String> userIds = new ArrayList<String>(); + + ViewGroup userIdEditors = userIdsView.getEditors(); + + boolean gotMainUserId = false; + for (int i = 0; i < userIdEditors.getChildCount(); ++i) { + UserIdEditor editor = (UserIdEditor) userIdEditors.getChildAt(i); + String userId = null; + try { + userId = editor.getValue(); + } catch (UserIdEditor.NoNameException e) { + throw new PgpMain.PgpGeneralException( + this.getString(R.string.error_userIdNeedsAName)); + } catch (UserIdEditor.NoEmailException e) { + throw new PgpMain.PgpGeneralException( + this.getString(R.string.error_userIdNeedsAnEmailAddress)); + } catch (UserIdEditor.InvalidEmailException e) { + throw new PgpMain.PgpGeneralException(e.getMessage()); + } + + if (userId.equals("")) { + continue; + } + + if (editor.isMainUserId()) { + userIds.add(0, userId); + gotMainUserId = true; + } else { + userIds.add(userId); + } + } + + if (userIds.size() == 0) { + throw new PgpMain.PgpGeneralException(getString(R.string.error_keyNeedsAUserId)); + } + + if (!gotMainUserId) { + throw new PgpMain.PgpGeneralException( + getString(R.string.error_mainUserIdMustNotBeEmpty)); + } + + return userIds; + } + + /** + * Returns keys from the SectionView + * + * @param keysView + * @return + */ + private ArrayList<PGPSecretKey> getKeys(SectionView keysView) + throws PgpMain.PgpGeneralException { + ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>(); + + ViewGroup keyEditors = keysView.getEditors(); + + if (keyEditors.getChildCount() == 0) { + throw new PgpMain.PgpGeneralException(getString(R.string.error_keyNeedsMasterKey)); + } + + for (int i = 0; i < keyEditors.getChildCount(); ++i) { + KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i); + keys.add(editor.getValue()); + } + + return keys; + } + + /** + * Returns usage selections of keys from the SectionView + * + * @param keysView + * @return + */ + private ArrayList<Integer> getKeysUsages(SectionView keysView) + throws PgpMain.PgpGeneralException { + ArrayList<Integer> getKeysUsages = new ArrayList<Integer>(); + + ViewGroup keyEditors = keysView.getEditors(); + + if (keyEditors.getChildCount() == 0) { + throw new PgpMain.PgpGeneralException(getString(R.string.error_keyNeedsMasterKey)); + } + + for (int i = 0; i < keyEditors.getChildCount(); ++i) { + KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i); + getKeysUsages.add(editor.getUsage()); + } + + return getKeysUsages; + } + + private void updatePassPhraseButtonText() { + mChangePassPhrase.setText(isPassphraseSet() ? R.string.btn_changePassPhrase + : R.string.btn_setPassPhrase); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java new file mode 100644 index 000000000..bb61fb890 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java @@ -0,0 +1,1105 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.helper.PgpMain; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; +import org.sufficientlysecure.keychain.util.Choice; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.animation.AnimationUtils; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ViewFlipper; + +import java.io.File; +import java.util.Vector; + +public class EncryptActivity extends SherlockFragmentActivity { + + /* Intents */ + // without permission + public static final String ACTION_ENCRYPT = Constants.INTENT_PREFIX + "ENCRYPT"; + public static final String ACTION_ENCRYPT_FILE = Constants.INTENT_PREFIX + "ENCRYPT_FILE"; + + // with permission + public static final String ACTION_ENCRYPT_AND_RETURN = Constants.INTENT_PREFIX + + "ENCRYPT_AND_RETURN"; + public static final String ACTION_GENERATE_SIGNATURE_AND_RETURN = Constants.INTENT_PREFIX + + "GENERATE_SIGNATURE_AND_RETURN"; + public static final String ACTION_ENCRYPT_STREAM_AND_RETURN = Constants.INTENT_PREFIX + + "ENCRYPT_STREAM_AND_RETURN"; + + /* EXTRA keys for input */ + public static final String EXTRA_TEXT = "text"; + public static final String EXTRA_DATA = "data"; + public static final String EXTRA_ASCII_ARMOUR = "asciiArmour"; + public static final String EXTRA_SEND_TO = "sendTo"; + public static final String EXTRA_SUBJECT = "subject"; + public static final String EXTRA_SIGNATURE_KEY_ID = "signatureKeyId"; + public static final String EXTRA_ENCRYPTION_KEY_IDS = "encryptionKeyIds"; + + private String mSubject = null; + private String mSendTo = null; + + private long mEncryptionKeyIds[] = null; + + private boolean mEncryptImmediately = false; + private EditText mMessage = null; + private Button mSelectKeysButton = null; + + private boolean mEncryptEnabled = false; + private String mEncryptString = ""; + private boolean mEncryptToClipboardEnabled = false; + private String mEncryptToClipboardString = ""; + + private CheckBox mSign = null; + private TextView mMainUserId = null; + private TextView mMainUserIdRest = null; + + private ViewFlipper mSource = null; + private TextView mSourceLabel = null; + private ImageView mSourcePrevious = null; + private ImageView mSourceNext = null; + + private ViewFlipper mMode = null; + private TextView mModeLabel = null; + private ImageView mModePrevious = null; + private ImageView mModeNext = null; + + private int mEncryptTarget; + + private EditText mPassPhrase = null; + private EditText mPassPhraseAgain = null; + private CheckBox mAsciiArmour = null; + private Spinner mFileCompression = null; + + private EditText mFilename = null; + private CheckBox mDeleteAfter = null; + private ImageButton mBrowse = null; + + private String mInputFilename = null; + private String mOutputFilename = null; + + private boolean mAsciiArmorDemand = false; + private boolean mOverrideAsciiArmour = false; + private Uri mStreamAndReturnUri = null; + private byte[] mData = null; + + private boolean mGenerateSignature = false; + + private long mSecretKeyId = Id.key.none; + + private FileDialogFragment mFileDialog; + + /** + * ActionBar menu is created based on class variables to change it at runtime + * + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (mEncryptToClipboardEnabled) { + menu.add(1, Id.menu.option.encrypt_to_clipboard, 0, mEncryptToClipboardString) + .setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + if (mEncryptEnabled) { + menu.add(1, Id.menu.option.encrypt, 1, mEncryptString).setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + case Id.menu.option.encrypt_to_clipboard: + encryptToClipboardClicked(); + + return true; + + case Id.menu.option.encrypt: + encryptClicked(); + + return true; + + default: + return super.onOptionsItemSelected(item); + + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // check permissions for intent actions without user interaction + String[] restrictedActions = new String[] { ACTION_ENCRYPT_AND_RETURN, + ACTION_GENERATE_SIGNATURE_AND_RETURN, ACTION_ENCRYPT_STREAM_AND_RETURN }; + OtherHelper.checkPackagePermissionForActions(this, this.getCallingPackage(), + Constants.PERMISSION_ACCESS_API, getIntent().getAction(), restrictedActions); + + setContentView(R.layout.encrypt); + + // set actionbar without home button if called from another app + OtherHelper.setActionBarBackButton(this); + + initView(); + + // Handle intent actions + handleActions(getIntent()); + + updateView(); + updateSource(); + updateMode(); + + if (mEncryptImmediately) { + mSourcePrevious.setClickable(false); + mSourcePrevious.setEnabled(false); + mSourcePrevious.setVisibility(View.INVISIBLE); + + mSourceNext.setClickable(false); + mSourceNext.setEnabled(false); + mSourceNext.setVisibility(View.INVISIBLE); + + mSourceLabel.setClickable(false); + mSourceLabel.setEnabled(false); + } + + updateActionBarButtons(); + + if (mEncryptImmediately + && (mMessage.getText().length() > 0 || mData != null) + && ((mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) || mSecretKeyId != 0)) { + encryptClicked(); + } + } + + /** + * Handles all actions with this intent + * + * @param intent + */ + private void handleActions(Intent intent) { + String action = intent.getAction(); + Bundle extras = intent.getExtras(); + String type = intent.getType(); + Uri uri = intent.getData(); + + if (extras == null) { + extras = new Bundle(); + } + + /* + * Android's Action + */ + if (Intent.ACTION_SEND.equals(action) && type != null) { + // When sending to APG Encrypt via share menu + if ("text/plain".equals(type)) { + // Plain text + String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); + if (sharedText != null) { + // handle like normal text encryption, override action and extras to later + // execute ACTION_ENCRYPT in main actions + extras.putString(EXTRA_TEXT, sharedText); + extras.putBoolean(EXTRA_ASCII_ARMOUR, true); + action = ACTION_ENCRYPT; + } + } else { + // Files via content provider, override uri and action + uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); + action = ACTION_ENCRYPT_FILE; + } + } + + if (ACTION_ENCRYPT_AND_RETURN.equals(action) + || ACTION_GENERATE_SIGNATURE_AND_RETURN.equals(action)) { + mEncryptImmediately = true; + } + + if (ACTION_GENERATE_SIGNATURE_AND_RETURN.equals(action)) { + mGenerateSignature = true; + mOverrideAsciiArmour = true; + mAsciiArmorDemand = false; + } + + if (extras.containsKey(EXTRA_ASCII_ARMOUR)) { + mAsciiArmorDemand = extras.getBoolean(EXTRA_ASCII_ARMOUR, true); + mOverrideAsciiArmour = true; + mAsciiArmour.setChecked(mAsciiArmorDemand); + } + + mData = extras.getByteArray(EXTRA_DATA); + String textData = null; + if (mData == null) { + textData = extras.getString(EXTRA_TEXT); + } + mSendTo = extras.getString(EXTRA_SEND_TO); + mSubject = extras.getString(EXTRA_SUBJECT); + long signatureKeyId = extras.getLong(EXTRA_SIGNATURE_KEY_ID); + long[] encryptionKeyIds = extras.getLongArray(EXTRA_ENCRYPTION_KEY_IDS); + + // preselect keys given by intent + preselectKeys(signatureKeyId, encryptionKeyIds); + + /** + * Main Actions + */ + if (ACTION_ENCRYPT.equals(action) || ACTION_ENCRYPT_AND_RETURN.equals(action) + || ACTION_GENERATE_SIGNATURE_AND_RETURN.equals(action)) { + if (textData != null) { + mMessage.setText(textData); + } + mSource.setInAnimation(null); + mSource.setOutAnimation(null); + while (mSource.getCurrentView().getId() != R.id.sourceMessage) { + mSource.showNext(); + } + } else if (ACTION_ENCRYPT_FILE.equals(action)) { + // get file path from uri + String path = FileHelper.getPath(this, uri); + + if (path != null) { + mInputFilename = path; + mFilename.setText(mInputFilename); + + mSource.setInAnimation(null); + mSource.setOutAnimation(null); + while (mSource.getCurrentView().getId() != R.id.sourceFile) { + mSource.showNext(); + } + } else { + Log.e(Constants.TAG, + "Direct binary data without actual file in filesystem is not supported. This is only supported by ACTION_ENCRYPT_STREAM_AND_RETURN."); + Toast.makeText(this, R.string.error_onlyFilesAreSupported, Toast.LENGTH_LONG) + .show(); + // end activity + finish(); + } + } else if (ACTION_ENCRYPT_STREAM_AND_RETURN.equals(action)) { + // TODO: Set mStreamAndReturnUri that is used later to encrypt a stream! + + mStreamAndReturnUri = uri; + } + } + + /** + * If an Intent gives a signatureKeyId and/or encryptionKeyIds, preselect those! + * + * @param preselectedSignatureKeyId + * @param preselectedEncryptionKeyIds + */ + private void preselectKeys(long preselectedSignatureKeyId, long[] preselectedEncryptionKeyIds) { + if (preselectedSignatureKeyId != 0) { + PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this, + preselectedSignatureKeyId); + PGPSecretKey masterKey = null; + if (keyRing != null) { + masterKey = PgpHelper.getMasterKey(keyRing); + if (masterKey != null) { + Vector<PGPSecretKey> signKeys = PgpHelper.getUsableSigningKeys(keyRing); + if (signKeys.size() > 0) { + mSecretKeyId = masterKey.getKeyID(); + } + } + } + } + + if (preselectedEncryptionKeyIds != null) { + Vector<Long> goodIds = new Vector<Long>(); + for (int i = 0; i < preselectedEncryptionKeyIds.length; ++i) { + PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(this, + preselectedEncryptionKeyIds[i]); + PGPPublicKey masterKey = null; + if (keyRing == null) { + continue; + } + masterKey = PgpHelper.getMasterKey(keyRing); + if (masterKey == null) { + continue; + } + Vector<PGPPublicKey> encryptKeys = PgpHelper.getUsableEncryptKeys(keyRing); + if (encryptKeys.size() == 0) { + continue; + } + goodIds.add(masterKey.getKeyID()); + } + if (goodIds.size() > 0) { + mEncryptionKeyIds = new long[goodIds.size()]; + for (int i = 0; i < goodIds.size(); ++i) { + mEncryptionKeyIds[i] = goodIds.get(i); + } + } + } + } + + /** + * Guess output filename based on input path + * + * @param path + * @return Suggestion for output filename + */ + private String guessOutputFilename(String path) { + // output in the same directory but with additional ending + File file = new File(path); + String ending = (mAsciiArmour.isChecked() ? ".asc" : ".gpg"); + String outputFilename = file.getParent() + File.separator + file.getName() + ending; + + return outputFilename; + } + + private void updateSource() { + switch (mSource.getCurrentView().getId()) { + case R.id.sourceFile: { + mSourceLabel.setText(R.string.label_file); + break; + } + + case R.id.sourceMessage: { + mSourceLabel.setText(R.string.label_message); + break; + } + + default: { + break; + } + } + updateActionBarButtons(); + } + + /** + * Set ActionBar buttons based on parameters + * + * @param encryptEnabled + * @param encryptStringRes + * @param encryptToClipboardEnabled + * @param encryptToClipboardStringRes + */ + private void setActionbarButtons(boolean encryptEnabled, int encryptStringRes, + boolean encryptToClipboardEnabled, int encryptToClipboardStringRes) { + mEncryptEnabled = encryptEnabled; + if (encryptEnabled) { + mEncryptString = getString(encryptStringRes); + } + mEncryptToClipboardEnabled = encryptToClipboardEnabled; + if (encryptToClipboardEnabled) { + mEncryptToClipboardString = getString(encryptToClipboardStringRes); + } + + // build new action bar based on these class variables + invalidateOptionsMenu(); + } + + /** + * Update ActionBar buttons based on current selection in view + */ + private void updateActionBarButtons() { + switch (mSource.getCurrentView().getId()) { + case R.id.sourceFile: { + setActionbarButtons(true, R.string.btn_encryptFile, false, 0); + + break; + } + + case R.id.sourceMessage: { + mSourceLabel.setText(R.string.label_message); + + if (mMode.getCurrentView().getId() == R.id.modeSymmetric) { + if (mEncryptImmediately) { + setActionbarButtons(true, R.string.btn_encrypt, false, 0); + } else { + setActionbarButtons(true, R.string.btn_encryptAndEmail, true, + R.string.btn_encryptToClipboard); + } + } else { + if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) { + if (mSecretKeyId == 0) { + setActionbarButtons(false, 0, false, 0); + } else { + if (mEncryptImmediately) { + setActionbarButtons(true, R.string.btn_sign, false, 0); + } else { + setActionbarButtons(true, R.string.btn_signAndEmail, true, + R.string.btn_signToClipboard); + } + } + } else { + if (mEncryptImmediately) { + setActionbarButtons(true, R.string.btn_encrypt, false, 0); + } else { + setActionbarButtons(true, R.string.btn_encryptAndEmail, true, + R.string.btn_encryptToClipboard); + } + } + } + break; + } + + default: { + break; + } + } + + } + + private void updateMode() { + switch (mMode.getCurrentView().getId()) { + case R.id.modeAsymmetric: { + mModeLabel.setText(R.string.label_asymmetric); + break; + } + + case R.id.modeSymmetric: { + mModeLabel.setText(R.string.label_symmetric); + break; + } + + default: { + break; + } + } + updateActionBarButtons(); + } + + private void encryptToClipboardClicked() { + mEncryptTarget = Id.target.clipboard; + initiateEncryption(); + } + + private void encryptClicked() { + Log.d(Constants.TAG, "encryptClicked invoked!"); + + if (mSource.getCurrentView().getId() == R.id.sourceFile) { + mEncryptTarget = Id.target.file; + } else { + mEncryptTarget = Id.target.email; + } + initiateEncryption(); + } + + private void initiateEncryption() { + if (mEncryptTarget == Id.target.file) { + String currentFilename = mFilename.getText().toString(); + if (mInputFilename == null || !mInputFilename.equals(currentFilename)) { + mInputFilename = mFilename.getText().toString(); + } + + mOutputFilename = guessOutputFilename(mInputFilename); + + if (mInputFilename.equals("")) { + Toast.makeText(this, R.string.noFileSelected, Toast.LENGTH_SHORT).show(); + return; + } + + if (!mInputFilename.startsWith("content")) { + File file = new File(mInputFilename); + if (!file.exists() || !file.isFile()) { + Toast.makeText( + this, + getString(R.string.errorMessage, getString(R.string.error_fileNotFound)), + Toast.LENGTH_SHORT).show(); + return; + } + } + } + + // symmetric encryption + if (mMode.getCurrentView().getId() == R.id.modeSymmetric) { + boolean gotPassPhrase = false; + String passPhrase = mPassPhrase.getText().toString(); + String passPhraseAgain = mPassPhraseAgain.getText().toString(); + if (!passPhrase.equals(passPhraseAgain)) { + Toast.makeText(this, R.string.passPhrasesDoNotMatch, Toast.LENGTH_SHORT).show(); + return; + } + + gotPassPhrase = (passPhrase.length() != 0); + if (!gotPassPhrase) { + Toast.makeText(this, R.string.passPhraseMustNotBeEmpty, Toast.LENGTH_SHORT).show(); + return; + } + } else { + boolean encryptIt = (mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0); + // for now require at least one form of encryption for files + if (!encryptIt && mEncryptTarget == Id.target.file) { + Toast.makeText(this, R.string.selectEncryptionKey, Toast.LENGTH_SHORT).show(); + return; + } + + if (!encryptIt && mSecretKeyId == 0) { + Toast.makeText(this, R.string.selectEncryptionOrSignatureKey, Toast.LENGTH_SHORT) + .show(); + return; + } + + if (mSecretKeyId != 0 + && PassphraseCacheService.getCachedPassphrase(this, mSecretKeyId) == null) { + showPassphraseDialog(); + + return; + } + } + + if (mEncryptTarget == Id.target.file) { + showOutputFileDialog(); + } else { + encryptStart(); + } + } + + /** + * Shows passphrase dialog to cache a new passphrase the user enters for using it later for + * encryption + */ + private void showPassphraseDialog() { + // Message is received after passphrase is cached + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + if (mEncryptTarget == Id.target.file) { + showOutputFileDialog(); + } else { + encryptStart(); + } + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + try { + PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance( + EncryptActivity.this, messenger, mSecretKeyId); + + passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog"); + } catch (PgpMain.PgpGeneralException e) { + Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!"); + // send message to handler to start encryption directly + returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY); + } + } + + private void showOutputFileDialog() { + // Message is received after file is selected + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == FileDialogFragment.MESSAGE_OKAY) { + Bundle data = message.getData(); + mOutputFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME); + encryptStart(); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + mFileDialog = FileDialogFragment.newInstance(messenger, + getString(R.string.title_encryptToFile), + getString(R.string.specifyFileToEncryptTo), mOutputFilename, null, + Id.request.output_filename); + + mFileDialog.show(getSupportFragmentManager(), "fileDialog"); + } + + private void encryptStart() { + // Send all information needed to service to edit key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + // fill values for this action + Bundle data = new Bundle(); + + boolean useAsciiArmor = true; + long encryptionKeyIds[] = null; + int compressionId = 0; + boolean signOnly = false; + + if (mMode.getCurrentView().getId() == R.id.modeSymmetric) { + Log.d(Constants.TAG, "Symmetric encryption enabled!"); + String passPhrase = mPassPhrase.getText().toString(); + if (passPhrase.length() == 0) { + passPhrase = null; + } + + data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE, passPhrase); + } else { + encryptionKeyIds = mEncryptionKeyIds; + signOnly = (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0); + } + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_ENCRYPT_SIGN); + + // choose default settings, target and data bundle by target + if (mStreamAndReturnUri != null) { + // mIntentDataUri is only defined when ACTION_ENCRYPT_STREAM_AND_RETURN is used + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_STREAM); + data.putParcelable(KeychainIntentService.ENCRYPT_PROVIDER_URI, mStreamAndReturnUri); + + } else if (mEncryptTarget == Id.target.file) { + useAsciiArmor = mAsciiArmour.isChecked(); + compressionId = ((Choice) mFileCompression.getSelectedItem()).getId(); + + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE); + + Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename=" + + mOutputFilename); + + data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename); + data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename); + + } else { + useAsciiArmor = true; + compressionId = Preferences.getPreferences(this).getDefaultMessageCompression(); + + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES); + + if (mData != null) { + data.putByteArray(KeychainIntentService.ENCRYPT_MESSAGE_BYTES, mData); + } else { + String message = mMessage.getText().toString(); + if (signOnly && !mEncryptImmediately) { + fixBadCharactersForGmail(message); + } + data.putByteArray(KeychainIntentService.ENCRYPT_MESSAGE_BYTES, message.getBytes()); + } + } + + if (mOverrideAsciiArmour) { + useAsciiArmor = mAsciiArmorDemand; + } + + data.putLong(KeychainIntentService.ENCRYPT_SECRET_KEY_ID, mSecretKeyId); + data.putBoolean(KeychainIntentService.ENCRYPT_USE_ASCII_AMOR, useAsciiArmor); + data.putLongArray(KeychainIntentService.ENCRYPT_ENCRYPTION_KEYS_IDS, encryptionKeyIds); + data.putInt(KeychainIntentService.ENCRYPT_COMPRESSION_ID, compressionId); + data.putBoolean(KeychainIntentService.ENCRYPT_GENERATE_SIGNATURE, mGenerateSignature); + data.putBoolean(KeychainIntentService.ENCRYPT_SIGN_ONLY, signOnly); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after encrypting is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + R.string.progress_encrypting, ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + // get returned data bundle + Bundle data = message.getData(); + + String output; + switch (mEncryptTarget) { + case Id.target.clipboard: + output = data.getString(KeychainIntentService.RESULT_ENCRYPTED_STRING); + Log.d(Constants.TAG, "output: " + output); + ClipboardReflection.copyToClipboard(EncryptActivity.this, output); + Toast.makeText(EncryptActivity.this, + R.string.encryptionToClipboardSuccessful, Toast.LENGTH_SHORT) + .show(); + break; + + case Id.target.email: + if (mEncryptImmediately) { + Intent intent = new Intent(); + intent.putExtras(data); + setResult(RESULT_OK, intent); + finish(); + return; + } + + output = data.getString(KeychainIntentService.RESULT_ENCRYPTED_STRING); + Log.d(Constants.TAG, "output: " + output); + + Intent emailIntent = new Intent(Intent.ACTION_SEND); + emailIntent.setType("text/plain; charset=utf-8"); + emailIntent.putExtra(Intent.EXTRA_TEXT, output); + if (mSubject != null) { + emailIntent.putExtra(Intent.EXTRA_SUBJECT, mSubject); + } + if (mSendTo != null) { + emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] { mSendTo }); + } + startActivity(Intent.createChooser(emailIntent, + getString(R.string.title_sendEmail))); + break; + + case Id.target.file: + Toast.makeText(EncryptActivity.this, R.string.encryptionSuccessful, + Toast.LENGTH_SHORT).show(); + + if (mDeleteAfter.isChecked()) { + // Create and show dialog to delete original file + DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment + .newInstance(mInputFilename); + deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog"); + } + break; + + default: + // shouldn't happen + break; + + } + } + }; + }; + + // 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); + } + + /** + * Fixes bad message characters for gmail + * + * @param message + * @return + */ + private String fixBadCharactersForGmail(String message) { + // fix the message a bit, trailing spaces and newlines break stuff, + // because GMail sends as HTML and such things fuck up the + // signature, + // TODO: things like "<" and ">" also fuck up the signature + message = message.replaceAll(" +\n", "\n"); + message = message.replaceAll("\n\n+", "\n\n"); + message = message.replaceFirst("^\n+", ""); + // make sure there'll be exactly one newline at the end + message = message.replaceFirst("\n*$", "\n"); + + return message; + } + + private void initView() { + mSource = (ViewFlipper) findViewById(R.id.source); + mSourceLabel = (TextView) findViewById(R.id.sourceLabel); + mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious); + mSourceNext = (ImageView) findViewById(R.id.sourceNext); + + mSourcePrevious.setClickable(true); + mSourcePrevious.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mSource.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this, + R.anim.push_right_in)); + mSource.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this, + R.anim.push_right_out)); + mSource.showPrevious(); + updateSource(); + } + }); + + mSourceNext.setClickable(true); + OnClickListener nextSourceClickListener = new OnClickListener() { + public void onClick(View v) { + mSource.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this, + R.anim.push_left_in)); + mSource.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this, + R.anim.push_left_out)); + mSource.showNext(); + updateSource(); + } + }; + mSourceNext.setOnClickListener(nextSourceClickListener); + + mSourceLabel.setClickable(true); + mSourceLabel.setOnClickListener(nextSourceClickListener); + + mMode = (ViewFlipper) findViewById(R.id.mode); + mModeLabel = (TextView) findViewById(R.id.modeLabel); + mModePrevious = (ImageView) findViewById(R.id.modePrevious); + mModeNext = (ImageView) findViewById(R.id.modeNext); + + mModePrevious.setClickable(true); + mModePrevious.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mMode.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this, + R.anim.push_right_in)); + mMode.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this, + R.anim.push_right_out)); + mMode.showPrevious(); + updateMode(); + } + }); + + OnClickListener nextModeClickListener = new OnClickListener() { + public void onClick(View v) { + mMode.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this, + R.anim.push_left_in)); + mMode.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this, + R.anim.push_left_out)); + mMode.showNext(); + updateMode(); + } + }; + mModeNext.setOnClickListener(nextModeClickListener); + + mModeLabel.setClickable(true); + mModeLabel.setOnClickListener(nextModeClickListener); + + mMessage = (EditText) findViewById(R.id.message); + mSelectKeysButton = (Button) findViewById(R.id.btn_selectEncryptKeys); + mSign = (CheckBox) findViewById(R.id.sign); + mMainUserId = (TextView) findViewById(R.id.mainUserId); + mMainUserIdRest = (TextView) findViewById(R.id.mainUserIdRest); + + mPassPhrase = (EditText) findViewById(R.id.passPhrase); + mPassPhraseAgain = (EditText) findViewById(R.id.passPhraseAgain); + + // measure the height of the source_file view and set the message view's min height to that, + // so it fills mSource fully... bit of a hack. + View tmp = findViewById(R.id.sourceFile); + tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + int height = tmp.getMeasuredHeight(); + mMessage.setMinimumHeight(height); + + mFilename = (EditText) findViewById(R.id.filename); + mBrowse = (ImageButton) findViewById(R.id.btn_browse); + mBrowse.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + FileHelper.openFile(EncryptActivity.this, mFilename.getText().toString(), "*/*", + Id.request.filename); + } + }); + + mFileCompression = (Spinner) findViewById(R.id.fileCompression); + Choice[] choices = new Choice[] { + new Choice(Id.choice.compression.none, getString(R.string.choice_none) + " (" + + getString(R.string.fast) + ")"), + new Choice(Id.choice.compression.zip, "ZIP (" + getString(R.string.fast) + ")"), + new Choice(Id.choice.compression.zlib, "ZLIB (" + getString(R.string.fast) + ")"), + new Choice(Id.choice.compression.bzip2, "BZIP2 (" + getString(R.string.very_slow) + + ")"), }; + ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(this, + android.R.layout.simple_spinner_item, choices); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mFileCompression.setAdapter(adapter); + + int defaultFileCompression = Preferences.getPreferences(this).getDefaultFileCompression(); + for (int i = 0; i < choices.length; ++i) { + if (choices[i].getId() == defaultFileCompression) { + mFileCompression.setSelection(i); + break; + } + } + + mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterEncryption); + + mAsciiArmour = (CheckBox) findViewById(R.id.asciiArmour); + mAsciiArmour.setChecked(Preferences.getPreferences(this).getDefaultAsciiArmour()); + + mSelectKeysButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + selectPublicKeys(); + } + }); + + mSign.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + CheckBox checkBox = (CheckBox) v; + if (checkBox.isChecked()) { + selectSecretKey(); + } else { + mSecretKeyId = Id.key.none; + updateView(); + } + } + }); + } + + private void updateView() { + if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) { + mSelectKeysButton.setText(R.string.noKeysSelected); + } else if (mEncryptionKeyIds.length == 1) { + mSelectKeysButton.setText(R.string.oneKeySelected); + } else { + mSelectKeysButton.setText("" + mEncryptionKeyIds.length + " " + + getResources().getString(R.string.nKeysSelected)); + } + + if (mSecretKeyId == Id.key.none) { + mSign.setChecked(false); + mMainUserId.setText(""); + mMainUserIdRest.setText(""); + } else { + String uid = getResources().getString(R.string.unknownUserId); + String uidExtra = ""; + PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this, + mSecretKeyId); + if (keyRing != null) { + PGPSecretKey key = PgpHelper.getMasterKey(keyRing); + if (key != null) { + String userId = PgpHelper.getMainUserIdSafe(this, key); + String chunks[] = userId.split(" <", 2); + uid = chunks[0]; + if (chunks.length > 1) { + uidExtra = "<" + chunks[1]; + } + } + } + mMainUserId.setText(uid); + mMainUserIdRest.setText(uidExtra); + mSign.setChecked(true); + } + + updateActionBarButtons(); + } + + private void selectPublicKeys() { + Intent intent = new Intent(this, SelectPublicKeyActivity.class); + Vector<Long> keyIds = new Vector<Long>(); + if (mSecretKeyId != 0) { + keyIds.add(mSecretKeyId); + } + if (mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) { + for (int i = 0; i < mEncryptionKeyIds.length; ++i) { + keyIds.add(mEncryptionKeyIds[i]); + } + } + long[] initialKeyIds = null; + if (keyIds.size() > 0) { + initialKeyIds = new long[keyIds.size()]; + for (int i = 0; i < keyIds.size(); ++i) { + initialKeyIds[i] = keyIds.get(i); + } + } + intent.putExtra(SelectPublicKeyActivity.EXTRA_SELECTED_MASTER_KEY_IDS, initialKeyIds); + startActivityForResult(intent, Id.request.public_keys); + } + + private void selectSecretKey() { + Intent intent = new Intent(this, SelectSecretKeyActivity.class); + startActivityForResult(intent, Id.request.secret_keys); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case Id.request.filename: { + if (resultCode == RESULT_OK && data != null) { + try { + String path = FileHelper.getPath(this, data.getData()); + Log.d(Constants.TAG, "path=" + path); + + mFilename.setText(path); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Nullpointer while retrieving path!"); + } + } + return; + } + + case Id.request.output_filename: { + if (resultCode == RESULT_OK && data != null) { + try { + String path = data.getData().getPath(); + Log.d(Constants.TAG, "path=" + path); + + mFileDialog.setFilename(path); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Nullpointer while retrieving path!"); + } + } + return; + } + + case Id.request.public_keys: { + if (resultCode == RESULT_OK) { + Bundle bundle = data.getExtras(); + mEncryptionKeyIds = bundle + .getLongArray(SelectPublicKeyActivity.RESULT_EXTRA_MASTER_KEY_IDS); + } + updateView(); + break; + } + + case Id.request.secret_keys: { + if (resultCode == RESULT_OK) { + Bundle bundle = data.getExtras(); + mSecretKeyId = bundle.getLong(SelectSecretKeyActivity.RESULT_EXTRA_MASTER_KEY_ID); + } else { + mSecretKeyId = Id.key.none; + } + updateView(); + break; + } + + default: { + break; + } + } + + super.onActivityResult(requestCode, resultCode, data); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java new file mode 100644 index 000000000..5b2750742 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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; + +import org.sufficientlysecure.keychain.R; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.widget.TextView; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.ActionBar.Tab; +import com.actionbarsherlock.view.MenuItem; + +import java.util.ArrayList; + +import android.content.Context; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; + +import com.actionbarsherlock.app.SherlockFragmentActivity; + +public class HelpActivity extends SherlockFragmentActivity { + public static final String EXTRA_SELECTED_TAB = "selectedTab"; + + ViewPager mViewPager; + TabsAdapter mTabsAdapter; + TextView tabCenter; + TextView tabText; + + /** + * Menu Items + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mViewPager = new ViewPager(this); + mViewPager.setId(R.id.pager); + + setContentView(mViewPager); + ActionBar bar = getSupportActionBar(); + bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + bar.setDisplayShowTitleEnabled(true); + bar.setDisplayHomeAsUpEnabled(true); + + mTabsAdapter = new TabsAdapter(this, mViewPager); + + int selectedTab = 0; + Intent intent = getIntent(); + if (intent.getExtras() != null && intent.getExtras().containsKey(EXTRA_SELECTED_TAB)) { + selectedTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB); + } + + Bundle startBundle = new Bundle(); + startBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_start); + mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_start)), + HelpFragmentHtml.class, startBundle, (selectedTab == 0 ? true : false)); + + Bundle nfcBundle = new Bundle(); + nfcBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_nfc_beam); + mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_nfc_beam)), + HelpFragmentHtml.class, nfcBundle, (selectedTab == 1 ? true : false)); + + Bundle changelogBundle = new Bundle(); + changelogBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_changelog); + mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_changelog)), + HelpFragmentHtml.class, changelogBundle, (selectedTab == 2 ? true : false)); + + mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_about)), + HelpFragmentAbout.class, null, (selectedTab == 3 ? true : false)); + } + + public static class TabsAdapter extends FragmentPagerAdapter implements ActionBar.TabListener, + ViewPager.OnPageChangeListener { + private final Context mContext; + private final ActionBar mActionBar; + private final ViewPager mViewPager; + private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>(); + + static final class TabInfo { + private final Class<?> clss; + private final Bundle args; + + TabInfo(Class<?> _class, Bundle _args) { + clss = _class; + args = _args; + } + } + + public TabsAdapter(SherlockFragmentActivity activity, ViewPager pager) { + super(activity.getSupportFragmentManager()); + mContext = activity; + mActionBar = activity.getSupportActionBar(); + mViewPager = pager; + mViewPager.setAdapter(this); + mViewPager.setOnPageChangeListener(this); + } + + public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args, boolean selected) { + TabInfo info = new TabInfo(clss, args); + tab.setTag(info); + tab.setTabListener(this); + mTabs.add(info); + mActionBar.addTab(tab, selected); + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return mTabs.size(); + } + + @Override + public Fragment getItem(int position) { + TabInfo info = mTabs.get(position); + return Fragment.instantiate(mContext, info.clss.getName(), info.args); + } + + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + public void onPageSelected(int position) { + mActionBar.setSelectedNavigationItem(position); + } + + public void onPageScrollStateChanged(int state) { + } + + public void onTabSelected(Tab tab, FragmentTransaction ft) { + Object tag = tab.getTag(); + for (int i = 0; i < mTabs.size(); i++) { + if (mTabs.get(i) == tag) { + mViewPager.setCurrentItem(i); + } + } + } + + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + } + + public void onTabReselected(Tab tab, FragmentTransaction ft) { + } + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java new file mode 100644 index 000000000..05891b053 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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; + +import java.io.IOException; +import java.io.InputStream; + +import net.nightwhistler.htmlspanner.HtmlSpanner; +import net.nightwhistler.htmlspanner.JellyBeanSpanFixTextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.actionbarsherlock.app.SherlockFragment; + +public class HelpFragmentAbout extends SherlockFragment { + + /** + * Workaround for Android Bug. See + * http://stackoverflow.com/questions/8748064/starting-activity-from + * -fragment-causes-nullpointerexception + */ + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + setUserVisibleHint(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.help_fragment_about, container, false); + + // load html from html file from /res/raw + InputStream inputStreamText = getResources().openRawResource(R.raw.help_about); + + TextView versionText = (TextView) view.findViewById(R.id.help_about_version); + versionText.setText(getString(R.string.help_about_version) + " " + getVersion()); + + JellyBeanSpanFixTextView aboutTextView = (JellyBeanSpanFixTextView) view + .findViewById(R.id.help_about_text); + + // load html into textview + HtmlSpanner htmlSpanner = new HtmlSpanner(); + htmlSpanner.setStripExtraWhiteSpace(true); + try { + aboutTextView.setText(htmlSpanner.fromHtml(inputStreamText)); + } catch (IOException e) { + Log.e(Constants.TAG, "Error while reading raw resources as stream", e); + } + + // make links work + aboutTextView.setMovementMethod(LinkMovementMethod.getInstance()); + + // no flickering when clicking textview for Android < 4 + aboutTextView.setTextColor(getResources().getColor(android.R.color.black)); + + return view; + } + + /** + * Get the current package version. + * + * @return The current version. + */ + private String getVersion() { + String result = ""; + try { + PackageManager manager = getActivity().getPackageManager(); + PackageInfo info = manager.getPackageInfo(getActivity().getPackageName(), 0); + + result = String.format("%s (%s)", info.versionName, info.versionCode); + } catch (NameNotFoundException e) { + Log.w(Constants.TAG, "Unable to get application version: " + e.getMessage()); + result = "Unable to get application version."; + } + + return result; + } + +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java new file mode 100644 index 000000000..9c0bb6ded --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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; + +import java.io.IOException; +import java.io.InputStream; + +import net.nightwhistler.htmlspanner.HtmlSpanner; +import net.nightwhistler.htmlspanner.JellyBeanSpanFixTextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.app.Activity; +import android.os.Bundle; +import android.text.method.LinkMovementMethod; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +import com.actionbarsherlock.app.SherlockFragment; + +public class HelpFragmentHtml extends SherlockFragment { + private Activity mActivity; + + private int htmlFile; + + public static final String ARG_HTML_FILE = "htmlFile"; + + /** + * Create a new instance of HelpFragmentHtml, providing "htmlFile" as an argument. + */ + static HelpFragmentHtml newInstance(int htmlFile) { + HelpFragmentHtml f = new HelpFragmentHtml(); + + // Supply html raw file input as an argument. + Bundle args = new Bundle(); + args.putInt(ARG_HTML_FILE, htmlFile); + f.setArguments(args); + + return f; + } + + /** + * Workaround for Android Bug. See + * http://stackoverflow.com/questions/8748064/starting-activity-from + * -fragment-causes-nullpointerexception + */ + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + setUserVisibleHint(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + htmlFile = getArguments().getInt(ARG_HTML_FILE); + + // load html from html file from /res/raw + InputStream inputStreamText = getResources().openRawResource(htmlFile); + + mActivity = getActivity(); + + ScrollView scroller = new ScrollView(mActivity); + JellyBeanSpanFixTextView text = new JellyBeanSpanFixTextView(mActivity); + + // padding + int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, mActivity + .getResources().getDisplayMetrics()); + text.setPadding(padding, padding, padding, 0); + + scroller.addView(text); + + // load html into textview + HtmlSpanner htmlSpanner = new HtmlSpanner(); + htmlSpanner.setStripExtraWhiteSpace(true); + try { + text.setText(htmlSpanner.fromHtml(inputStreamText)); + } catch (IOException e) { + Log.e(Constants.TAG, "Error while reading raw resources as stream", e); + } + + // make links work + text.setMovementMethod(LinkMovementMethod.getInstance()); + + // no flickering when clicking textview for Android < 4 + text.setTextColor(getResources().getColor(android.R.color.black)); + + return scroller; + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java new file mode 100644 index 000000000..cd5a12df0 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2011 Senecaso + * + * 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; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; + + +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.view.View; +import android.widget.Toast; + +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; + +public class ImportKeysActivity extends SherlockFragmentActivity { + public static final String ACTION_IMPORT = Constants.INTENT_PREFIX + "IMPORT"; + public static final String ACTION_IMPORT_FROM_FILE = Constants.INTENT_PREFIX + + "IMPORT_FROM_FILE"; + public static final String ACTION_IMPORT_FROM_QR_CODE = Constants.INTENT_PREFIX + + "IMPORT_FROM_QR_CODE"; + public static final String ACTION_IMPORT_FROM_NFC = Constants.INTENT_PREFIX + "IMPORT_FROM_NFC"; + + // only used by IMPORT + public static final String EXTRA_TEXT = "text"; + public static final String EXTRA_KEYRING_BYTES = "keyringBytes"; + + // public static final String EXTRA_KEY_ID = "keyId"; + + protected String mImportFilename; + protected byte[] mImportData; + + protected boolean mDeleteAfterImport = false; + + FileDialogFragment mFileDialog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.import_keys); + + // set actionbar without home button if called from another app + OtherHelper.setActionBarBackButton(this); + + handleActions(getIntent()); + } + + /** + * ActionBar menu is created based on class variables to change it at runtime + * + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(1, Id.menu.option.import_from_file, 0, R.string.menu_importFromFile) + .setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + menu.add(1, Id.menu.option.import_from_qr_code, 1, R.string.menu_importFromQrCode) + .setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + menu.add(1, Id.menu.option.import_from_nfc, 2, R.string.menu_importFromNfc) + .setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + case Id.menu.option.import_from_file: + showImportFromFileDialog(); + return true; + + case Id.menu.option.import_from_qr_code: + importFromQrCode(); + return true; + + case Id.menu.option.import_from_nfc: + importFromNfc(); + return true; + + default: + return super.onOptionsItemSelected(item); + + } + } + + protected void handleActions(Intent intent) { + String action = intent.getAction(); + Bundle extras = intent.getExtras(); + + if (extras == null) { + extras = new Bundle(); + } + + /** + * Android Standard Actions + */ + if (Intent.ACTION_VIEW.equals(action)) { + // Android's Action when opening file associated to APG (see AndroidManifest.xml) + // override action to delegate it to APGs ACTION_IMPORT + action = ACTION_IMPORT; + } + + /** + * APG's own Actions + */ + if (ACTION_IMPORT.equals(action)) { + if ("file".equals(intent.getScheme()) && intent.getDataString() != null) { + mImportFilename = intent.getData().getPath(); + mImportData = null; + } else if (extras.containsKey(EXTRA_TEXT)) { + mImportData = intent.getStringExtra(EXTRA_TEXT).getBytes(); + mImportFilename = null; + } else if (extras.containsKey(EXTRA_KEYRING_BYTES)) { + mImportData = intent.getByteArrayExtra(EXTRA_KEYRING_BYTES); + mImportFilename = null; + } + loadKeyListFragment(); + } else if (ACTION_IMPORT_FROM_FILE.equals(action)) { + if ("file".equals(intent.getScheme()) && intent.getDataString() != null) { + mImportFilename = intent.getData().getPath(); + mImportData = null; + } + showImportFromFileDialog(); + } else if (ACTION_IMPORT_FROM_QR_CODE.equals(action)) { + importFromQrCode(); + } else if (ACTION_IMPORT_FROM_NFC.equals(action)) { + importFromNfc(); + } + } + + public void loadKeyListFragment() { + if (mImportData != null || mImportFilename != null) { + // generate list of keyrings + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + + ImportKeysListFragment listFragment = new ImportKeysListFragment(); + Bundle args = new Bundle(); + args.putByteArray(ImportKeysListFragment.ARG_KEYRING_BYTES, mImportData); + args.putString(ImportKeysListFragment.ARG_IMPORT_FILENAME, mImportFilename); + listFragment.setArguments(args); + // replace container in view with fragment + fragmentTransaction.replace(R.id.import_keys_list_container, listFragment); + fragmentTransaction.commit(); + } + } + + private void importFromQrCode() { + new IntentIntegrator(this).initiateScan(); + } + + private void importFromNfc() { + // show nfc help + Intent intent = new Intent(this, HelpActivity.class); + intent.putExtra(HelpActivity.EXTRA_SELECTED_TAB, 1); + startActivityForResult(intent, 0); + } + + /** + * Show to dialog from where to import keys + */ + public void showImportFromFileDialog() { + // Message is received after file is selected + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == FileDialogFragment.MESSAGE_OKAY) { + Bundle data = message.getData(); + mImportFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME); + mDeleteAfterImport = data.getBoolean(FileDialogFragment.MESSAGE_DATA_CHECKED); + + Log.d(Constants.TAG, "mImportFilename: " + mImportFilename); + Log.d(Constants.TAG, "mDeleteAfterImport: " + mDeleteAfterImport); + + loadKeyListFragment(); + } + } + }; + + // Create a new Messenger for the communication back + final Messenger messenger = new Messenger(returnHandler); + + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + public void run() { + mFileDialog = FileDialogFragment.newInstance(messenger, + getString(R.string.title_importKeys), + getString(R.string.specifyFileToImportFrom), Constants.path.APP_DIR + "/", + null, Id.request.filename); + + mFileDialog.show(getSupportFragmentManager(), "fileDialog"); + } + }); + } + + // private void importAndSignOld(final long keyId, final String expectedFingerprint) { + // if (expectedFingerprint != null && expectedFingerprint.length() > 0) { + // + // Thread t = new Thread() { + // @Override + // public void run() { + // try { + // // TODO: display some sort of spinner here while the user waits + // + // // TODO: there should be only 1 + // HkpKeyServer server = new HkpKeyServer(mPreferences.getKeyServers()[0]); + // String encodedKey = server.get(keyId); + // + // PGPKeyRing keyring = PGPHelper.decodeKeyRing(new ByteArrayInputStream( + // encodedKey.getBytes())); + // if (keyring != null && keyring instanceof PGPPublicKeyRing) { + // PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring; + // + // // make sure the fingerprints match before we cache this thing + // String actualFingerprint = PGPHelper.convertFingerprintToHex(publicKeyRing + // .getPublicKey().getFingerprint()); + // if (expectedFingerprint.equals(actualFingerprint)) { + // // store the signed key in our local cache + // int retval = PGPMain.storeKeyRingInCache(publicKeyRing); + // if (retval != Id.return_value.ok + // && retval != Id.return_value.updated) { + // status.putString(EXTRA_ERROR, + // "Failed to store signed key in local cache"); + // } else { + // Intent intent = new Intent(ImportFromQRCodeActivity.this, + // SignKeyActivity.class); + // intent.putExtra(EXTRA_KEY_ID, keyId); + // startActivityForResult(intent, Id.request.sign_key); + // } + // } else { + // status.putString( + // EXTRA_ERROR, + // "Scanned fingerprint does NOT match the fingerprint of the received key. You shouldnt trust this key."); + // } + // } + // } catch (QueryException e) { + // Log.e(TAG, "Failed to query KeyServer", e); + // status.putString(EXTRA_ERROR, "Failed to query KeyServer"); + // status.putInt(Constants.extras.STATUS, Id.message.done); + // } catch (IOException e) { + // Log.e(TAG, "Failed to query KeyServer", e); + // status.putString(EXTRA_ERROR, "Failed to query KeyServer"); + // status.putInt(Constants.extras.STATUS, Id.message.done); + // } + // } + // }; + // + // t.setName("KeyExchange Download Thread"); + // t.setDaemon(true); + // t.start(); + // } + // } + + public void scanAgainOnClick(View view) { + new IntentIntegrator(this).initiateScan(); + } + + public void finishOnClick(View view) { + finish(); + } + + public void importOnClick(View view) { + Log.d(Constants.TAG, "Import key button clicked!"); + + importKeys(); + } + + /** + * Import keys with mImportData + */ + public void importKeys() { + if (mImportData != null || mImportFilename != null) { + Log.d(Constants.TAG, "importKeys started"); + + // Send all information needed to service to import key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_IMPORT_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + + // TODO: check for key type? + // data.putInt(ApgIntentService.IMPORT_KEY_TYPE, Id.type.secret_key); + + if (mImportData != null) { + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES); + data.putByteArray(KeychainIntentService.IMPORT_BYTES, mImportData); + } else { + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE); + data.putString(KeychainIntentService.IMPORT_FILENAME, mImportFilename); + } + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after importing is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + R.string.progress_importing, ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + // get returned data bundle + Bundle returnData = message.getData(); + + int added = returnData.getInt(KeychainIntentService.RESULT_IMPORT_ADDED); + int updated = returnData.getInt(KeychainIntentService.RESULT_IMPORT_UPDATED); + int bad = returnData.getInt(KeychainIntentService.RESULT_IMPORT_BAD); + String toastMessage; + if (added > 0 && updated > 0) { + toastMessage = getString(R.string.keysAddedAndUpdated, added, updated); + } else if (added > 0) { + toastMessage = getString(R.string.keysAdded, added); + } else if (updated > 0) { + toastMessage = getString(R.string.keysUpdated, updated); + } else { + toastMessage = getString(R.string.noKeysAddedOrUpdated); + } + Toast.makeText(ImportKeysActivity.this, toastMessage, Toast.LENGTH_SHORT) + .show(); + if (bad > 0) { + AlertDialog.Builder alert = new AlertDialog.Builder( + ImportKeysActivity.this); + + alert.setIcon(android.R.drawable.ic_dialog_alert); + alert.setTitle(R.string.warning); + alert.setMessage(ImportKeysActivity.this.getString( + R.string.badKeysEncountered, bad)); + + alert.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + alert.setCancelable(true); + alert.create().show(); + } else if (mDeleteAfterImport) { + // everything went well, so now delete, if that was turned on + DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment + .newInstance(mImportFilename); + deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog"); + } + } + }; + }; + + // 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); + } else { + Toast.makeText(this, R.string.error_nothingImport, Toast.LENGTH_LONG).show(); + } + } + + public void signAndUploadOnClick(View view) { + // first, import! + importOnClick(view); + + // TODO: implement sign and upload! + Toast.makeText(ImportKeysActivity.this, "Not implemented right now!", Toast.LENGTH_SHORT) + .show(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case Id.request.filename: { + if (resultCode == RESULT_OK && data != null) { + try { + String path = data.getData().getPath(); + Log.d(Constants.TAG, "path=" + path); + + // set filename used in export/import dialogs + mFileDialog.setFilename(path); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Nullpointer while retrieving path!", e); + } + } + return; + } + case IntentIntegrator.REQUEST_CODE: { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, + data); + if (scanResult != null && scanResult.getFormatName() != null) { + + // mScannedContent = scanResult.getContents(); + + mImportData = scanResult.getContents().getBytes(); + mImportFilename = null; + + // mContentView.setText(mScannedContent); + // String[] bits = scanResult.getContents().split(","); + // if (bits.length != 2) { + // return; // dont know how to handle this. Not a valid code + // } + // + // long keyId = Long.parseLong(bits[0]); + // String expectedFingerprint = bits[1]; + + // importAndSign(keyId, expectedFingerprint); + } + + break; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + } + } + } + + @Override + protected void onResume() { + super.onResume(); + + loadKeyListFragment(); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java new file mode 100644 index 000000000..170b459be --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.ui.widget.ImportKeysListLoader; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.SherlockListFragment; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.content.Loader; +import android.support.v4.app.LoaderManager; +import android.view.View; +import android.widget.ListView; +import android.widget.SimpleAdapter; + +public class ImportKeysListFragment extends SherlockListFragment implements + LoaderManager.LoaderCallbacks<List<Map<String, String>>> { + public static String ARG_KEYRING_BYTES = "bytes"; + public static String ARG_IMPORT_FILENAME = "filename"; + + byte[] mKeyringBytes; + String mImportFilename; + + private Activity mActivity; + private SimpleAdapter mAdapter; + + @Override + public void onListItemClick(ListView listView, View view, int position, long id) { + // Map<String, String> item = (Map<String, String>) listView.getItemAtPosition(position); + // String userId = item.get(ImportKeysListLoader.MAP_ATTR_USER_ID); + } + + /** + * Resume is called after rotating + */ + @Override + public void onResume() { + super.onResume(); + + // Start out with a progress indicator. + setListShown(false); + + // reload list + getLoaderManager().restartLoader(0, null, this); + } + + /** + * Define Adapter and Loader on create of Activity + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mActivity = this.getActivity(); + + mKeyringBytes = getArguments().getByteArray(ARG_KEYRING_BYTES); + mImportFilename = getArguments().getString(ARG_IMPORT_FILENAME); + + // register long press context menu + registerForContextMenu(getListView()); + + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText(mActivity.getString(R.string.error_nothingImport)); + + // Create an empty adapter we will use to display the loaded data. + String[] from = new String[] {}; + int[] to = new int[] {}; + List<Map<String, String>> data = new ArrayList<Map<String, String>>(); + mAdapter = new SimpleAdapter(getActivity(), data, android.R.layout.two_line_list_item, + from, to); + setListAdapter(mAdapter); + + // Start out with a progress indicator. + setListShown(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); + } + + @Override + public Loader<List<Map<String, String>>> onCreateLoader(int id, Bundle args) { + return new ImportKeysListLoader(mActivity, mKeyringBytes, mImportFilename); + } + + @Override + public void onLoadFinished(Loader<List<Map<String, String>>> loader, + List<Map<String, String>> data) { + // Set the new data in the adapter. + // for (String item : data) { + // mAdapter.add(item); + // } + Log.d(Constants.TAG, "data: " + data); + // TODO: real swapping the data to the already defined adapter doesn't work + // Workaround: recreate adapter! + // http://stackoverflow.com/questions/2356091/android-add-function-of-arrayadapter-not-working + // mAdapter = new ArrayAdapter<String>(mActivity, android.R.layout.two_line_list_item, + // data); + + String[] from = new String[] { ImportKeysListLoader.MAP_ATTR_USER_ID, + ImportKeysListLoader.MAP_ATTR_FINGERPINT }; + int[] to = new int[] { android.R.id.text1, android.R.id.text2 }; + mAdapter = new SimpleAdapter(getActivity(), data, android.R.layout.two_line_list_item, + from, to); + + mAdapter.notifyDataSetChanged(); + setListAdapter(mAdapter); + + // The list should now be shown. + if (isResumed()) { + setListShown(true); + } else { + setListShownNoAnimation(true); + } + } + + @Override + public void onLoaderReset(Loader<List<Map<String, String>>> loader) { + // Clear the data in the adapter. + // Not available in SimpleAdapter! + // mAdapter.clear(); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java new file mode 100644 index 000000000..a128263c1 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.app.ProgressDialog; +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.widget.Toast; + +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +public class KeyListActivity extends SherlockFragmentActivity { + + protected String mExportFilename = Constants.path.APP_DIR + "/"; + + protected String mImportData; + protected boolean mDeleteAfterImport = false; + + protected int mKeyType; + + FileDialogFragment mFileDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + handleActions(getIntent()); + } + + // TODO: needed? + // @Override + // protected void onNewIntent(Intent intent) { + // super.onNewIntent(intent); + // handleActions(intent); + // } + + protected void handleActions(Intent intent) { + String action = intent.getAction(); + Bundle extras = intent.getExtras(); + + if (extras == null) { + extras = new Bundle(); + } + + /** + * Android Standard Actions + */ + String searchString = null; + if (Intent.ACTION_SEARCH.equals(action)) { + searchString = extras.getString(SearchManager.QUERY); + if (searchString != null && searchString.trim().length() == 0) { + searchString = null; + } + } + + // if (searchString == null) { + // mFilterLayout.setVisibility(View.GONE); + // } else { + // mFilterLayout.setVisibility(View.VISIBLE); + // mFilterInfo.setText(getString(R.string.filterInfo, searchString)); + // } + // + // if (mListAdapter != null) { + // mListAdapter.cleanup(); + // } + // mListAdapter = new KeyListAdapter(this, searchString); + // mList.setAdapter(mListAdapter); + + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case Id.request.filename: { + if (resultCode == RESULT_OK && data != null) { + try { + String path = data.getData().getPath(); + Log.d(Constants.TAG, "path=" + path); + + // set filename used in export/import dialogs + mFileDialog.setFilename(path); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Nullpointer while retrieving path!", e); + } + } + return; + } + + default: { + break; + } + } + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + // TODO: reimplement! + // menu.add(3, Id.menu.option.search, 0, R.string.menu_search) + // .setIcon(R.drawable.ic_menu_search).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + menu.add(0, Id.menu.option.import_from_file, 5, R.string.menu_importFromFile) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + menu.add(0, Id.menu.option.export_keys, 6, R.string.menu_exportKeys).setShowAsAction( + MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + case Id.menu.option.import_from_file: { + Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class); + intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_FILE); + startActivityForResult(intentImportFromFile, 0); + return true; + } + + case Id.menu.option.export_keys: { + showExportKeysDialog(-1); + return true; + } + + // case Id.menu.option.search: + // startSearch("", false, null, false); + // return true; + + default: { + return super.onOptionsItemSelected(item); + } + } + } + + /** + * Show dialog where to export keys + * + * @param keyRingMasterKeyId + * if -1 export all keys + */ + public void showExportKeysDialog(final long keyRingMasterKeyId) { + // Message is received after file is selected + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == FileDialogFragment.MESSAGE_OKAY) { + Bundle data = message.getData(); + mExportFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME); + + exportKeys(keyRingMasterKeyId); + } + } + }; + + // Create a new Messenger for the communication back + final Messenger messenger = new Messenger(returnHandler); + + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + public void run() { + String title = null; + if (keyRingMasterKeyId != -1) { + // single key export + title = getString(R.string.title_exportKey); + } else { + title = getString(R.string.title_exportKeys); + } + + String message = null; + if (mKeyType == Id.type.public_key) { + message = getString(R.string.specifyFileToExportTo); + } else { + message = getString(R.string.specifyFileToExportSecretKeysTo); + } + + mFileDialog = FileDialogFragment.newInstance(messenger, title, message, + mExportFilename, null, Id.request.filename); + + mFileDialog.show(getSupportFragmentManager(), "fileDialog"); + } + }); + } + + /** + * Show dialog to delete key + * + * @param keyRingId + */ + public void showDeleteKeyDialog(long keyRingId) { + // Message is received after key is deleted + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) { + // no further actions needed + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger, + keyRingId, mKeyType); + + deleteKeyDialog.show(getSupportFragmentManager(), "deleteKeyDialog"); + } + + /** + * Export keys + * + * @param keyRingMasterKeyId + * if -1 export all keys + */ + public void exportKeys(long keyRingMasterKeyId) { + Log.d(Constants.TAG, "exportKeys started"); + + // Send all information needed to service to export key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_EXPORT_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + + data.putString(KeychainIntentService.EXPORT_FILENAME, mExportFilename); + data.putInt(KeychainIntentService.EXPORT_KEY_TYPE, mKeyType); + + if (keyRingMasterKeyId == -1) { + data.putBoolean(KeychainIntentService.EXPORT_ALL, true); + } else { + data.putLong(KeychainIntentService.EXPORT_KEY_RING_MASTER_KEY_ID, keyRingMasterKeyId); + } + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after exporting is done in ApgService + KeychainIntentServiceHandler exportHandler = new KeychainIntentServiceHandler(this, + R.string.progress_exporting, ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + // get returned data bundle + Bundle returnData = message.getData(); + + int exported = returnData.getInt(KeychainIntentService.RESULT_EXPORT); + String toastMessage; + if (exported == 1) { + toastMessage = getString(R.string.keyExported); + } else if (exported > 0) { + toastMessage = getString(R.string.keysExported, exported); + } else { + toastMessage = getString(R.string.noKeysExported); + } + Toast.makeText(KeyListActivity.this, toastMessage, Toast.LENGTH_SHORT).show(); + + } + }; + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(exportHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + exportHandler.showProgressDialog(this); + + // start service with intent + startService(intent); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java new file mode 100644 index 000000000..9e90e254c --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.widget.ExpandableListFragment; +import org.sufficientlysecure.keychain.R; + +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View; +import android.widget.ExpandableListView; +import android.widget.ExpandableListView.ExpandableListContextMenuInfo; + +public class KeyListFragment extends ExpandableListFragment { + protected KeyListActivity mKeyListActivity; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mKeyListActivity = (KeyListActivity) getActivity(); + + // register long press context menu + registerForContextMenu(getListView()); + + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText(getString(R.string.listEmpty)); + } + + /** + * Context Menu on Long Click + */ + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + menu.add(0, Id.menu.export, 5, R.string.menu_exportKey); + menu.add(0, Id.menu.delete, 111, R.string.menu_deleteKey); + } + + @Override + public boolean onContextItemSelected(android.view.MenuItem item) { + ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo(); + + // expInfo.id would also return row id of childs, but we always want to get the row id of + // the group item, thus we are using the following way + int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition); + long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition); + + switch (item.getItemId()) { + case Id.menu.export: + long masterKeyId = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId); + if (masterKeyId == -1) { + masterKeyId = ProviderHelper.getSecretMasterKeyId(mKeyListActivity, keyRingRowId); + } + + mKeyListActivity.showExportKeysDialog(masterKeyId); + return true; + + case Id.menu.delete: + mKeyListActivity.showDeleteKeyDialog(keyRingRowId); + return true; + + default: + return super.onContextItemSelected(item); + + } + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java new file mode 100644 index 000000000..adc8656d0 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +import android.content.Intent; +import android.os.Bundle; + +public class KeyListPublicActivity extends KeyListActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mKeyType = Id.type.public_key; + + setContentView(R.layout.key_list_public_activity); + + mExportFilename = Constants.path.APP_DIR + "/pubexport.asc"; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + menu.add(1, Id.menu.option.key_server, 1, R.string.menu_keyServer) + .setIcon(R.drawable.ic_menu_search_list) + .setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS); + menu.add(1, Id.menu.option.import_from_qr_code, 2, R.string.menu_importFromQrCode) + .setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + menu.add(1, Id.menu.option.import_from_nfc, 3, R.string.menu_importFromNfc) + .setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case Id.menu.option.key_server: { + startActivityForResult(new Intent(this, KeyServerQueryActivity.class), 0); + + return true; + } + case Id.menu.option.import_from_file: { + Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class); + intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_FILE); + startActivityForResult(intentImportFromFile, 0); + + return true; + } + + case Id.menu.option.import_from_qr_code: { + Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class); + intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_QR_CODE); + startActivityForResult(intentImportFromFile, Id.request.import_from_qr_code); + + return true; + } + + case Id.menu.option.import_from_nfc: { + Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class); + intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_NFC); + startActivityForResult(intentImportFromFile, 0); + + return true; + } + + default: { + return super.onOptionsItemSelected(item); + } + } + } + + // @Override + // protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // switch (requestCode) { + // case Id.request.look_up_key_id: { + // if (resultCode == RESULT_CANCELED || data == null + // || data.getStringExtra(KeyServerQueryActivity.RESULT_EXTRA_TEXT) == null) { + // return; + // } + // + // Intent intent = new Intent(this, KeyListPublicActivity.class); + // intent.setAction(KeyListPublicActivity.ACTION_IMPORT); + // intent.putExtra(KeyListPublicActivity.EXTRA_TEXT, + // data.getStringExtra(KeyListActivity.EXTRA_TEXT)); + // handleActions(intent); + // break; + // } + // + // default: { + // super.onActivityResult(requestCode, resultCode, data); + // break; + // } + // } + // } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java new file mode 100644 index 000000000..648b09ef7 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.ui.widget.KeyListAdapter; +import org.sufficientlysecure.keychain.R; + +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.app.LoaderManager; +import android.view.ContextMenu; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.ExpandableListView; +import android.widget.ExpandableListView.ExpandableListContextMenuInfo; + +public class KeyListPublicFragment extends KeyListFragment implements + LoaderManager.LoaderCallbacks<Cursor> { + + private KeyListPublicActivity mKeyListPublicActivity; + + private KeyListAdapter mAdapter; + + /** + * Define Adapter and Loader on create of Activity + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mKeyListPublicActivity = (KeyListPublicActivity) getActivity(); + + mAdapter = new KeyListAdapter(mKeyListPublicActivity, null, Id.type.public_key); + setListAdapter(mAdapter); + + // Start out with a progress indicator. + setListShown(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + // id is -1 as the child cursors are numbered 0,...,n + getLoaderManager().initLoader(-1, null, this); + } + + /** + * Context Menu on Long Click + */ + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + menu.add(0, Id.menu.update, 1, R.string.menu_updateKey); + menu.add(0, Id.menu.signKey, 2, R.string.menu_signKey); + menu.add(0, Id.menu.exportToServer, 3, R.string.menu_exportKeyToServer); + menu.add(0, Id.menu.share, 6, R.string.menu_share); + menu.add(0, Id.menu.share_qr_code, 7, R.string.menu_shareQrCode); + menu.add(0, Id.menu.share_nfc, 8, R.string.menu_shareNfc); + + } + + @Override + public boolean onContextItemSelected(android.view.MenuItem item) { + ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo(); + + // expInfo.id would also return row id of childs, but we always want to get the row id of + // the group item, thus we are using the following way + int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition); + long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition); + + switch (item.getItemId()) { + case Id.menu.update: + long updateKeyId = 0; + PGPPublicKeyRing updateKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId( + mKeyListActivity, keyRingRowId); + if (updateKeyRing != null) { + updateKeyId = PgpHelper.getMasterKey(updateKeyRing).getKeyID(); + } + if (updateKeyId == 0) { + // this shouldn't happen + return true; + } + + Intent queryIntent = new Intent(mKeyListActivity, KeyServerQueryActivity.class); + queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID_AND_RETURN); + queryIntent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, updateKeyId); + + // TODO: lookup?? + startActivityForResult(queryIntent, Id.request.look_up_key_id); + + return true; + + case Id.menu.exportToServer: + Intent uploadIntent = new Intent(mKeyListActivity, KeyServerUploadActivity.class); + uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER); + uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, keyRingRowId); + startActivityForResult(uploadIntent, Id.request.export_to_server); + + return true; + + case Id.menu.signKey: + long keyId = 0; + PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId( + mKeyListActivity, keyRingRowId); + if (signKeyRing != null) { + keyId = PgpHelper.getMasterKey(signKeyRing).getKeyID(); + } + if (keyId == 0) { + // this shouldn't happen + return true; + } + + Intent signIntent = new Intent(mKeyListActivity, SignKeyActivity.class); + signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, keyId); + startActivity(signIntent); + + return true; + + case Id.menu.share_qr_code: + // get master key id using row id + long masterKeyId = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId); + + Intent qrCodeIntent = new Intent(mKeyListActivity, ShareActivity.class); + qrCodeIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING_WITH_QR_CODE); + qrCodeIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId); + startActivityForResult(qrCodeIntent, 0); + + return true; + + case Id.menu.share_nfc: + // get master key id using row id + long masterKeyId2 = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId); + + Intent nfcIntent = new Intent(mKeyListActivity, ShareNfcBeamActivity.class); + nfcIntent.setAction(ShareNfcBeamActivity.ACTION_SHARE_KEYRING_WITH_NFC); + nfcIntent.putExtra(ShareNfcBeamActivity.EXTRA_MASTER_KEY_ID, masterKeyId2); + startActivityForResult(nfcIntent, 0); + + return true; + + case Id.menu.share: + // get master key id using row id + long masterKeyId3 = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId); + + Intent shareIntent = new Intent(mKeyListActivity, ShareActivity.class); + shareIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING); + shareIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId3); + startActivityForResult(shareIntent, 0); + + return true; + + default: + return super.onContextItemSelected(item); + + } + } + + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID, + UserIds.USER_ID }; + + static final String SORT_ORDER = UserIds.USER_ID + " ASC"; + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + // This is called when a new Loader needs to be created. This + // sample only has one Loader, so we don't care about the ID. + Uri baseUri = KeyRings.buildPublicKeyRingsUri(); + + // 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, null, null, SORT_ORDER); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.setGroupCursor(data); + + // The list should now be shown. + if (isResumed()) { + setListShown(true); + } else { + setListShownNoAnimation(true); + } + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + // 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. + mAdapter.setGroupCursor(null); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java new file mode 100644 index 000000000..63d06edaf --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpMain; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; + +public class KeyListSecretActivity extends KeyListActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mKeyType = Id.type.secret_key; + + setContentView(R.layout.key_list_secret_activity); + + mExportFilename = Constants.path.APP_DIR + "/secexport.asc"; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + menu.add(1, Id.menu.option.create, 1, R.string.menu_createKey).setShowAsAction( + MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case Id.menu.option.create: { + createKey(); + return true; + } + + default: { + return super.onOptionsItemSelected(item); + } + } + } + + public void checkPassPhraseAndEdit(long keyId) { + String passPhrase = PassphraseCacheService.getCachedPassphrase(this, keyId); + if (passPhrase == null) { + showPassphraseDialog(keyId); + } else { + PgpMain.setEditPassPhrase(passPhrase); + editKey(keyId); + } + } + + private void showPassphraseDialog(final long secretKeyId) { + // Message is received after passphrase is cached + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + String passPhrase = PassphraseCacheService.getCachedPassphrase( + KeyListSecretActivity.this, secretKeyId); + PgpMain.setEditPassPhrase(passPhrase); + editKey(secretKeyId); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + try { + PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance( + KeyListSecretActivity.this, messenger, secretKeyId); + + passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog"); + } catch (PgpMain.PgpGeneralException e) { + Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!"); + // send message to handler to start encryption directly + returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY); + } + } + + private void createKey() { + PgpMain.setEditPassPhrase(""); + Intent intent = new Intent(EditKeyActivity.ACTION_CREATE_KEY); + startActivityForResult(intent, 0); + } + + private void editKey(long keyId) { + Intent intent = new Intent(EditKeyActivity.ACTION_EDIT_KEY); + intent.putExtra(EditKeyActivity.EXTRA_KEY_ID, keyId); + startActivityForResult(intent, 0); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java new file mode 100644 index 000000000..f4ba2b4d9 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import java.util.ArrayList; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.ui.widget.KeyListAdapter; +import org.sufficientlysecure.keychain.R; + +import com.google.zxing.integration.android.IntentIntegrator; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.app.LoaderManager; +import android.view.ContextMenu; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.ExpandableListView; +import android.widget.Toast; +import android.widget.ExpandableListView.ExpandableListContextMenuInfo; + +public class KeyListSecretFragment extends KeyListFragment implements + LoaderManager.LoaderCallbacks<Cursor> { + + private KeyListSecretActivity mKeyListSecretActivity; + + private KeyListAdapter mAdapter; + + /** + * Define Adapter and Loader on create of Activity + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mKeyListSecretActivity = (KeyListSecretActivity) getActivity(); + + mAdapter = new KeyListAdapter(mKeyListSecretActivity, null, Id.type.secret_key); + setListAdapter(mAdapter); + + // Start out with a progress indicator. + setListShown(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + // id is -1 as the child cursors are numbered 0,...,n + getLoaderManager().initLoader(-1, null, this); + } + + /** + * Context Menu on Long Click + */ + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + menu.add(0, Id.menu.edit, 0, R.string.menu_editKey); + } + + @Override + public boolean onContextItemSelected(android.view.MenuItem item) { + ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo(); + + // expInfo.id would also return row id of childs, but we always want to get the row id of + // the group item, thus we are using the following way + int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition); + long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition); + + // get master key id using row id + long masterKeyId = ProviderHelper + .getSecretMasterKeyId(mKeyListSecretActivity, keyRingRowId); + + switch (item.getItemId()) { + case Id.menu.edit: + mKeyListSecretActivity.checkPassPhraseAndEdit(masterKeyId); + + return true; + + default: + return super.onContextItemSelected(item); + + } + } + + + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID, + UserIds.USER_ID }; + + static final String SORT_ORDER = UserIds.USER_ID + " ASC"; + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + // This is called when a new Loader needs to be created. This + // sample only has one Loader, so we don't care about the ID. + Uri baseUri = KeyRings.buildSecretKeyRingsUri(); + + // 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, null, null, SORT_ORDER); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.setGroupCursor(data); + + // The list should now be shown. + if (isResumed()) { + setListShown(true); + } else { + setListShownNoAnimation(true); + } + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + // 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. + mAdapter.setGroupCursor(null); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java new file mode 100644 index 000000000..df8b7b374 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import java.util.ArrayList; +import java.util.List; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.MenuItem; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +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.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +public class KeyServerQueryActivity extends SherlockFragmentActivity { + + // possible intent actions for this activity + public static final String ACTION_LOOK_UP_KEY_ID = Constants.INTENT_PREFIX + "LOOK_UP_KEY_ID"; + public static final String ACTION_LOOK_UP_KEY_ID_AND_RETURN = Constants.INTENT_PREFIX + + "LOOK_UP_KEY_ID_AND_RETURN"; + + public static final String EXTRA_KEY_ID = "keyId"; + + public static final String RESULT_EXTRA_TEXT = "text"; + + private ListView mList; + private EditText mQuery; + private Button mSearch; + private KeyInfoListAdapter mAdapter; + private Spinner mKeyServer; + + private int mQueryType; + private String mQueryString; + private long mQueryId; + private volatile List<KeyInfo> mSearchResult; + private volatile String mKeyData; + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, KeyListPublicActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + default: + break; + + } + return false; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.key_server_query_layout); + + mQuery = (EditText) findViewById(R.id.query); + mSearch = (Button) findViewById(R.id.btn_search); + mList = (ListView) findViewById(R.id.list); + mAdapter = new KeyInfoListAdapter(this); + mList.setAdapter(mAdapter); + + mKeyServer = (Spinner) findViewById(R.id.keyServer); + ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + android.R.layout.simple_spinner_item, Preferences.getPreferences(this) + .getKeyServers()); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mKeyServer.setAdapter(adapter); + if (adapter.getCount() > 0) { + mKeyServer.setSelection(0); + } else { + mSearch.setEnabled(false); + } + + mList.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView<?> adapter, View view, int position, long keyId) { + get(keyId); + } + }); + + mSearch.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + String query = mQuery.getText().toString(); + search(query); + } + }); + + Intent intent = getIntent(); + String action = intent.getAction(); + if (ACTION_LOOK_UP_KEY_ID.equals(action) || ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(action)) { + long keyId = intent.getLongExtra(EXTRA_KEY_ID, 0); + if (keyId != 0) { + String query = "0x" + PgpHelper.keyToHex(keyId); + mQuery.setText(query); + search(query); + } + } + } + + private void search(String query) { + mQueryType = Id.keyserver.search; + mQueryString = query; + mAdapter.setKeys(new ArrayList<KeyInfo>()); + + start(); + } + + private void get(long keyId) { + mQueryType = Id.keyserver.get; + mQueryId = keyId; + + start(); + } + + private void start() { + Log.d(Constants.TAG, "start search with service"); + + // Send all information needed to service to query keys in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_QUERY_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + + String server = (String) mKeyServer.getSelectedItem(); + data.putString(KeychainIntentService.QUERY_KEY_SERVER, server); + + data.putInt(KeychainIntentService.QUERY_KEY_TYPE, mQueryType); + + if (mQueryType == Id.keyserver.search) { + data.putString(KeychainIntentService.QUERY_KEY_STRING, mQueryString); + } else if (mQueryType == Id.keyserver.get) { + data.putLong(KeychainIntentService.QUERY_KEY_ID, mQueryId); + } + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after querying is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + R.string.progress_querying, ProgressDialog.STYLE_SPINNER) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + // get returned data bundle + Bundle returnData = message.getData(); + + if (mQueryType == Id.keyserver.search) { + mSearchResult = returnData + .getParcelableArrayList(KeychainIntentService.RESULT_QUERY_KEY_SEARCH_RESULT); + } else if (mQueryType == Id.keyserver.get) { + mKeyData = returnData.getString(KeychainIntentService.RESULT_QUERY_KEY_KEY_DATA); + } + + // TODO: IMPROVE CODE!!! some global variables can be avoided!!! + if (mQueryType == Id.keyserver.search) { + if (mSearchResult != null) { + Toast.makeText(KeyServerQueryActivity.this, + getString(R.string.keysFound, mSearchResult.size()), + Toast.LENGTH_SHORT).show(); + mAdapter.setKeys(mSearchResult); + } + } else if (mQueryType == Id.keyserver.get) { + Intent orgIntent = getIntent(); + if (ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(orgIntent.getAction())) { + if (mKeyData != null) { + Intent intent = new Intent(); + intent.putExtra(RESULT_EXTRA_TEXT, mKeyData); + setResult(RESULT_OK, intent); + } else { + setResult(RESULT_CANCELED); + } + finish(); + } else { + if (mKeyData != null) { + Intent intent = new Intent(KeyServerQueryActivity.this, + ImportKeysActivity.class); + intent.setAction(ImportKeysActivity.ACTION_IMPORT); + intent.putExtra(ImportKeysActivity.EXTRA_TEXT, mKeyData); + startActivity(intent); + } + } + } + + } + }; + }; + + // 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); + } + + public class KeyInfoListAdapter extends BaseAdapter { + protected LayoutInflater mInflater; + protected Activity mActivity; + protected List<KeyInfo> mKeys; + + public KeyInfoListAdapter(Activity activity) { + mActivity = activity; + mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mKeys = new ArrayList<KeyInfo>(); + } + + public void setKeys(List<KeyInfo> keys) { + mKeys = keys; + notifyDataSetChanged(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + public int getCount() { + return mKeys.size(); + } + + public Object getItem(int position) { + return mKeys.get(position); + } + + public long getItemId(int position) { + return mKeys.get(position).keyId; + } + + public View getView(int position, View convertView, ViewGroup parent) { + KeyInfo keyInfo = mKeys.get(position); + + View view = mInflater.inflate(R.layout.key_server_query_result_item, null); + + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + mainUserId.setText(R.string.unknownUserId); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + mainUserIdRest.setText(""); + TextView keyId = (TextView) view.findViewById(R.id.keyId); + keyId.setText(R.string.noKey); + TextView algorithm = (TextView) view.findViewById(R.id.algorithm); + algorithm.setText(""); + TextView status = (TextView) view.findViewById(R.id.status); + status.setText(""); + + String userId = keyInfo.userIds.get(0); + if (userId != null) { + String chunks[] = userId.split(" <", 2); + userId = chunks[0]; + if (chunks.length > 1) { + mainUserIdRest.setText("<" + chunks[1]); + } + mainUserId.setText(userId); + } + + keyId.setText(PgpHelper.getSmallFingerPrint(keyInfo.keyId)); + + if (mainUserIdRest.getText().length() == 0) { + mainUserIdRest.setVisibility(View.GONE); + } + + algorithm.setText("" + keyInfo.size + "/" + keyInfo.algorithm); + + if (keyInfo.revoked != null) { + status.setText("revoked"); + } else { + status.setVisibility(View.GONE); + } + + LinearLayout ll = (LinearLayout) view.findViewById(R.id.list); + if (keyInfo.userIds.size() == 1) { + ll.setVisibility(View.GONE); + } else { + boolean first = true; + boolean second = true; + for (String uid : keyInfo.userIds) { + if (first) { + first = false; + continue; + } + if (!second) { + View sep = new View(mActivity); + sep.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1)); + sep.setBackgroundResource(android.R.drawable.divider_horizontal_dark); + ll.addView(sep); + } + TextView uidView = (TextView) mInflater.inflate( + R.layout.key_server_query_result_user_id, null); + uidView.setText(uid); + ll.addView(uidView); + second = false; + } + } + + return view; + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java new file mode 100644 index 000000000..657044f2b --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2011 Senecaso + * + * 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; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.MenuItem; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Spinner; +import android.widget.Toast; + +/** + * gpg --send-key activity + * + * Sends the selected public key to a key server + */ +public class KeyServerUploadActivity extends SherlockFragmentActivity { + + // Not used in sourcode, but listed in AndroidManifest! + public static final String ACTION_EXPORT_KEY_TO_SERVER = Constants.INTENT_PREFIX + + "EXPORT_KEY_TO_SERVER"; + + public static final String EXTRA_KEYRING_ROW_ID = "keyId"; + + private Button export; + private Spinner keyServer; + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, KeyListPublicActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + default: + break; + + } + return false; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.key_server_export_layout); + + export = (Button) findViewById(R.id.btn_export_to_server); + keyServer = (Spinner) findViewById(R.id.keyServer); + + ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + android.R.layout.simple_spinner_item, Preferences.getPreferences(this) + .getKeyServers()); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + keyServer.setAdapter(adapter); + if (adapter.getCount() > 0) { + keyServer.setSelection(0); + } else { + export.setEnabled(false); + } + + export.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + uploadKey(); + } + }); + } + + private void uploadKey() { + // Send all information needed to service to upload key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_UPLOAD_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + + int keyRingId = getIntent().getIntExtra(EXTRA_KEYRING_ROW_ID, -1); + data.putInt(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, keyRingId); + + String server = (String) keyServer.getSelectedItem(); + data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after uploading is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, R.string.progress_exporting, + ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + + Toast.makeText(KeyServerUploadActivity.this, R.string.keySendSuccess, + 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); + + // start service with intent + startService(intent); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java new file mode 100644 index 000000000..447801e55 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.SherlockActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +public class MainActivity extends SherlockActivity { + + public void manageKeysOnClick(View view) { + // used instead of startActivity set actionbar based on callingPackage + startActivityForResult(new Intent(this, KeyListPublicActivity.class), 0); + } + + public void myKeysOnClick(View view) { + // used instead of startActivity set actionbar based on callingPackage + startActivityForResult(new Intent(this, KeyListSecretActivity.class), 0); + } + + public void encryptOnClick(View view) { + Intent intent = new Intent(MainActivity.this, EncryptActivity.class); + intent.setAction(EncryptActivity.ACTION_ENCRYPT); + // used instead of startActivity set actionbar based on callingPackage + startActivityForResult(intent, 0); + } + + public void decryptOnClick(View view) { + Intent intent = new Intent(MainActivity.this, DecryptActivity.class); + intent.setAction(DecryptActivity.ACTION_DECRYPT); + // used instead of startActivity set actionbar based on callingPackage + startActivityForResult(intent, 0); + } + + public void scanQrcodeOnClick(View view) { + Intent intent = new Intent(this, ImportKeysActivity.class); + startActivityForResult(intent, 0); + } + + public void helpOnClick(View view) { + startActivity(new Intent(this, HelpActivity.class)); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setHomeButtonEnabled(false); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, Id.menu.option.preferences, 0, R.string.menu_preferences) + .setIcon(R.drawable.ic_menu_settings) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case Id.menu.option.preferences: + startActivity(new Intent(this, PreferencesActivity.class)); + return true; + + default: + break; + + } + return false; + } + +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java new file mode 100644 index 000000000..3c070c6db --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.SherlockPreferenceActivity; +import com.actionbarsherlock.view.MenuItem; + +import android.content.Intent; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceScreen; + +public class PreferencesActivity extends SherlockPreferenceActivity { + private IntegerListPreference mPassPhraseCacheTtl = null; + private IntegerListPreference mEncryptionAlgorithm = null; + private IntegerListPreference mHashAlgorithm = null; + private IntegerListPreference mMessageCompression = null; + private IntegerListPreference mFileCompression = null; + private CheckBoxPreference mAsciiArmour = null; + private CheckBoxPreference mForceV3Signatures = null; + private PreferenceScreen mKeyServerPreference = null; + private Preferences mPreferences; + + @Override + protected void onCreate(Bundle savedInstanceState) { + mPreferences = Preferences.getPreferences(this); + super.onCreate(savedInstanceState); + + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + + addPreferencesFromResource(R.xml.preferences); + + mPassPhraseCacheTtl = (IntegerListPreference) findPreference(Constants.pref.PASS_PHRASE_CACHE_TTL); + mPassPhraseCacheTtl.setValue("" + mPreferences.getPassPhraseCacheTtl()); + mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry()); + mPassPhraseCacheTtl + .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mPassPhraseCacheTtl.setValue(newValue.toString()); + mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry()); + mPreferences.setPassPhraseCacheTtl(Integer.parseInt(newValue.toString())); + return false; + } + }); + + mEncryptionAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM); + int valueIds[] = { PGPEncryptedData.AES_128, PGPEncryptedData.AES_192, + PGPEncryptedData.AES_256, PGPEncryptedData.BLOWFISH, PGPEncryptedData.TWOFISH, + PGPEncryptedData.CAST5, PGPEncryptedData.DES, PGPEncryptedData.TRIPLE_DES, + PGPEncryptedData.IDEA, }; + String entries[] = { "AES-128", "AES-192", "AES-256", "Blowfish", "Twofish", "CAST5", + "DES", "Triple DES", "IDEA", }; + String values[] = new String[valueIds.length]; + for (int i = 0; i < values.length; ++i) { + values[i] = "" + valueIds[i]; + } + mEncryptionAlgorithm.setEntries(entries); + mEncryptionAlgorithm.setEntryValues(values); + mEncryptionAlgorithm.setValue("" + mPreferences.getDefaultEncryptionAlgorithm()); + mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry()); + mEncryptionAlgorithm + .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mEncryptionAlgorithm.setValue(newValue.toString()); + mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry()); + mPreferences.setDefaultEncryptionAlgorithm(Integer.parseInt(newValue + .toString())); + return false; + } + }); + + mHashAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_HASH_ALGORITHM); + valueIds = new int[] { HashAlgorithmTags.MD5, HashAlgorithmTags.RIPEMD160, + HashAlgorithmTags.SHA1, HashAlgorithmTags.SHA224, HashAlgorithmTags.SHA256, + HashAlgorithmTags.SHA384, HashAlgorithmTags.SHA512, }; + entries = new String[] { "MD5", "RIPEMD-160", "SHA-1", "SHA-224", "SHA-256", "SHA-384", + "SHA-512", }; + values = new String[valueIds.length]; + for (int i = 0; i < values.length; ++i) { + values[i] = "" + valueIds[i]; + } + mHashAlgorithm.setEntries(entries); + mHashAlgorithm.setEntryValues(values); + mHashAlgorithm.setValue("" + mPreferences.getDefaultHashAlgorithm()); + mHashAlgorithm.setSummary(mHashAlgorithm.getEntry()); + mHashAlgorithm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mHashAlgorithm.setValue(newValue.toString()); + mHashAlgorithm.setSummary(mHashAlgorithm.getEntry()); + mPreferences.setDefaultHashAlgorithm(Integer.parseInt(newValue.toString())); + return false; + } + }); + + mMessageCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_MESSAGE_COMPRESSION); + valueIds = new int[] { Id.choice.compression.none, Id.choice.compression.zip, + Id.choice.compression.zlib, Id.choice.compression.bzip2, }; + entries = new String[] { + getString(R.string.choice_none) + " (" + getString(R.string.fast) + ")", + "ZIP (" + getString(R.string.fast) + ")", + "ZLIB (" + getString(R.string.fast) + ")", + "BZIP2 (" + getString(R.string.very_slow) + ")", }; + values = new String[valueIds.length]; + for (int i = 0; i < values.length; ++i) { + values[i] = "" + valueIds[i]; + } + mMessageCompression.setEntries(entries); + mMessageCompression.setEntryValues(values); + mMessageCompression.setValue("" + mPreferences.getDefaultMessageCompression()); + mMessageCompression.setSummary(mMessageCompression.getEntry()); + mMessageCompression + .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mMessageCompression.setValue(newValue.toString()); + mMessageCompression.setSummary(mMessageCompression.getEntry()); + mPreferences.setDefaultMessageCompression(Integer.parseInt(newValue + .toString())); + return false; + } + }); + + mFileCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_FILE_COMPRESSION); + mFileCompression.setEntries(entries); + mFileCompression.setEntryValues(values); + mFileCompression.setValue("" + mPreferences.getDefaultFileCompression()); + mFileCompression.setSummary(mFileCompression.getEntry()); + mFileCompression.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mFileCompression.setValue(newValue.toString()); + mFileCompression.setSummary(mFileCompression.getEntry()); + mPreferences.setDefaultFileCompression(Integer.parseInt(newValue.toString())); + return false; + } + }); + + mAsciiArmour = (CheckBoxPreference) findPreference(Constants.pref.DEFAULT_ASCII_ARMOUR); + mAsciiArmour.setChecked(mPreferences.getDefaultAsciiArmour()); + mAsciiArmour.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mAsciiArmour.setChecked((Boolean) newValue); + mPreferences.setDefaultAsciiArmour((Boolean) newValue); + return false; + } + }); + + mForceV3Signatures = (CheckBoxPreference) findPreference(Constants.pref.FORCE_V3_SIGNATURES); + mForceV3Signatures.setChecked(mPreferences.getForceV3Signatures()); + mForceV3Signatures + .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mForceV3Signatures.setChecked((Boolean) newValue); + mPreferences.setForceV3Signatures((Boolean) newValue); + return false; + } + }); + + mKeyServerPreference = (PreferenceScreen) findPreference(Constants.pref.KEY_SERVERS); + String servers[] = mPreferences.getKeyServers(); + mKeyServerPreference.setSummary(getResources().getString(R.string.nKeyServers, + servers.length)); + mKeyServerPreference + .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(PreferencesActivity.this, + PreferencesKeyServerActivity.class); + intent.putExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS, + mPreferences.getKeyServers()); + startActivityForResult(intent, Id.request.key_server_preference); + return false; + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case Id.request.key_server_preference: { + if (resultCode == RESULT_CANCELED || data == null) { + return; + } + String servers[] = data + .getStringArrayExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS); + mPreferences.setKeyServers(servers); + mKeyServerPreference.setSummary(getResources().getString(R.string.nKeyServers, + servers.length)); + break; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + break; + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + default: + break; + + } + return false; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java new file mode 100644 index 000000000..b86fe0a2e --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import java.util.Vector; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.ui.widget.Editor; +import org.sufficientlysecure.keychain.ui.widget.KeyServerEditor; +import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.SherlockActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; + +public class PreferencesKeyServerActivity extends SherlockActivity implements OnClickListener, + EditorListener { + + public static final String EXTRA_KEY_SERVERS = "keyServers"; + + private LayoutInflater mInflater; + private ViewGroup mEditors; + private View mAdd; + private TextView mTitle; + private TextView mSummary; + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, PreferencesActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + + return true; + + case Id.menu.option.okay: + okClicked(); + + return true; + + case Id.menu.option.cancel: + cancelClicked(); + + return true; + + default: + break; + + } + return false; + } + + /** + * ActionBar menu is created based on class variables to change it at runtime + * + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + + menu.add(1, Id.menu.option.cancel, 0, android.R.string.cancel).setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + + menu.add(1, Id.menu.option.okay, 1, android.R.string.ok).setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + + return true; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.key_server_preference); + + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + + mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + mTitle = (TextView) findViewById(R.id.title); + mSummary = (TextView) findViewById(R.id.summary); + + mTitle.setText(R.string.label_keyServers); + + mEditors = (ViewGroup) findViewById(R.id.editors); + mAdd = findViewById(R.id.add); + mAdd.setOnClickListener(this); + + Intent intent = getIntent(); + String servers[] = intent.getStringArrayExtra(EXTRA_KEY_SERVERS); + if (servers != null) { + for (int i = 0; i < servers.length; ++i) { + KeyServerEditor view = (KeyServerEditor) mInflater.inflate( + R.layout.key_server_editor, mEditors, false); + view.setEditorListener(this); + view.setValue(servers[i]); + mEditors.addView(view); + } + } + } + + public void onDeleted(Editor editor) { + // nothing to do + } + + public void onClick(View v) { + KeyServerEditor view = (KeyServerEditor) mInflater.inflate(R.layout.key_server_editor, + mEditors, false); + view.setEditorListener(this); + mEditors.addView(view); + } + + private void cancelClicked() { + setResult(RESULT_CANCELED, null); + finish(); + } + + private void okClicked() { + Intent data = new Intent(); + Vector<String> servers = new Vector<String>(); + 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(); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java new file mode 100644 index 000000000..fc1cfba17 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +import android.content.Intent; +import android.os.Bundle; + +public class SelectPublicKeyActivity extends SherlockFragmentActivity { + + // Not used in sourcode, but listed in AndroidManifest! + public static final String ACTION_SELECT_PUBLIC_KEYS = Constants.INTENT_PREFIX + + "SELECT_PUBLIC_KEYS"; + + public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "masterKeyIds"; + + public static final String RESULT_EXTRA_MASTER_KEY_IDS = "masterKeyIds"; + public static final String RESULT_EXTRA_USER_IDS = "userIds"; + + SelectPublicKeyFragment mSelectFragment; + + long selectedMasterKeyIds[]; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.select_public_key_activity); + + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setHomeButtonEnabled(false); + + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + + mSelectFragment = (SelectPublicKeyFragment) getSupportFragmentManager().findFragmentById( + R.id.select_public_key_fragment); + + // TODO: reimplement! + // mFilterLayout = findViewById(R.id.layout_filter); + // mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo); + // mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear); + // + // mClearFilterButton.setOnClickListener(new OnClickListener() { + // public void onClick(View v) { + // handleIntent(new Intent()); + // } + // }); + + handleIntent(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); + } + + private void handleIntent(Intent intent) { + // TODO: reimplement! + + // String searchString = null; + // if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + // searchString = intent.getStringExtra(SearchManager.QUERY); + // if (searchString != null && searchString.trim().length() == 0) { + // searchString = null; + // } + // } + + // if (searchString == null) { + // mFilterLayout.setVisibility(View.GONE); + // } else { + // mFilterLayout.setVisibility(View.VISIBLE); + // mFilterInfo.setText(getString(R.string.filterInfo, searchString)); + // } + + // preselected master keys + selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS); + } + + /** + * returns preselected key ids, this is used in the fragment + * + * @return + */ + public long[] getSelectedMasterKeyIds() { + return selectedMasterKeyIds; + } + + private void cancelClicked() { + setResult(RESULT_CANCELED, null); + finish(); + } + + private void okClicked() { + Intent data = new Intent(); + data.putExtra(RESULT_EXTRA_MASTER_KEY_IDS, mSelectFragment.getSelectedMasterKeyIds()); + data.putExtra(RESULT_EXTRA_USER_IDS, mSelectFragment.getSelectedUserIds()); + setResult(RESULT_OK, data); + finish(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // TODO: reimplement! + // menu.add(0, Id.menu.option.search, 0, R.string.menu_search).setIcon( + // android.R.drawable.ic_menu_search); + menu.add(1, Id.menu.option.cancel, 0, R.string.btn_doNotSave).setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + menu.add(1, Id.menu.option.okay, 1, R.string.btn_okay).setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + return true; + } + + /** + * Menu Options + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + case Id.menu.option.okay: + okClicked(); + return true; + + case Id.menu.option.cancel: + cancelClicked(); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java new file mode 100644 index 000000000..8b3e75d05 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import java.util.Date; +import java.util.Vector; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.ui.widget.SelectKeyCursorAdapter; +import org.sufficientlysecure.keychain.R; + +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.app.LoaderManager; +import android.widget.ListView; + +public class SelectPublicKeyFragment extends ListFragmentWorkaround implements + LoaderManager.LoaderCallbacks<Cursor> { + + private SelectPublicKeyActivity mActivity; + private SelectKeyCursorAdapter mAdapter; + private ListView mListView; + + private long mSelectedMasterKeyIds[]; + + /** + * Define Adapter and Loader on create of Activity + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mActivity = (SelectPublicKeyActivity) getSherlockActivity(); + mListView = getListView(); + + // get selected master key ids, which are given to activity by intent + mSelectedMasterKeyIds = mActivity.getSelectedMasterKeyIds(); + + mListView.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.listEmpty)); + + mAdapter = new SelectKeyCursorAdapter(mActivity, mListView, null, Id.type.public_key); + + setListAdapter(mAdapter); + + // Start out with a progress indicator. + setListShown(false); + + // 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(long[] masterKeyIds) { + if (masterKeyIds != null) { + for (int i = 0; i < mListView.getCount(); ++i) { + long keyId = mAdapter.getMasterKeyId(i); + for (int j = 0; j < masterKeyIds.length; ++j) { + if (keyId == masterKeyIds[j]) { + mListView.setItemChecked(i, true); + break; + } + } + } + } + } + + /** + * Returns all selected master key ids + * + * @return + */ + public long[] getSelectedMasterKeyIds() { + // mListView.getCheckedItemIds() would give the row ids of the KeyRings not the master key + // ids! + Vector<Long> vector = new Vector<Long>(); + for (int i = 0; i < mListView.getCount(); ++i) { + if (mListView.isItemChecked(i)) { + vector.add(mAdapter.getMasterKeyId(i)); + } + } + + // convert to long array + long[] selectedMasterKeyIds = new long[vector.size()]; + for (int i = 0; i < vector.size(); ++i) { + selectedMasterKeyIds[i] = vector.get(i); + } + + return selectedMasterKeyIds; + } + + /** + * Returns all selected user ids + * + * @return + */ + public String[] getSelectedUserIds() { + Vector<String> userIds = new Vector<String>(); + for (int i = 0; i < mListView.getCount(); ++i) { + if (mListView.isItemChecked(i)) { + userIds.add((String) mAdapter.getUserId(i)); + } + } + + // make empty array to not return null + String userIdArray[] = new String[0]; + return userIds.toArray(userIdArray); + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + // This is called when a new Loader needs to be created. This + // sample only has one Loader, so we don't care about the ID. + Uri baseUri = KeyRings.buildPublicKeyRingsUri(); + + // These are the rows that we will retrieve. + long now = new Date().getTime() / 1000; + String[] projection = new String[] { + KeyRings._ID, + KeyRings.MASTER_KEY_ID, + UserIds.USER_ID, + "(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS + + " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = " + + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + + " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys." + + Keys.CAN_ENCRYPT + " = '1') AS " + + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE, + "(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS + + " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = " + + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND valid_keys." + + Keys.IS_REVOKED + " = '0' AND valid_keys." + Keys.CAN_ENCRYPT + + " = '1' AND valid_keys." + Keys.CREATION + " <= '" + now + "' AND " + + "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys." + Keys.EXPIRY + + " >= '" + now + "')) AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, }; + + String inMasterKeyList = null; + if (mSelectedMasterKeyIds != null && mSelectedMasterKeyIds.length > 0) { + inMasterKeyList = KeyRings.MASTER_KEY_ID + " IN ("; + for (int i = 0; i < mSelectedMasterKeyIds.length; ++i) { + if (i != 0) { + inMasterKeyList += ", "; + } + inMasterKeyList += DatabaseUtils.sqlEscapeString("" + mSelectedMasterKeyIds[i]); + } + inMasterKeyList += ")"; + } + + // if (searchString != null && searchString.trim().length() > 0) { + // String[] chunks = searchString.trim().split(" +"); + // qb.appendWhere("(EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME + // + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME + "." + // + Keys._ID); + // for (int i = 0; i < chunks.length; ++i) { + // qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE "); + // qb.appendWhereEscapeString("%" + chunks[i] + "%"); + // } + // qb.appendWhere("))"); + // + // if (inIdList != null) { + // qb.appendWhere(" OR (" + inIdList + ")"); + // } + // } + + String orderBy = UserIds.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, null, null, orderBy); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.swapCursor(data); + + // The list should now be shown. + if (isResumed()) { + setListShown(true); + } else { + setListShownNoAnimation(true); + } + + // preselect given master keys + preselectMasterKeyIds(mSelectedMasterKeyIds); + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + // 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. + mAdapter.swapCursor(null); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java new file mode 100644 index 000000000..6f43cbabc --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +import android.content.Intent; +import android.os.Bundle; + +public class SelectSecretKeyActivity extends SherlockFragmentActivity { + + // Not used in sourcode, but listed in AndroidManifest! + public static final String ACTION_SELECT_SECRET_KEY = Constants.INTENT_PREFIX + + "SELECT_SECRET_KEY"; + + public static final String RESULT_EXTRA_MASTER_KEY_ID = "masterKeyId"; + public static final String RESULT_EXTRA_USER_ID = "userId"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.select_secret_key_activity); + + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setHomeButtonEnabled(false); + + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + + // TODO: reimplement! + // mFilterLayout = findViewById(R.id.layout_filter); + // mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo); + // mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear); + // + // mClearFilterButton.setOnClickListener(new OnClickListener() { + // public void onClick(View v) { + // handleIntent(new Intent()); + // } + // }); + + handleIntent(getIntent()); + } + + /** + * This is executed by SelectSecretKeyFragment after clicking on an item + * + * @param masterKeyId + * @param userId + */ + public void afterListSelection(long masterKeyId, String userId) { + Intent data = new Intent(); + data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, masterKeyId); + data.putExtra(RESULT_EXTRA_USER_ID, (String) userId); + setResult(RESULT_OK, data); + finish(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); + } + + private void handleIntent(Intent intent) { + // TODO: reimplement! + + // String searchString = null; + // if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + // searchString = intent.getStringExtra(SearchManager.QUERY); + // if (searchString != null && searchString.trim().length() == 0) { + // searchString = null; + // } + // } + + // if (searchString == null) { + // mFilterLayout.setVisibility(View.GONE); + // } else { + // mFilterLayout.setVisibility(View.VISIBLE); + // mFilterInfo.setText(getString(R.string.filterInfo, searchString)); + // } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // TODO: reimplement! + // menu.add(0, Id.menu.option.search, 0, R.string.menu_search).setIcon( + // android.R.drawable.ic_menu_search); + return true; + } + + /** + * Menu Options + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // app icon in Action Bar clicked; go home + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java new file mode 100644 index 000000000..d7d9fb0af --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + +import java.util.Date; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.ui.widget.SelectKeyCursorAdapter; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.SherlockListFragment; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.app.LoaderManager; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; + +public class SelectSecretKeyFragment extends SherlockListFragment implements + LoaderManager.LoaderCallbacks<Cursor> { + + private SelectSecretKeyActivity mActivity; + private SelectKeyCursorAdapter mAdapter; + private ListView mListView; + + /** + * Define Adapter and Loader on create of Activity + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mActivity = (SelectSecretKeyActivity) getSherlockActivity(); + mListView = getListView(); + + mListView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { + long masterKeyId = mAdapter.getMasterKeyId(position); + String userId = mAdapter.getUserId(position); + + // return data to activity, which results in finishing it + mActivity.afterListSelection(masterKeyId, userId); + } + }); + + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText(getString(R.string.listEmpty)); + + mAdapter = new SelectKeyCursorAdapter(mActivity, mListView, null, Id.type.secret_key); + + setListAdapter(mAdapter); + + // Start out with a progress indicator. + setListShown(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + // This is called when a new Loader needs to be created. This + // sample only has one Loader, so we don't care about the ID. + Uri baseUri = KeyRings.buildPublicKeyRingsUri(); + + // These are the rows that we will retrieve. + long now = new Date().getTime() / 1000; + String[] projection = new String[] { + KeyRings._ID, + KeyRings.MASTER_KEY_ID, + UserIds.USER_ID, + "(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS + + " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = " + + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + + " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys." + + Keys.CAN_SIGN + " = '1') AS " + + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE, + "(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS + + " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = " + + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND valid_keys." + + Keys.IS_REVOKED + " = '0' AND valid_keys." + Keys.CAN_SIGN + + " = '1' AND valid_keys." + Keys.CREATION + " <= '" + now + "' AND " + + "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys." + Keys.EXPIRY + + " >= '" + now + "')) AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, }; + + // if (searchString != null && searchString.trim().length() > 0) { + // String[] chunks = searchString.trim().split(" +"); + // qb.appendWhere("EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME + // + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME + "." + // + Keys._ID); + // for (int i = 0; i < chunks.length; ++i) { + // qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE "); + // qb.appendWhereEscapeString("%" + chunks[i] + "%"); + // } + // qb.appendWhere(")"); + // } + + String orderBy = UserIds.USER_ID + " ASC"; + + // 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, null, null, orderBy); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.swapCursor(data); + + // The list should now be shown. + if (isResumed()) { + setListShown(true); + } else { + setListShownNoAnimation(true); + } + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + // 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. + mAdapter.swapCursor(null); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java new file mode 100644 index 000000000..29960748b --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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; + +import java.util.ArrayList; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.google.zxing.integration.android.IntentIntegrator; + +import android.content.Intent; +import android.os.Bundle; + +public class ShareActivity extends SherlockFragmentActivity { + public static final String ACTION_SHARE_KEYRING = Constants.INTENT_PREFIX + "SHARE_KEYRING"; + public static final String ACTION_SHARE_KEYRING_WITH_QR_CODE = Constants.INTENT_PREFIX + + "SHARE_KEYRING_WITH_QR_CODE"; + + public static final String EXTRA_MASTER_KEY_ID = "masterKeyId"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + handleActions(getIntent()); + } + + protected void handleActions(Intent intent) { + String action = intent.getAction(); + Bundle extras = intent.getExtras(); + + if (extras == null) { + extras = new Bundle(); + } + + long masterKeyId = extras.getLong(EXTRA_MASTER_KEY_ID); + + // get public keyring as ascii armored string + ArrayList<String> keyringArmored = ProviderHelper.getPublicKeyRingsAsArmoredString(this, + new long[] { masterKeyId }); + + // close this activity + finish(); + + if (ACTION_SHARE_KEYRING.equals(action)) { + // let user choose application + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, keyringArmored.get(0)); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, + getResources().getText(R.string.shareKeyringWith))); + } else if (ACTION_SHARE_KEYRING_WITH_QR_CODE.equals(action)) { + // use barcode scanner integration library + new IntentIntegrator(this).shareText(keyringArmored.get(0)); + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java new file mode 100644 index 000000000..c106614d3 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2011 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; + +import java.io.IOException; +import java.io.InputStream; + +import net.nightwhistler.htmlspanner.HtmlSpanner; +import net.nightwhistler.htmlspanner.JellyBeanSpanFixTextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.NfcAdapter.CreateNdefMessageCallback; +import android.nfc.NfcEvent; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; +import android.widget.Toast; +import android.nfc.NfcAdapter.OnNdefPushCompleteCallback; +import android.os.Handler; +import android.os.Message; +import android.provider.Settings; +import android.text.method.LinkMovementMethod; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public class ShareNfcBeamActivity extends SherlockFragmentActivity implements + CreateNdefMessageCallback, OnNdefPushCompleteCallback { + public static final String ACTION_SHARE_KEYRING_WITH_NFC = Constants.INTENT_PREFIX + + "SHARE_KEYRING_WITH_NFC"; + + public static final String EXTRA_MASTER_KEY_ID = "masterKeyId"; + + NfcAdapter mNfcAdapter; + + byte[] mSharedKeyringBytes; + + private static final int MESSAGE_SENT = 1; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + Toast.makeText(this, + getString(R.string.error) + ": " + getString(R.string.error_jellyBeanNeeded), + Toast.LENGTH_LONG).show(); + finish(); + } else { + // Check for available NFC Adapter + mNfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (mNfcAdapter == null) { + Toast.makeText(this, + getString(R.string.error) + ": " + getString(R.string.error_nfcNeeded), + Toast.LENGTH_LONG).show(); + finish(); + } else { + // handle actions after verifying that nfc works... + handleActions(getIntent()); + } + } + } + + protected void handleActions(Intent intent) { + String action = intent.getAction(); + Bundle extras = intent.getExtras(); + + if (extras == null) { + extras = new Bundle(); + } + + if (ACTION_SHARE_KEYRING_WITH_NFC.equals(action)) { + long masterKeyId = extras.getLong(EXTRA_MASTER_KEY_ID); + + // get public keyring as byte array + mSharedKeyringBytes = ProviderHelper.getPublicKeyRingsAsByteArray(this, + new long[] { masterKeyId }); + + // Register callback to set NDEF message + mNfcAdapter.setNdefPushMessageCallback(this, this); + // Register callback to listen for message-sent success + mNfcAdapter.setOnNdefPushCompleteCallback(this, this); + } + } + + /** + * Parses the NDEF Message from the intent and prints to the TextView + */ + void handleActionNdefDiscovered(Intent intent) { + Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); + // 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(); + + Intent importIntent = new Intent(this, ImportKeysActivity.class); + importIntent.setAction(ImportKeysActivity.ACTION_IMPORT); + importIntent.putExtra(ImportKeysActivity.EXTRA_KEYRING_BYTES, receivedKeyringBytes); + + finish(); + + startActivity(importIntent); + } + + private void buildView() { + // load html from html file from /res/raw + InputStream inputStreamText = getResources().openRawResource(R.raw.nfc_beam_share); + + setContentView(R.layout.share_nfc_beam); + + JellyBeanSpanFixTextView text = (JellyBeanSpanFixTextView) findViewById(R.id.nfc_beam_text); + + // load html into textview + HtmlSpanner htmlSpanner = new HtmlSpanner(); + htmlSpanner.setStripExtraWhiteSpace(true); + try { + text.setText(htmlSpanner.fromHtml(inputStreamText)); + } catch (IOException e) { + Log.e(Constants.TAG, "Error while reading raw resources as stream", e); + } + + // make links work + text.setMovementMethod(LinkMovementMethod.getInstance()); + + // no flickering when clicking textview for Android < 4 + text.setTextColor(getResources().getColor(android.R.color.black)); + + // set actionbar without home button if called from another app + OtherHelper.setActionBarBackButton(this); + } + + /** + * Implementation for the CreateNdefMessageCallback interface + */ + @Override + public NdefMessage createNdefMessage(NfcEvent event) { + /** + * When a device receives a push with an AAR in it, the application specified in the AAR is + * guaranteed to run. The AAR overrides the tag dispatch system. You can add it back in to + * guarantee that this activity starts when receiving a beamed message. For now, this code + * uses the tag dispatch system. + */ + NdefMessage msg = new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME, + mSharedKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME)); + return msg; + } + + /** + * Implementation for the OnNdefPushCompleteCallback interface + */ + @Override + public void onNdefPushComplete(NfcEvent arg0) { + // A handler is needed to send messages to the activity when this + // callback occurs, because it happens from a binder thread + mHandler.obtainMessage(MESSAGE_SENT).sendToTarget(); + } + + /** This handler receives a message from onNdefPushComplete */ + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_SENT: + Toast.makeText(getApplicationContext(), R.string.nfcSuccessfull, Toast.LENGTH_LONG) + .show(); + break; + } + } + }; + + @Override + public void onResume() { + super.onResume(); + // Check to see that the Activity started due to an Android Beam + if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) { + handleActionNdefDiscovered(getIntent()); + } else { + // build view only when sending nfc, not when receiving, as it gets directly into Import + // activity on receiving + buildView(); + } + } + + @Override + public void onNewIntent(Intent intent) { + // onResume gets called after this to handle the intent + setIntent(intent); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getSupportMenuInflater(); + inflater.inflate(R.menu.nfc_beam, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + // app icon in Action Bar clicked; go to KeyListPublicActivity + Intent intent = new Intent(this, KeyListPublicActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + + case R.id.menu_settings: + Intent intentSettings = new Intent(Settings.ACTION_NFCSHARING_SETTINGS); + startActivity(intentSettings); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java new file mode 100644 index 000000000..21d55aca0 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2011 Senecaso + * + * 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; + +import java.util.Iterator; + +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpMain; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.SherlockFragmentActivity; + +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.view.View; +import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.Spinner; +import android.widget.Toast; + +/** + * gpg --sign-key + * + * signs the specified public key with the specified secret master key + */ +public class SignKeyActivity extends SherlockFragmentActivity { + + public static final String EXTRA_KEY_ID = "keyId"; + + private long mPubKeyId = 0; + private long mMasterKeyId = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // check we havent already signed it + setContentView(R.layout.sign_key_layout); + + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setHomeButtonEnabled(false); + + final Spinner keyServer = (Spinner) findViewById(R.id.keyServer); + ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + android.R.layout.simple_spinner_item, Preferences.getPreferences(this) + .getKeyServers()); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + keyServer.setAdapter(adapter); + + final CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey); + if (!sendKey.isChecked()) { + keyServer.setEnabled(false); + } else { + keyServer.setEnabled(true); + } + + sendKey.setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (!isChecked) { + keyServer.setEnabled(false); + } else { + keyServer.setEnabled(true); + } + } + }); + + Button sign = (Button) findViewById(R.id.sign); + sign.setEnabled(false); // disabled until the user selects a key to sign with + sign.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mPubKeyId != 0) { + initiateSigning(); + } + } + }); + + mPubKeyId = getIntent().getLongExtra(EXTRA_KEY_ID, 0); + if (mPubKeyId == 0) { + finish(); // nothing to do if we dont know what key to sign + } else { + // kick off the SecretKey selection activity so the user chooses which key to sign with + // first + Intent intent = new Intent(this, SelectSecretKeyActivity.class); + startActivityForResult(intent, Id.request.secret_keys); + } + } + + private void showPassphraseDialog(final long secretKeyId) { + // Message is received after passphrase is cached + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + startSigning(); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + try { + PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this, + messenger, secretKeyId); + + passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog"); + } catch (PgpMain.PgpGeneralException e) { + Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!"); + // send message to handler to start encryption directly + returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY); + } + } + + /** + * handles the UI bits of the signing process on the UI thread + */ + private void initiateSigning() { + PGPPublicKeyRing pubring = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(this, mPubKeyId); + if (pubring != null) { + // if we have already signed this key, dont bother doing it again + boolean alreadySigned = false; + + @SuppressWarnings("unchecked") + Iterator<PGPSignature> itr = pubring.getPublicKey(mPubKeyId).getSignatures(); + while (itr.hasNext()) { + PGPSignature sig = itr.next(); + if (sig.getKeyID() == mMasterKeyId) { + alreadySigned = true; + break; + } + } + + if (!alreadySigned) { + /* + * get the user's passphrase for this key (if required) + */ + String passphrase = PassphraseCacheService.getCachedPassphrase(this, mMasterKeyId); + if (passphrase == null) { + showPassphraseDialog(mMasterKeyId); + return; // bail out; need to wait until the user has entered the passphrase + // before trying again + } else { + startSigning(); + } + } else { + Toast.makeText(this, "Key has already been signed", Toast.LENGTH_SHORT).show(); + + setResult(RESULT_CANCELED); + finish(); + } + } + } + + /** + * kicks off the actual signing process on a background thread + */ + private void startSigning() { + // Send all information needed to service to sign key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_SIGN_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + + data.putLong(KeychainIntentService.SIGN_KEY_MASTER_KEY_ID, mMasterKeyId); + data.putLong(KeychainIntentService.SIGN_KEY_PUB_KEY_ID, mPubKeyId); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after signing is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, R.string.progress_signing, + ProgressDialog.STYLE_SPINNER) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + + Toast.makeText(SignKeyActivity.this, R.string.keySignSuccess, + Toast.LENGTH_SHORT).show(); + + // check if we need to send the key to the server or not + CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey); + if (sendKey.isChecked()) { + /* + * upload the newly signed key to the key server + */ + uploadKey(); + } else { + 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); + + // start service with intent + startService(intent); + } + + private void uploadKey() { + // Send all information needed to service to upload key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_UPLOAD_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + + data.putLong(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, mPubKeyId); + + Spinner keyServer = (Spinner) findViewById(R.id.keyServer); + String server = (String) keyServer.getSelectedItem(); + data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after uploading is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, R.string.progress_exporting, + ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + + Toast.makeText(SignKeyActivity.this, R.string.keySendSuccess, + 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); + + // start service with intent + startService(intent); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case Id.request.secret_keys: { + if (resultCode == RESULT_OK) { + mMasterKeyId = data.getLongExtra(EXTRA_KEY_ID, 0); + + // re-enable the sign button so the user can initiate the sign process + Button sign = (Button) findViewById(R.id.sign); + sign.setEnabled(true); + } + + break; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + } + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java new file mode 100644 index 000000000..defc3bc6c --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.dialog; + +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.R; + +import android.app.AlertDialog; +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.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.widget.Toast; + +public class DeleteFileDialogFragment extends DialogFragment { + private static final String ARG_DELETE_FILE = "delete_file"; + + /** + * Creates new instance of this delete file dialog fragment + */ + public static DeleteFileDialogFragment newInstance(String deleteFile) { + DeleteFileDialogFragment frag = new DeleteFileDialogFragment(); + Bundle args = new Bundle(); + + args.putString(ARG_DELETE_FILE, deleteFile); + + frag.setArguments(args); + + return frag; + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final FragmentActivity activity = getActivity(); + + final String deleteFile = getArguments().getString(ARG_DELETE_FILE); + + AlertDialog.Builder alert = new AlertDialog.Builder(activity); + + alert.setIcon(android.R.drawable.ic_dialog_alert); + alert.setTitle(R.string.warning); + alert.setMessage(this.getString(R.string.fileDeleteConfirmation, deleteFile)); + + alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + + // Send all information needed to service to edit key in other thread + Intent intent = new Intent(activity, KeychainIntentService.class); + + // fill values for this action + Bundle data = new Bundle(); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_DELETE_FILE_SECURELY); + data.putString(KeychainIntentService.DELETE_FILE, deleteFile); + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + ProgressDialogFragment deletingDialog = ProgressDialogFragment.newInstance( + R.string.progress_deletingSecurely, ProgressDialog.STYLE_HORIZONTAL); + + // Message is received after deleting is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(activity, deletingDialog) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + Toast.makeText(activity, R.string.fileDeleteSuccessful, + Toast.LENGTH_SHORT).show(); + } + }; + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + deletingDialog.show(activity.getSupportFragmentManager(), "deletingDialog"); + + // start service with intent + activity.startService(intent); + } + }); + alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dismiss(); + } + }); + alert.setCancelable(true); + + return alert.create(); + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java new file mode 100644 index 000000000..e61003a9b --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.dialog; + +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +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; + +public class DeleteKeyDialogFragment extends DialogFragment { + private static final String ARG_MESSENGER = "messenger"; + private static final String ARG_DELETE_KEY_RING_ROW_ID = "delete_file"; + private static final String ARG_KEY_TYPE = "key_type"; + + public static final int MESSAGE_OKAY = 1; + + private Messenger mMessenger; + + /** + * Creates new instance of this delete file dialog fragment + */ + public static DeleteKeyDialogFragment newInstance(Messenger messenger, long deleteKeyRingRowId, + int keyType) { + DeleteKeyDialogFragment frag = new DeleteKeyDialogFragment(); + Bundle args = new Bundle(); + + args.putParcelable(ARG_MESSENGER, messenger); + args.putLong(ARG_DELETE_KEY_RING_ROW_ID, deleteKeyRingRowId); + args.putInt(ARG_KEY_TYPE, keyType); + + frag.setArguments(args); + + return frag; + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final FragmentActivity activity = getActivity(); + mMessenger = getArguments().getParcelable(ARG_MESSENGER); + + final long deleteKeyRingRowId = getArguments().getLong(ARG_DELETE_KEY_RING_ROW_ID); + final int keyType = getArguments().getInt(ARG_KEY_TYPE); + + // TODO: better way to do this? + String userId = activity.getString(R.string.unknownUserId); + + if (keyType == Id.type.public_key) { + PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByRowId(activity, + deleteKeyRingRowId); + userId = PgpHelper.getMainUserIdSafe(activity, PgpHelper.getMasterKey(keyRing)); + } else { + PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByRowId(activity, + deleteKeyRingRowId); + userId = PgpHelper.getMainUserIdSafe(activity, PgpHelper.getMasterKey(keyRing)); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.warning); + builder.setMessage(getString( + keyType == Id.type.public_key ? R.string.keyDeletionConfirmation + : R.string.secretKeyDeletionConfirmation, userId)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setPositiveButton(R.string.btn_delete, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + if (keyType == Id.type.public_key) { + ProviderHelper.deletePublicKeyRing(activity, deleteKeyRingRowId); + } else { + ProviderHelper.deleteSecretKeyRing(activity, deleteKeyRingRowId); + } + + dismiss(); + + sendMessageToHandler(MESSAGE_OKAY); + } + }); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + } + }); + return builder.create(); + } + + /** + * Send message back to handler which is initialized in a activity + * + * @param what + * Message integer you want to send + */ + private void sendMessageToHandler(Integer what) { + Message msg = Message.obtain(); + msg.what = what; + + 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); + } + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java new file mode 100644 index 000000000..2f9cdb3b1 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.dialog; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageButton; + +public class FileDialogFragment 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_DEFAULT_FILE = "default_file"; + private static final String ARG_CHECKBOX_TEXT = "checkbox_text"; + private static final String ARG_REQUEST_CODE = "request_code"; + + public static final int MESSAGE_OKAY = 1; + + public static final String MESSAGE_DATA_FILENAME = "filename"; + public static final String MESSAGE_DATA_CHECKED = "checked"; + + private Messenger mMessenger; + + /** + * Creates new instance of this file dialog fragment + */ + public static FileDialogFragment newInstance(Messenger messenger, String title, String message, + String defaultFile, String checkboxText, int requestCode) { + FileDialogFragment frag = new FileDialogFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_MESSENGER, messenger); + + args.putString(ARG_TITLE, title); + args.putString(ARG_MESSAGE, message); + args.putString(ARG_DEFAULT_FILE, defaultFile); + args.putString(ARG_CHECKBOX_TEXT, checkboxText); + args.putInt(ARG_REQUEST_CODE, requestCode); + + frag.setArguments(args); + + return frag; + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + mMessenger = getArguments().getParcelable(ARG_MESSENGER); + + String title = getArguments().getString(ARG_TITLE); + String message = getArguments().getString(ARG_MESSAGE); + String defaultFile = getArguments().getString(ARG_DEFAULT_FILE); + String checkboxText = getArguments().getString(ARG_CHECKBOX_TEXT); + final int requestCode = getArguments().getInt(ARG_REQUEST_CODE); + + final EditText mFilename; + final ImageButton mBrowse; + final CheckBox mCheckBox; + + LayoutInflater inflater = (LayoutInflater) activity + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + AlertDialog.Builder alert = new AlertDialog.Builder(activity); + + alert.setTitle(title); + alert.setMessage(message); + + View view = inflater.inflate(R.layout.file_dialog, null); + + mFilename = (EditText) view.findViewById(R.id.input); + mFilename.setText(defaultFile); + mBrowse = (ImageButton) view.findViewById(R.id.btn_browse); + mBrowse.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // only .asc or .gpg files + FileHelper.openFile(activity, mFilename.getText().toString(), "text/plain", + requestCode); + } + }); + + mCheckBox = (CheckBox) view.findViewById(R.id.checkbox); + if (checkboxText == null) { + mCheckBox.setEnabled(false); + mCheckBox.setVisibility(View.GONE); + } else { + mCheckBox.setEnabled(true); + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setText(checkboxText); + } + + alert.setView(view); + + alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + + boolean checked = false; + if (mCheckBox.isEnabled()) { + checked = mCheckBox.isChecked(); + } + + // return resulting data back to activity + Bundle data = new Bundle(); + data.putString(MESSAGE_DATA_FILENAME, mFilename.getText().toString()); + data.putBoolean(MESSAGE_DATA_CHECKED, checked); + + sendMessageToHandler(MESSAGE_OKAY, data); + } + }); + + alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + } + }); + return alert.create(); + } + + /** + * Updates filename in dialog, normally called in onActivityResult in activity using the + * FileDialog + * + * @param messageId + * @param progress + * @param max + */ + public void setFilename(String filename) { + AlertDialog dialog = (AlertDialog) getDialog(); + EditText filenameEditText = (EditText) dialog.findViewById(R.id.input); + + if (filenameEditText != null) { + filenameEditText.setText(filename); + } + } + + /** + * Send message back to handler which is initialized in a activity + * + * @param what + * Message integer you want to send + */ + private void sendMessageToHandler(Integer what, Bundle data) { + Message msg = Message.obtain(); + msg.what = what; + 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); + } + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java new file mode 100644 index 000000000..17f900552 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.dialog; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.ui.KeyServerQueryActivity; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +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; + + +public class LookupUnknownKeyDialogFragment extends DialogFragment { + private static final String ARG_MESSENGER = "messenger"; + private static final String ARG_UNKNOWN_KEY_ID = "unknown_key_id"; + + public static final int MESSAGE_OKAY = 1; + public static final int MESSAGE_CANCEL = 2; + + private Messenger mMessenger; + + /** + * Creates new instance of this dialog fragment + * + * @param messenger + * @param unknownKeyId + * @return + */ + public static LookupUnknownKeyDialogFragment newInstance(Messenger messenger, long unknownKeyId) { + LookupUnknownKeyDialogFragment frag = new LookupUnknownKeyDialogFragment(); + Bundle args = new Bundle(); + args.putLong(ARG_UNKNOWN_KEY_ID, unknownKeyId); + args.putParcelable(ARG_MESSENGER, messenger); + + frag.setArguments(args); + + return frag; + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + final long unknownKeyId = getArguments().getLong(ARG_UNKNOWN_KEY_ID); + mMessenger = getArguments().getParcelable(ARG_MESSENGER); + + AlertDialog.Builder alert = new AlertDialog.Builder(activity); + + alert.setIcon(android.R.drawable.ic_dialog_alert); + alert.setTitle(R.string.title_unknownSignatureKey); + alert.setMessage(getString(R.string.lookupUnknownKey, + PgpHelper.getSmallFingerPrint(unknownKeyId))); + + alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + + sendMessageToHandler(MESSAGE_OKAY); + + Intent intent = new Intent(activity, KeyServerQueryActivity.class); + intent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID); + intent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, unknownKeyId); + startActivityForResult(intent, Id.request.look_up_key_id); + } + }); + alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + + sendMessageToHandler(MESSAGE_CANCEL); + } + }); + alert.setCancelable(true); + alert.setOnCancelListener(new OnCancelListener() { + + @Override + public void onCancel(DialogInterface dialog) { + sendMessageToHandler(MESSAGE_CANCEL); + } + }); + + return alert.create(); + } + + /** + * Send message back to handler which is initialized in a activity + * + * @param what + * Message integer you want to send + */ + private void sendMessageToHandler(Integer what) { + Message msg = Message.obtain(); + msg.what = what; + + 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); + } + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java new file mode 100644 index 000000000..504de9d69 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.dialog; + +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPPrivateKey; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.helper.PgpMain; +import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.DialogFragment; + + +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager.LayoutParams; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; +import android.widget.Toast; + +public class PassphraseDialogFragment extends DialogFragment implements OnEditorActionListener { + private static final String ARG_MESSENGER = "messenger"; + private static final String ARG_SECRET_KEY_ID = "secret_key_id"; + + public static final int MESSAGE_OKAY = 1; + + private Messenger mMessenger; + private EditText mPassphraseEditText; + + /** + * Creates new instance of this dialog fragment + * + * @param secretKeyId + * secret key id you want to use + * @param messenger + * to communicate back after caching the passphrase + * @return + * @throws PgpGeneralException + */ + public static PassphraseDialogFragment newInstance(Context context, Messenger messenger, + long secretKeyId) throws PgpGeneralException { + // check if secret key has a passphrase + if (!(secretKeyId == Id.key.symmetric || secretKeyId == Id.key.none)) { + if (!hasPassphrase(context, secretKeyId)) { + throw new PgpMain.PgpGeneralException("No passphrase! No passphrase dialog needed!"); + } + } + + PassphraseDialogFragment frag = new PassphraseDialogFragment(); + Bundle args = new Bundle(); + args.putLong(ARG_SECRET_KEY_ID, secretKeyId); + args.putParcelable(ARG_MESSENGER, messenger); + + frag.setArguments(args); + + return frag; + } + + /** + * Checks if key has a passphrase + * + * @param secretKeyId + * @return true if it has a passphrase + */ + private static boolean hasPassphrase(Context context, long secretKeyId) { + // check if the key has no passphrase + try { + PGPSecretKey secretKey = PgpHelper.getMasterKey(ProviderHelper + .getPGPSecretKeyRingByKeyId(context, secretKeyId)); + // PGPSecretKey secretKey = + // PGPHelper.getMasterKey(PGPMain.getSecretKeyRing(secretKeyId)); + + Log.d(Constants.TAG, "Check if key has no passphrase..."); + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + "SC").build("".toCharArray()); + PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor); + if (testKey != null) { + Log.d(Constants.TAG, "Key has no passphrase! Caches empty passphrase!"); + + // cache empty passphrase + PassphraseCacheService.addCachedPassphrase(context, secretKey.getKeyID(), ""); + + return false; + } + } catch (PGPException e) { + // silently catch + } + + return true; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + long secretKeyId = getArguments().getLong(ARG_SECRET_KEY_ID); + mMessenger = getArguments().getParcelable(ARG_MESSENGER); + + AlertDialog.Builder alert = new AlertDialog.Builder(activity); + + alert.setTitle(R.string.title_authentication); + + final PGPSecretKey secretKey; + + if (secretKeyId == Id.key.symmetric || secretKeyId == Id.key.none) { + secretKey = null; + alert.setMessage(R.string.passPhraseForSymmetricEncryption); + } else { + // TODO: by master key id??? + secretKey = PgpHelper.getMasterKey(ProviderHelper.getPGPSecretKeyRingByMasterKeyId( + activity, secretKeyId)); + // secretKey = PGPHelper.getMasterKey(PGPMain.getSecretKeyRing(secretKeyId)); + + if (secretKey == null) { + alert.setTitle(R.string.title_keyNotFound); + alert.setMessage(getString(R.string.keyNotFound, secretKeyId)); + alert.setPositiveButton(android.R.string.ok, new OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }); + alert.setCancelable(false); + return alert.create(); + } + String userId = PgpHelper.getMainUserIdSafe(activity, secretKey); + + Log.d(Constants.TAG, "User id: '" + userId + "'"); + alert.setMessage(getString(R.string.passPhraseFor, userId)); + } + + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.passphrase, null); + alert.setView(view); + + mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase); + + alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + + String passPhrase = mPassphraseEditText.getText().toString(); + long keyId; + if (secretKey != null) { + try { + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder() + .setProvider(PgpMain.BOUNCY_CASTLE_PROVIDER_NAME).build( + passPhrase.toCharArray()); + PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor); + if (testKey == null) { + Toast.makeText(activity, R.string.error_couldNotExtractPrivateKey, + Toast.LENGTH_SHORT).show(); + return; + } + } catch (PGPException e) { + Toast.makeText(activity, R.string.wrongPassPhrase, Toast.LENGTH_SHORT) + .show(); + return; + } + keyId = secretKey.getKeyID(); + } else { + keyId = Id.key.symmetric; + } + + // cache the new passphrase + Log.d(Constants.TAG, "Everything okay! Caching entered passphrase"); + PassphraseCacheService.addCachedPassphrase(activity, keyId, passPhrase); + + sendMessageToHandler(MESSAGE_OKAY); + } + }); + + alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + } + }); + + return alert.create(); + } + + @Override + public void onActivityCreated(Bundle arg0) { + super.onActivityCreated(arg0); + + // request focus and open soft keyboard + mPassphraseEditText.requestFocus(); + getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE); + + mPassphraseEditText.setOnEditorActionListener(this); + } + + /** + * 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) { + if (EditorInfo.IME_ACTION_DONE == actionId) { + AlertDialog dialog = ((AlertDialog) getDialog()); + Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + + bt.performClick(); + return true; + } + return false; + } + + /** + * Send message back to handler which is initialized in a activity + * + * @param what + * Message integer you want to send + */ + private void sendMessageToHandler(Integer what) { + Message msg = Message.obtain(); + msg.what = what; + + 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); + } + } + +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java new file mode 100644 index 000000000..69b31419e --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.dialog; + +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnKeyListener; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.KeyEvent; + +public class ProgressDialogFragment extends DialogFragment { + private static final String ARG_MESSAGE_ID = "message_id"; + private static final String ARG_STYLE = "style"; + + /** + * Creates new instance of this fragment + * + * @param id + * @return + */ + public static ProgressDialogFragment newInstance(int messageId, int style) { + ProgressDialogFragment frag = new ProgressDialogFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_MESSAGE_ID, messageId); + args.putInt(ARG_STYLE, style); + + frag.setArguments(args); + return frag; + } + + /** + * Updates progress of dialog + * + * @param messageId + * @param progress + * @param max + */ + public void setProgress(int messageId, int progress, int max) { + setProgress(getString(messageId), progress, max); + } + + /** + * Updates progress of dialog + * + * @param messageId + * @param progress + * @param max + */ + public void setProgress(int progress, int max) { + ProgressDialog dialog = (ProgressDialog) getDialog(); + + dialog.setProgress(progress); + dialog.setMax(max); + } + + /** + * Updates progress of dialog + * + * @param messageId + * @param progress + * @param max + */ + public void setProgress(String message, int progress, int max) { + ProgressDialog dialog = (ProgressDialog) getDialog(); + + dialog.setMessage(message); + dialog.setProgress(progress); + dialog.setMax(max); + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Activity activity = getActivity(); + + ProgressDialog dialog = new ProgressDialog(activity); + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + dialog.setCancelable(false); + dialog.setCanceledOnTouchOutside(false); + + int messageId = getArguments().getInt(ARG_MESSAGE_ID); + int style = getArguments().getInt(ARG_STYLE); + + dialog.setMessage(getString(messageId)); + dialog.setProgressStyle(style); + + // Disable the back button + OnKeyListener keyListener = new OnKeyListener() { + + @Override + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { + + if (keyCode == KeyEvent.KEYCODE_BACK) { + return true; + } + return false; + } + + }; + dialog.setOnKeyListener(keyListener); + + return dialog; + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java new file mode 100644 index 000000000..f001e7a7d --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.dialog; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.DialogFragment; + +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager.LayoutParams; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.TextView.OnEditorActionListener; + +public class SetPassphraseDialogFragment extends DialogFragment implements OnEditorActionListener { + private static final String ARG_MESSENGER = "messenger"; + private static final String ARG_TITLE = "title"; + + public static final int MESSAGE_OKAY = 1; + + public static final String MESSAGE_NEW_PASSPHRASE = "new_passphrase"; + + private Messenger mMessenger; + private EditText mPassphraseEditText; + private EditText mPassphraseAgainEditText; + + /** + * Creates new instance of this dialog fragment + * + * @param title + * title of dialog + * @param messenger + * to communicate back after setting the passphrase + * @return + */ + public static SetPassphraseDialogFragment newInstance(Messenger messenger, int title) { + SetPassphraseDialogFragment frag = new SetPassphraseDialogFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_TITLE, title); + args.putParcelable(ARG_MESSENGER, messenger); + + frag.setArguments(args); + + return frag; + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + int title = getArguments().getInt(ARG_TITLE); + mMessenger = getArguments().getParcelable(ARG_MESSENGER); + + AlertDialog.Builder alert = new AlertDialog.Builder(activity); + + alert.setTitle(title); + alert.setMessage(R.string.enterPassPhraseTwice); + + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.passphrase_repeat, null); + alert.setView(view); + + mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase); + mPassphraseAgainEditText = (EditText) view.findViewById(R.id.passphrase_passphrase_again); + + alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + + String passPhrase1 = mPassphraseEditText.getText().toString(); + String passPhrase2 = mPassphraseAgainEditText.getText().toString(); + if (!passPhrase1.equals(passPhrase2)) { + Toast.makeText( + activity, + getString(R.string.errorMessage, + getString(R.string.passPhrasesDoNotMatch)), Toast.LENGTH_SHORT) + .show(); + return; + } + + if (passPhrase1.equals("")) { + Toast.makeText( + activity, + getString(R.string.errorMessage, + getString(R.string.passPhraseMustNotBeEmpty)), + Toast.LENGTH_SHORT).show(); + return; + } + + // return resulting data back to activity + Bundle data = new Bundle(); + data.putString(MESSAGE_NEW_PASSPHRASE, passPhrase1); + + sendMessageToHandler(MESSAGE_OKAY, data); + } + }); + + alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + } + }); + + return alert.create(); + } + + @Override + public void onActivityCreated(Bundle arg0) { + super.onActivityCreated(arg0); + + // request focus and open soft keyboard + mPassphraseEditText.requestFocus(); + getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE); + + mPassphraseAgainEditText.setOnEditorActionListener(this); + } + + /** + * 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) { + if (EditorInfo.IME_ACTION_DONE == actionId) { + AlertDialog dialog = ((AlertDialog) getDialog()); + Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + + bt.performClick(); + return true; + } + return false; + } + + /** + * Send message back to handler which is initialized in a activity + * + * @param what + * Message integer you want to send + */ + private void sendMessageToHandler(Integer what, Bundle data) { + Message msg = Message.obtain(); + msg.what = what; + 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); + } + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java new file mode 100644 index 000000000..158a271bc --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java @@ -0,0 +1,186 @@ +/* + * Copyright 2011 Google Inc. + * + * 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.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * Custom layout that arranges children in a grid-like manner, optimizing for even horizontal and + * vertical whitespace. + */ +public class DashboardLayout extends ViewGroup { + private static final int UNEVEN_GRID_PENALTY_MULTIPLIER = 10; + + private int mMaxChildWidth = 0; + private int mMaxChildHeight = 0; + + public DashboardLayout(Context context) { + super(context, null); + } + + public DashboardLayout(Context context, AttributeSet attrs) { + super(context, attrs, 0); + } + + public DashboardLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mMaxChildWidth = 0; + mMaxChildHeight = 0; + + // Measure once to find the maximum child size. + + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST); + int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.AT_MOST); + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + mMaxChildWidth = Math.max(mMaxChildWidth, child.getMeasuredWidth()); + mMaxChildHeight = Math.max(mMaxChildHeight, child.getMeasuredHeight()); + } + + // Measure again for each child to be exactly the same size. + + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxChildWidth, MeasureSpec.EXACTLY); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxChildHeight, MeasureSpec.EXACTLY); + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + setMeasuredDimension(resolveSize(mMaxChildWidth, widthMeasureSpec), + resolveSize(mMaxChildHeight, heightMeasureSpec)); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int width = r - l; + int height = b - t; + + final int count = getChildCount(); + + // Calculate the number of visible children. + int visibleCount = 0; + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } + ++visibleCount; + } + + if (visibleCount == 0) { + return; + } + + // Calculate what number of rows and columns will optimize for even horizontal and + // vertical whitespace between items. Start with a 1 x N grid, then try 2 x N, and so on. + int bestSpaceDifference = Integer.MAX_VALUE; + int spaceDifference; + + // Horizontal and vertical space between items + int hSpace = 0; + int vSpace = 0; + + int cols = 1; + int rows; + + while (true) { + rows = (visibleCount - 1) / cols + 1; + + hSpace = ((width - mMaxChildWidth * cols) / (cols + 1)); + vSpace = ((height - mMaxChildHeight * rows) / (rows + 1)); + + spaceDifference = Math.abs(vSpace - hSpace); + if (rows * cols != visibleCount) { + spaceDifference *= UNEVEN_GRID_PENALTY_MULTIPLIER; + } else if (rows * mMaxChildHeight > height || cols * mMaxChildWidth > width) { + spaceDifference *= UNEVEN_GRID_PENALTY_MULTIPLIER; + } + + if (spaceDifference < bestSpaceDifference) { + // Found a better whitespace squareness/ratio + bestSpaceDifference = spaceDifference; + + // If we found a better whitespace squareness and there's only 1 row, this is + // the best we can do. + if (rows == 1) { + break; + } + } else { + // This is a worse whitespace ratio, use the previous value of cols and exit. + --cols; + rows = (visibleCount - 1) / cols + 1; + hSpace = ((width - mMaxChildWidth * cols) / (cols + 1)); + vSpace = ((height - mMaxChildHeight * rows) / (rows + 1)); + break; + } + + ++cols; + } + + // Lay out children based on calculated best-fit number of rows and cols. + + // If we chose a layout that has negative horizontal or vertical space, force it to zero. + hSpace = Math.max(0, hSpace); + vSpace = Math.max(0, vSpace); + + // Re-use width/height variables to be child width/height. + width = (width - hSpace * (cols + 1)) / cols; + height = (height - vSpace * (rows + 1)) / rows; + + int left, top; + int col, row; + int visibleIndex = 0; + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } + + row = visibleIndex / cols; + col = visibleIndex % cols; + + left = hSpace * (col + 1) + width * col; + top = vSpace * (row + 1) + height * row; + + child.layout(left, top, (hSpace == 0 && col == cols - 1) ? r : (left + width), + (vSpace == 0 && row == rows - 1) ? b : (top + height)); + ++visibleIndex; + } + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java new file mode 100644 index 000000000..1cf510d3a --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui.widget; + +public interface Editor { + public interface EditorListener { + public void onDeleted(Editor editor); + } + + public void setEditorListener(EditorListener listener); +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java new file mode 100644 index 000000000..54022342f --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2011 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.widget; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnCreateContextMenuListener; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.ExpandableListAdapter; +import android.widget.ExpandableListView; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ProgressBar; +import android.widget.TextView; + +/** + * @author Khoa Tran + * + * @see android.support.v4.app.ListFragment + * @see android.app.ExpandableListActivity + * + * ExpandableListFragment for Android < 3.0 + * + * from + * http://stackoverflow.com/questions/6051050/expandablelistfragment-with-loadermanager-for- + * compatibility-package + * + */ +public class ExpandableListFragment extends Fragment implements OnCreateContextMenuListener, + ExpandableListView.OnChildClickListener, ExpandableListView.OnGroupCollapseListener, + ExpandableListView.OnGroupExpandListener { + + static final int INTERNAL_EMPTY_ID = 0x00ff0001; + static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002; + static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003; + + final private Handler mHandler = new Handler(); + + final private Runnable mRequestFocus = new Runnable() { + public void run() { + mExpandableList.focusableViewAvailable(mExpandableList); + } + }; + + final private AdapterView.OnItemClickListener mOnClickListener = new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView<?> parent, View v, int position, long id) { + onListItemClick((ExpandableListView) parent, v, position, id); + } + }; + + ExpandableListAdapter mAdapter; + ExpandableListView mExpandableList; + boolean mFinishedStart = false; + View mEmptyView; + TextView mStandardEmptyView; + View mProgressContainer; + View mExpandableListContainer; + CharSequence mEmptyText; + boolean mExpandableListShown; + + public ExpandableListFragment() { + } + + /** + * Provide default implementation to return a simple list view. Subclasses can override to + * replace with their own layout. If doing so, the returned view hierarchy <em>must</em> have a + * ListView whose id is {@link android.R.id#list android.R.id.list} and can optionally have a + * sibling view id {@link android.R.id#empty android.R.id.empty} that is to be shown when the + * list is empty. + * + * <p> + * If you are overriding this method with your own custom content, consider including the + * standard layout {@link android.R.layout#list_content} in your layout file, so that you + * continue to retain all of the standard behavior of ListFragment. In particular, this is + * currently the only way to have the built-in indeterminant progress state be shown. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final Context context = getActivity(); + + FrameLayout root = new FrameLayout(context); + + // ------------------------------------------------------------------ + + LinearLayout pframe = new LinearLayout(context); + pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID); + pframe.setOrientation(LinearLayout.VERTICAL); + pframe.setVisibility(View.GONE); + pframe.setGravity(Gravity.CENTER); + + ProgressBar progress = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge); + pframe.addView(progress, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + root.addView(pframe, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + // ------------------------------------------------------------------ + + FrameLayout lframe = new FrameLayout(context); + lframe.setId(INTERNAL_LIST_CONTAINER_ID); + + TextView tv = new TextView(getActivity()); + tv.setId(INTERNAL_EMPTY_ID); + tv.setGravity(Gravity.CENTER); + lframe.addView(tv, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + ExpandableListView lv = new ExpandableListView(getActivity()); + lv.setId(android.R.id.list); + lv.setDrawSelectorOnTop(false); + lframe.addView(lv, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + root.addView(lframe, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + // ------------------------------------------------------------------ + + root.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + return root; + } + + /** + * Attach to list view once the view hierarchy has been created. + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ensureList(); + } + + /** + * Detach from list view. + */ + @Override + public void onDestroyView() { + mHandler.removeCallbacks(mRequestFocus); + mExpandableList = null; + mExpandableListShown = false; + mEmptyView = mProgressContainer = mExpandableListContainer = null; + mStandardEmptyView = null; + super.onDestroyView(); + } + + /** + * This method will be called when an item in the list is selected. Subclasses should override. + * Subclasses can call getListView().getItemAtPosition(position) if they need to access the data + * associated with the selected item. + * + * @param l + * The ListView where the click happened + * @param v + * The view that was clicked within the ListView + * @param position + * The position of the view in the list + * @param id + * The row id of the item that was clicked + */ + public void onListItemClick(ExpandableListView l, View v, int position, long id) { + } + + /** + * Provide the cursor for the list view. + */ + public void setListAdapter(ExpandableListAdapter adapter) { + boolean hadAdapter = mAdapter != null; + mAdapter = adapter; + if (mExpandableList != null) { + mExpandableList.setAdapter(adapter); + if (!mExpandableListShown && !hadAdapter) { + // The list was hidden, and previously didn't have an + // adapter. It is now time to show it. + setListShown(true, getView().getWindowToken() != null); + } + } + } + + /** + * Set the currently selected list item to the specified position with the adapter's data + * + * @param position + */ + public void setSelection(int position) { + ensureList(); + mExpandableList.setSelection(position); + } + + /** + * Get the position of the currently selected list item. + */ + public int getSelectedItemPosition() { + ensureList(); + return mExpandableList.getSelectedItemPosition(); + } + + /** + * Get the cursor row ID of the currently selected list item. + */ + public long getSelectedItemId() { + ensureList(); + return mExpandableList.getSelectedItemId(); + } + + /** + * Get the activity's list view widget. + */ + public ExpandableListView getListView() { + ensureList(); + return mExpandableList; + } + + /** + * The default content for a ListFragment has a TextView that can be shown when the list is + * empty. If you would like to have it shown, call this method to supply the text it should use. + */ + public void setEmptyText(CharSequence text) { + ensureList(); + if (mStandardEmptyView == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + mStandardEmptyView.setText(text); + if (mEmptyText == null) { + mExpandableList.setEmptyView(mStandardEmptyView); + } + mEmptyText = text; + } + + /** + * Control whether the list is being displayed. You can make it not displayed if you are waiting + * for the initial data to show in it. During this time an indeterminant progress indicator will + * be shown instead. + * + * <p> + * Applications do not normally need to use this themselves. The default behavior of + * ListFragment is to start with the list not being shown, only showing it once an adapter is + * given with {@link #setListAdapter(ListAdapter)}. If the list at that point had not been + * shown, when it does get shown it will be do without the user ever seeing the hidden state. + * + * @param shown + * If true, the list view is shown; if false, the progress indicator. The initial + * value is true. + */ + public void setListShown(boolean shown) { + setListShown(shown, true); + } + + /** + * Like {@link #setListShown(boolean)}, but no animation is used when transitioning from the + * previous state. + */ + public void setListShownNoAnimation(boolean shown) { + setListShown(shown, false); + } + + /** + * Control whether the list is being displayed. You can make it not displayed if you are waiting + * for the initial data to show in it. During this time an indeterminant progress indicator will + * be shown instead. + * + * @param shown + * If true, the list view is shown; if false, the progress indicator. The initial + * value is true. + * @param animate + * If true, an animation will be used to transition to the new state. + */ + private void setListShown(boolean shown, boolean animate) { + ensureList(); + if (mProgressContainer == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + if (mExpandableListShown == shown) { + return; + } + mExpandableListShown = shown; + if (shown) { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), + android.R.anim.fade_out)); + mExpandableListContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), + android.R.anim.fade_in)); + } else { + mProgressContainer.clearAnimation(); + mExpandableListContainer.clearAnimation(); + } + mProgressContainer.setVisibility(View.GONE); + mExpandableListContainer.setVisibility(View.VISIBLE); + } else { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), + android.R.anim.fade_in)); + mExpandableListContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(), + android.R.anim.fade_out)); + } else { + mProgressContainer.clearAnimation(); + mExpandableListContainer.clearAnimation(); + } + mProgressContainer.setVisibility(View.VISIBLE); + mExpandableListContainer.setVisibility(View.GONE); + } + } + + /** + * Get the ListAdapter associated with this activity's ListView. + */ + public ExpandableListAdapter getListAdapter() { + return mAdapter; + } + + private void ensureList() { + if (mExpandableList != null) { + return; + } + View root = getView(); + if (root == null) { + throw new IllegalStateException("Content view not yet created"); + } + if (root instanceof ExpandableListView) { + mExpandableList = (ExpandableListView) root; + } else { + mStandardEmptyView = (TextView) root.findViewById(INTERNAL_EMPTY_ID); + if (mStandardEmptyView == null) { + mEmptyView = root.findViewById(android.R.id.empty); + } else { + mStandardEmptyView.setVisibility(View.GONE); + } + mProgressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID); + mExpandableListContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID); + View rawExpandableListView = root.findViewById(android.R.id.list); + if (!(rawExpandableListView instanceof ExpandableListView)) { + if (rawExpandableListView == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + + "'android.R.id.list'"); + } + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + mExpandableList = (ExpandableListView) rawExpandableListView; + if (mEmptyView != null) { + mExpandableList.setEmptyView(mEmptyView); + } else if (mEmptyText != null) { + mStandardEmptyView.setText(mEmptyText); + mExpandableList.setEmptyView(mStandardEmptyView); + } + } + mExpandableListShown = true; + mExpandableList.setOnItemClickListener(mOnClickListener); + if (mAdapter != null) { + ExpandableListAdapter adapter = mAdapter; + mAdapter = null; + setListAdapter(adapter); + } else { + // We are starting without an adapter, so assume we won't + // have our data right away and start with the progress indicator. + if (mProgressContainer != null) { + setListShown(false, false); + } + } + mHandler.post(mRequestFocus); + } + + /** + * Override this to populate the context menu when an item is long pressed. menuInfo will + * contain an {@link android.widget.ExpandableListView.ExpandableListContextMenuInfo} whose + * packedPosition is a packed position that should be used with + * {@link ExpandableListView#getPackedPositionType(long)} and the other similar methods. + * <p> + * {@inheritDoc} + */ + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + } + + /** + * Override this for receiving callbacks when a child has been clicked. + * <p> + * {@inheritDoc} + */ + public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, + int childPosition, long id) { + return false; + } + + /** + * Override this for receiving callbacks when a group has been collapsed. + */ + public void onGroupCollapse(int groupPosition) { + } + + /** + * Override this for receiving callbacks when a group has been expanded. + */ + public void onGroupExpand(int groupPosition) { + } + + // /** + // * Ensures the expandable list view has been created before Activity restores all + // * of the view states. + // * + // *@see Activity#onRestoreInstanceState(Bundle) + // */ + // @Override + // protected void onRestoreInstanceState(Bundle state) { + // ensureList(); + // super.onRestoreInstanceState(state); + // } + + /** + * Updates the screen state (current list and other views) when the content changes. + * + * @see Activity#onContentChanged() + */ + + public void onContentChanged() { + // super.onContentChanged(); + View emptyView = getView().findViewById(android.R.id.empty); + mExpandableList = (ExpandableListView) getView().findViewById(android.R.id.list); + if (mExpandableList == null) { + throw new RuntimeException( + "Your content must have a ExpandableListView whose id attribute is " + + "'android.R.id.list'"); + } + if (emptyView != null) { + mExpandableList.setEmptyView(emptyView); + } + mExpandableList.setOnChildClickListener(this); + mExpandableList.setOnGroupExpandListener(this); + mExpandableList.setOnGroupCollapseListener(this); + + if (mFinishedStart) { + setListAdapter(mAdapter); + } + mFinishedStart = true; + } + + /** + * Get the activity's expandable list view widget. This can be used to get the selection, set + * the selection, and many other useful functions. + * + * @see ExpandableListView + */ + public ExpandableListView getExpandableListView() { + ensureList(); + return mExpandableList; + } + + /** + * Get the ExpandableListAdapter associated with this activity's ExpandableListView. + */ + public ExpandableListAdapter getExpandableListAdapter() { + return mAdapter; + } + + /** + * Gets the ID of the currently selected group or child. + * + * @return The ID of the currently selected group or child. + */ + public long getSelectedId() { + return mExpandableList.getSelectedId(); + } + + /** + * Gets the position (in packed position representation) of the currently selected group or + * child. Use {@link ExpandableListView#getPackedPositionType}, + * {@link ExpandableListView#getPackedPositionGroup}, and + * {@link ExpandableListView#getPackedPositionChild} to unpack the returned packed position. + * + * @return A packed position representation containing the currently selected group or child's + * position and type. + */ + public long getSelectedPosition() { + return mExpandableList.getSelectedPosition(); + } + + /** + * Sets the selection to the specified child. If the child is in a collapsed group, the group + * will only be expanded and child subsequently selected if shouldExpandGroup is set to true, + * otherwise the method will return false. + * + * @param groupPosition + * The position of the group that contains the child. + * @param childPosition + * The position of the child within the group. + * @param shouldExpandGroup + * Whether the child's group should be expanded if it is collapsed. + * @return Whether the selection was successfully set on the child. + */ + public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) { + return mExpandableList.setSelectedChild(groupPosition, childPosition, shouldExpandGroup); + } + + /** + * Sets the selection to the specified group. + * + * @param groupPosition + * The position of the group that should be selected. + */ + public void setSelectedGroup(int groupPosition) { + mExpandableList.setSelectedGroup(groupPosition); + } +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java new file mode 100644 index 000000000..198140e0c --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.widget; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPObjectFactory; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPPublicKeyRingCollection; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPSecretKeyRingCollection; +import org.spongycastle.openpgp.PGPUtil; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.helper.PgpConversionHelper; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.PositionAwareInputStream; +import org.sufficientlysecure.keychain.R; + +import android.content.Context; +import android.support.v4.content.AsyncTaskLoader; + +/** + * A custom Loader to search for bad adware apps, based on + * https://github.com/brosmike/AirPush-Detector. Daniel Bjorge licensed it under Apachev2 after + * asking him by mail. + */ +public class ImportKeysListLoader extends AsyncTaskLoader<List<Map<String, String>>> { + public static final String MAP_ATTR_USER_ID = "user_id"; + public static final String MAP_ATTR_FINGERPINT = "fingerprint"; + + ArrayList<Map<String, String>> data = new ArrayList<Map<String, String>>(); + + Context mContext; + List<String> mItems; + + byte[] mKeyringBytes; + String mImportFilename; + + public ImportKeysListLoader(Context context, byte[] keyringBytes, String importFilename) { + super(context); + this.mContext = context; + this.mKeyringBytes = keyringBytes; + this.mImportFilename = importFilename; + } + + @Override + public List<Map<String, String>> loadInBackground() { + InputData inputData = null; + if (mKeyringBytes != null) { + inputData = new InputData(new ByteArrayInputStream(mKeyringBytes), mKeyringBytes.length); + } else { + try { + inputData = new InputData(new FileInputStream(mImportFilename), + mImportFilename.length()); + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "Failed to init FileInputStream!", e); + } + } + + generateListOfKeyrings(inputData); + + return data; + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + } + + @Override + protected void onStartLoading() { + forceLoad(); + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + public void deliverResult(List<Map<String, String>> data) { + super.deliverResult(data); + } + + /** + * Similar to PGPMain.importKeyRings + * + * @param keyringBytes + * @return + */ + private void generateListOfKeyrings(InputData inputData) { + PositionAwareInputStream progressIn = new PositionAwareInputStream( + inputData.getInputStream()); + + // need to have access to the bufferedInput, so we can reuse it for the possible + // PGPObject chunks after the first one, e.g. files with several consecutive ASCII + // armour blocks + BufferedInputStream bufferedInput = new BufferedInputStream(progressIn); + try { + + // read all available blocks... (asc files can contain many blocks with BEGIN END) + while (bufferedInput.available() > 0) { + InputStream in = PGPUtil.getDecoderStream(bufferedInput); + PGPObjectFactory objectFactory = new PGPObjectFactory(in); + + // go through all objects in this block + Object obj; + while ((obj = objectFactory.nextObject()) != null) { + Log.d(Constants.TAG, "Found class: " + obj.getClass()); + + if (obj instanceof PGPKeyRing) { + PGPKeyRing newKeyring = (PGPKeyRing) obj; + addToData(newKeyring); + } else { + Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!"); + } + } + } + } catch (Exception e) { + Log.e(Constants.TAG, "Exception on parsing key file!", e); + } + } + + private void addToData(PGPKeyRing keyring) { + String userId = PgpHelper.getMainUserId(keyring.getPublicKey()); + + if (keyring instanceof PGPSecretKeyRing) { + userId = mContext.getString(R.string.secretKeyring) + " " + userId; + } + + String fingerprint = PgpHelper.convertFingerprintToHex(keyring.getPublicKey() + .getFingerprint()); + + Map<String, String> attrs = new HashMap<String, String>(); + attrs.put(MAP_ATTR_USER_ID, userId); + attrs.put(MAP_ATTR_FINGERPINT, mContext.getString(R.string.fingerprint) + "\n" + + fingerprint); + data.add(attrs); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java new file mode 100644 index 000000000..15b4ddc6a --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java @@ -0,0 +1,95 @@ +/* + * Copyright 2010 Google Inc. + * + * 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.widget; + +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; + +/** + * A list preference which persists its values as integers instead of strings. + * Code reading the values should use + * {@link android.content.SharedPreferences#getInt}. + * When using XML-declared arrays for entry values, the arrays should be regular + * string arrays containing valid integer values. + * + * @author Rodrigo Damazio + */ +public class IntegerListPreference extends ListPreference { + + public IntegerListPreference(Context context) { + super(context); + + verifyEntryValues(null); + } + + public IntegerListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + verifyEntryValues(null); + } + + @Override + public void setEntryValues(CharSequence[] entryValues) { + CharSequence[] oldValues = getEntryValues(); + super.setEntryValues(entryValues); + verifyEntryValues(oldValues); + } + + @Override + public void setEntryValues(int entryValuesResId) { + CharSequence[] oldValues = getEntryValues(); + super.setEntryValues(entryValuesResId); + verifyEntryValues(oldValues); + } + + @Override + protected String getPersistedString(String defaultReturnValue) { + // During initial load, there's no known default value + int defaultIntegerValue = Integer.MIN_VALUE; + if (defaultReturnValue != null) { + defaultIntegerValue = Integer.parseInt(defaultReturnValue); + } + + // When the list preference asks us to read a string, instead read an + // integer. + int value = getPersistedInt(defaultIntegerValue); + return Integer.toString(value); + } + + @Override + protected boolean persistString(String value) { + // When asked to save a string, instead save an integer + return persistInt(Integer.parseInt(value)); + } + + private void verifyEntryValues(CharSequence[] oldValues) { + CharSequence[] entryValues = getEntryValues(); + if (entryValues == null) { + return; + } + + for (CharSequence entryValue : entryValues) { + try { + Integer.parseInt(entryValue.toString()); + } catch (NumberFormatException nfe) { + super.setEntryValues(oldValues); + throw nfe; + } + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java new file mode 100644 index 000000000..1122fc522 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui.widget; + +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPSecretKey; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.util.Choice; +import org.sufficientlysecure.keychain.R; + +import android.app.DatePickerDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.DatePicker; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Vector; + +public class KeyEditor extends LinearLayout implements Editor, OnClickListener { + private PGPSecretKey mKey; + + private EditorListener mEditorListener = null; + + private boolean mIsMasterKey; + ImageButton mDeleteButton; + TextView mAlgorithm; + TextView mKeyId; + Spinner mUsage; + TextView mCreationDate; + Button mExpiryDateButton; + GregorianCalendar mExpiryDate; + + private DatePickerDialog.OnDateSetListener mExpiryDateSetListener = new DatePickerDialog.OnDateSetListener() { + public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { + GregorianCalendar date = new GregorianCalendar(year, monthOfYear, dayOfMonth); + setExpiryDate(date); + } + }; + + public KeyEditor(Context context) { + super(context); + } + + public KeyEditor(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + setDrawingCacheEnabled(true); + setAlwaysDrawnWithCacheEnabled(true); + + mAlgorithm = (TextView) findViewById(R.id.algorithm); + mKeyId = (TextView) findViewById(R.id.keyId); + mCreationDate = (TextView) findViewById(R.id.creation); + mExpiryDateButton = (Button) findViewById(R.id.expiry); + mUsage = (Spinner) findViewById(R.id.usage); + Choice choices[] = { + new Choice(Id.choice.usage.sign_only, getResources().getString( + R.string.choice_signOnly)), + new Choice(Id.choice.usage.encrypt_only, getResources().getString( + R.string.choice_encryptOnly)), + new Choice(Id.choice.usage.sign_and_encrypt, getResources().getString( + R.string.choice_signAndEncrypt)), }; + ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(getContext(), + android.R.layout.simple_spinner_item, choices); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mUsage.setAdapter(adapter); + + mDeleteButton = (ImageButton) findViewById(R.id.delete); + mDeleteButton.setOnClickListener(this); + + setExpiryDate(null); + + mExpiryDateButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + GregorianCalendar date = mExpiryDate; + if (date == null) { + date = new GregorianCalendar(); + } + + DatePickerDialog dialog = new DatePickerDialog(getContext(), + mExpiryDateSetListener, date.get(Calendar.YEAR), date.get(Calendar.MONTH), + date.get(Calendar.DAY_OF_MONTH)); + dialog.setCancelable(true); + dialog.setButton(Dialog.BUTTON_NEGATIVE, getContext() + .getString(R.string.btn_noDate), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setExpiryDate(null); + } + }); + dialog.show(); + } + }); + + super.onFinishInflate(); + } + + public void setValue(PGPSecretKey key, boolean isMasterKey, int usage) { + mKey = key; + + mIsMasterKey = isMasterKey; + if (mIsMasterKey) { + mDeleteButton.setVisibility(View.INVISIBLE); + } + + mAlgorithm.setText(PgpHelper.getAlgorithmInfo(key)); + String keyId1Str = PgpHelper.getSmallFingerPrint(key.getKeyID()); + String keyId2Str = PgpHelper.getSmallFingerPrint(key.getKeyID() >> 32); + mKeyId.setText(keyId1Str + " " + keyId2Str); + + Vector<Choice> choices = new Vector<Choice>(); + boolean isElGamalKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT); + boolean isDSAKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.DSA); + if (!isElGamalKey) { + choices.add(new Choice(Id.choice.usage.sign_only, getResources().getString( + R.string.choice_signOnly))); + } + if (!mIsMasterKey && !isDSAKey) { + choices.add(new Choice(Id.choice.usage.encrypt_only, getResources().getString( + R.string.choice_encryptOnly))); + } + if (!isElGamalKey && !isDSAKey) { + choices.add(new Choice(Id.choice.usage.sign_and_encrypt, getResources().getString( + R.string.choice_signAndEncrypt))); + } + + ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(getContext(), + android.R.layout.simple_spinner_item, choices); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mUsage.setAdapter(adapter); + + // Set value in choice dropdown to key + int selectId = 0; + if (PgpHelper.isEncryptionKey(key)) { + if (PgpHelper.isSigningKey(key)) { + selectId = Id.choice.usage.sign_and_encrypt; + } else { + selectId = Id.choice.usage.encrypt_only; + } + } else { + // set usage if it is predefined + if (usage != -1) { + selectId = usage; + } else { + selectId = Id.choice.usage.sign_only; + } + + } + + for (int i = 0; i < choices.size(); ++i) { + if (choices.get(i).getId() == selectId) { + mUsage.setSelection(i); + break; + } + } + + GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(PgpHelper.getCreationDate(key)); + mCreationDate.setText(DateFormat.getDateInstance().format(cal.getTime())); + cal = new GregorianCalendar(); + Date date = PgpHelper.getExpiryDate(key); + if (date == null) { + setExpiryDate(null); + } else { + cal.setTime(PgpHelper.getExpiryDate(key)); + setExpiryDate(cal); + } + + } + + public PGPSecretKey getValue() { + return mKey; + } + + public void onClick(View v) { + final ViewGroup parent = (ViewGroup) getParent(); + if (v == mDeleteButton) { + parent.removeView(this); + if (mEditorListener != null) { + mEditorListener.onDeleted(this); + } + } + } + + public void setEditorListener(EditorListener listener) { + mEditorListener = listener; + } + + private void setExpiryDate(GregorianCalendar date) { + mExpiryDate = date; + if (date == null) { + mExpiryDateButton.setText(R.string.none); + } else { + mExpiryDateButton.setText(DateFormat.getDateInstance().format(date.getTime())); + } + } + + public GregorianCalendar getExpiryDate() { + return mExpiryDate; + } + + public int getUsage() { + return ((Choice) mUsage.getSelectedItem()).getId(); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java new file mode 100644 index 000000000..8e8d33886 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.widget; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.R; + +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.MergeCursor; +import android.net.Uri; +import android.provider.BaseColumns; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorTreeAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class KeyListAdapter extends CursorTreeAdapter { + private Context mContext; + private LayoutInflater mInflater; + + protected int mKeyType; + + private static final int CHILD_KEY = 0; + private static final int CHILD_USER_ID = 1; + private static final int CHILD_FINGERPRINT = 2; + + public KeyListAdapter(Context context, Cursor groupCursor, int keyType) { + super(groupCursor, context); + mContext = context; + mInflater = LayoutInflater.from(context); + mKeyType = keyType; + } + + /** + * Inflate new view for group items + */ + @Override + public View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) { + return mInflater.inflate(R.layout.key_list_group_item, null); + } + + /** + * Binds TextViews from group view to results from database group cursor. + */ + @Override + protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) { + int userIdIndex = cursor.getColumnIndex(UserIds.USER_ID); + + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + mainUserId.setText(R.string.unknownUserId); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + mainUserIdRest.setText(""); + + String userId = cursor.getString(userIdIndex); + if (userId != null) { + String[] userIdSplit = OtherHelper.splitUserId(userId); + + if (userIdSplit[1] != null) { + mainUserIdRest.setText(userIdSplit[1]); + } + mainUserId.setText(userIdSplit[0]); + } + + if (mainUserId.getText().length() == 0) { + mainUserId.setText(R.string.unknownUserId); + } + + if (mainUserIdRest.getText().length() == 0) { + mainUserIdRest.setVisibility(View.GONE); + } + } + + /** + * Inflate new view for child items + */ + @Override + public View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) { + return mInflater.inflate(R.layout.key_list_child_item, null); + } + + /** + * Bind TextViews from view of childs based on query results + */ + @Override + protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) { + LinearLayout keyLayout = (LinearLayout) view.findViewById(R.id.keyLayout); + LinearLayout userIdLayout = (LinearLayout) view.findViewById(R.id.userIdLayout); + + // first entry is fingerprint + if (cursor.getPosition() == 0) { + // show only userId layout + keyLayout.setVisibility(View.GONE); + userIdLayout.setVisibility(View.VISIBLE); + + String fingerprint = PgpHelper.getFingerPrint(context, + cursor.getLong(cursor.getColumnIndex(Keys.KEY_ID))); + fingerprint = fingerprint.replace(" ", "\n"); + + TextView userId = (TextView) view.findViewById(R.id.userId); + if (userId == null) { + Log.d(Constants.TAG, "userId is null!"); + } + userId.setText(context.getString(R.string.fingerprint) + "\n" + fingerprint); + } else { + // differentiate between keys and userIds in MergeCursor + if (cursor.getColumnIndex(Keys.KEY_ID) != -1) { + keyLayout.setVisibility(View.VISIBLE); + userIdLayout.setVisibility(View.GONE); + + String keyIdStr = PgpHelper.getSmallFingerPrint(cursor.getLong(cursor + .getColumnIndex(Keys.KEY_ID))); + String algorithmStr = PgpHelper.getAlgorithmInfo( + cursor.getInt(cursor.getColumnIndex(Keys.ALGORITHM)), + cursor.getInt(cursor.getColumnIndex(Keys.KEY_SIZE))); + + TextView keyId = (TextView) view.findViewById(R.id.keyId); + keyId.setText(keyIdStr); + + TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails); + keyDetails.setText("(" + algorithmStr + ")"); + + ImageView masterKeyIcon = (ImageView) view.findViewById(R.id.ic_masterKey); + if (cursor.getInt(cursor.getColumnIndex(Keys.IS_MASTER_KEY)) != 1) { + masterKeyIcon.setVisibility(View.INVISIBLE); + } else { + masterKeyIcon.setVisibility(View.VISIBLE); + } + + ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey); + if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_ENCRYPT)) != 1) { + encryptIcon.setVisibility(View.GONE); + } else { + encryptIcon.setVisibility(View.VISIBLE); + } + + ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey); + if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_SIGN)) != 1) { + signIcon.setVisibility(View.GONE); + } else { + signIcon.setVisibility(View.VISIBLE); + } + } else { + keyLayout.setVisibility(View.GONE); + userIdLayout.setVisibility(View.VISIBLE); + + String userIdStr = cursor.getString(cursor.getColumnIndex(UserIds.USER_ID)); + + TextView userId = (TextView) view.findViewById(R.id.userId); + userId.setText(userIdStr); + } + } + } + + /** + * Given the group cursor, we start cursors for a fingerprint, keys, and userIds, which are + * merged together and build the child cursor + */ + @Override + protected Cursor getChildrenCursor(Cursor groupCursor) { + final long keyRingRowId = groupCursor.getLong(groupCursor.getColumnIndex(BaseColumns._ID)); + + Cursor fingerprintCursor = getChildCursor(keyRingRowId, CHILD_FINGERPRINT); + Cursor keyCursor = getChildCursor(keyRingRowId, CHILD_KEY); + Cursor userIdCursor = getChildCursor(keyRingRowId, CHILD_USER_ID); + + MergeCursor mergeCursor = new MergeCursor(new Cursor[] { fingerprintCursor, keyCursor, + userIdCursor }); + Log.d(Constants.TAG, "mergeCursor:" + DatabaseUtils.dumpCursorToString(mergeCursor)); + + return mergeCursor; + } + + /** + * This builds a cursor for a specific type of children + * + * @param keyRingRowId + * foreign row id of the keyRing + * @param type + * @return + */ + private Cursor getChildCursor(long keyRingRowId, int type) { + Uri uri = null; + String[] projection = null; + String sortOrder = null; + String selection = null; + + switch (type) { + case CHILD_FINGERPRINT: + projection = new String[] { Keys._ID, Keys.KEY_ID, Keys.IS_MASTER_KEY, Keys.ALGORITHM, + Keys.KEY_SIZE, Keys.CAN_SIGN, Keys.CAN_ENCRYPT, }; + sortOrder = Keys.RANK + " ASC"; + + // use only master key for fingerprint + selection = Keys.IS_MASTER_KEY + " = 1 "; + + if (mKeyType == Id.type.public_key) { + uri = Keys.buildPublicKeysUri(String.valueOf(keyRingRowId)); + } else { + uri = Keys.buildSecretKeysUri(String.valueOf(keyRingRowId)); + } + break; + + case CHILD_KEY: + projection = new String[] { Keys._ID, Keys.KEY_ID, Keys.IS_MASTER_KEY, Keys.ALGORITHM, + Keys.KEY_SIZE, Keys.CAN_SIGN, Keys.CAN_ENCRYPT, }; + sortOrder = Keys.RANK + " ASC"; + + if (mKeyType == Id.type.public_key) { + uri = Keys.buildPublicKeysUri(String.valueOf(keyRingRowId)); + } else { + uri = Keys.buildSecretKeysUri(String.valueOf(keyRingRowId)); + } + + break; + + case CHILD_USER_ID: + projection = new String[] { UserIds._ID, UserIds.USER_ID, UserIds.RANK, }; + sortOrder = UserIds.RANK + " ASC"; + + // not the main user id + selection = UserIds.RANK + " > 0 "; + + if (mKeyType == Id.type.public_key) { + uri = UserIds.buildPublicUserIdsUri(String.valueOf(keyRingRowId)); + } else { + uri = UserIds.buildSecretUserIdsUri(String.valueOf(keyRingRowId)); + } + + break; + + default: + return null; + + } + + return mContext.getContentResolver().query(uri, projection, selection, null, sortOrder); + } + +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java new file mode 100644 index 000000000..7c9ba5fe6 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui.widget; + +import org.sufficientlysecure.keychain.R; + +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; + +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); + } + } + } + + public void setEditorListener(EditorListener listener) { + mEditorListener = listener; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java new file mode 100644 index 000000000..7142e4426 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui.widget; + +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.PgpConversionHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener; +import org.sufficientlysecure.keychain.util.Choice; +import org.sufficientlysecure.keychain.R; + +import com.actionbarsherlock.app.SherlockFragmentActivity; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.Iterator; +import java.util.Vector; + +public class SectionView extends LinearLayout implements OnClickListener, EditorListener { + private LayoutInflater mInflater; + private View mAdd; + private ViewGroup mEditors; + private TextView mTitle; + private int mType = 0; + + private Choice mNewKeyAlgorithmChoice; + private int mNewKeySize; + + private SherlockFragmentActivity mActivity; + + private ProgressDialogFragment mGeneratingDialog; + + public SectionView(Context context) { + super(context); + mActivity = (SherlockFragmentActivity) context; + } + + public SectionView(Context context, AttributeSet attrs) { + super(context, attrs); + mActivity = (SherlockFragmentActivity) context; + } + + public ViewGroup getEditors() { + return mEditors; + } + + public void setType(int type) { + mType = type; + switch (type) { + case Id.type.user_id: { + mTitle.setText(R.string.section_userIds); + break; + } + + case Id.type.key: { + mTitle.setText(R.string.section_keys); + break; + } + + default: { + break; + } + } + } + + /** {@inheritDoc} */ + @Override + protected void onFinishInflate() { + mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + setDrawingCacheEnabled(true); + setAlwaysDrawnWithCacheEnabled(true); + + mAdd = findViewById(R.id.header); + mAdd.setOnClickListener(this); + + mEditors = (ViewGroup) findViewById(R.id.editors); + mTitle = (TextView) findViewById(R.id.title); + + updateEditorsVisible(); + super.onFinishInflate(); + } + + /** {@inheritDoc} */ + public void onDeleted(Editor editor) { + this.updateEditorsVisible(); + } + + protected void updateEditorsVisible() { + final boolean hasChildren = mEditors.getChildCount() > 0; + mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE); + } + + /** {@inheritDoc} */ + public void onClick(View v) { + switch (mType) { + case Id.type.user_id: { + UserIdEditor view = (UserIdEditor) mInflater.inflate(R.layout.edit_key_user_id_item, + mEditors, false); + view.setEditorListener(this); + if (mEditors.getChildCount() == 0) { + view.setIsMainUserId(true); + } + mEditors.addView(view); + break; + } + + case Id.type.key: { + AlertDialog.Builder dialog = new AlertDialog.Builder(getContext()); + + View view = mInflater.inflate(R.layout.create_key, null); + dialog.setView(view); + dialog.setTitle(R.string.title_createKey); + + boolean wouldBeMasterKey = (mEditors.getChildCount() == 0); + + final Spinner algorithm = (Spinner) view.findViewById(R.id.create_key_algorithm); + Vector<Choice> choices = new Vector<Choice>(); + choices.add(new Choice(Id.choice.algorithm.dsa, getResources().getString(R.string.dsa))); + if (!wouldBeMasterKey) { + choices.add(new Choice(Id.choice.algorithm.elgamal, getResources().getString( + R.string.elgamal))); + } + + choices.add(new Choice(Id.choice.algorithm.rsa, getResources().getString(R.string.rsa))); + + ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(getContext(), + android.R.layout.simple_spinner_item, choices); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + algorithm.setAdapter(adapter); + // make RSA the default + for (int i = 0; i < choices.size(); ++i) { + if (choices.get(i).getId() == Id.choice.algorithm.rsa) { + algorithm.setSelection(i); + break; + } + } + + final EditText keySize = (EditText) view.findViewById(R.id.create_key_size); + + dialog.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface di, int id) { + di.dismiss(); + try { + mNewKeySize = Integer.parseInt("" + keySize.getText()); + } catch (NumberFormatException e) { + mNewKeySize = 0; + } + + mNewKeyAlgorithmChoice = (Choice) algorithm.getSelectedItem(); + createKey(); + } + }); + + dialog.setCancelable(true); + dialog.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface di, int id) { + di.dismiss(); + } + }); + + dialog.create().show(); + break; + } + + default: { + break; + } + } + this.updateEditorsVisible(); + } + + public void setUserIds(Vector<String> list) { + if (mType != Id.type.user_id) { + return; + } + + mEditors.removeAllViews(); + for (String userId : list) { + UserIdEditor view = (UserIdEditor) mInflater.inflate(R.layout.edit_key_user_id_item, + mEditors, false); + view.setEditorListener(this); + view.setValue(userId); + if (mEditors.getChildCount() == 0) { + view.setIsMainUserId(true); + } + mEditors.addView(view); + } + + this.updateEditorsVisible(); + } + + public void setKeys(Vector<PGPSecretKey> list, Vector<Integer> usages) { + if (mType != Id.type.key) { + return; + } + + mEditors.removeAllViews(); + + // go through all keys and set view based on them + for (int i = 0; i < list.size(); i++) { + KeyEditor view = (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item, mEditors, + false); + view.setEditorListener(this); + boolean isMasterKey = (mEditors.getChildCount() == 0); + view.setValue(list.get(i), isMasterKey, usages.get(i)); + mEditors.addView(view); + } + + this.updateEditorsVisible(); + } + + private void createKey() { + // Send all information needed to service to edit key in other thread + Intent intent = new Intent(mActivity, KeychainIntentService.class); + + intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_GENERATE_KEY); + + // fill values for this action + Bundle data = new Bundle(); + + String passPhrase; + if (mEditors.getChildCount() > 0) { + PGPSecretKey masterKey = ((KeyEditor) mEditors.getChildAt(0)).getValue(); + passPhrase = PassphraseCacheService + .getCachedPassphrase(mActivity, masterKey.getKeyID()); + + data.putByteArray(KeychainIntentService.GENERATE_KEY_MASTER_KEY, + PgpConversionHelper.PGPSecretKeyToBytes(masterKey)); + } else { + passPhrase = ""; + } + data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE, passPhrase); + data.putInt(KeychainIntentService.GENERATE_KEY_ALGORITHM, mNewKeyAlgorithmChoice.getId()); + data.putInt(KeychainIntentService.GENERATE_KEY_KEY_SIZE, mNewKeySize); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // show progress dialog + mGeneratingDialog = ProgressDialogFragment.newInstance(R.string.progress_generating, + ProgressDialog.STYLE_SPINNER); + + // Message is received after generating is done in ApgService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(mActivity, mGeneratingDialog) { + public void handleMessage(Message message) { + // handle messages by standard ApgHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + // get new key from data bundle returned from service + Bundle data = message.getData(); + PGPSecretKeyRing newKeyRing = (PGPSecretKeyRing) PgpConversionHelper + .BytesToPGPKeyRing(data.getByteArray(KeychainIntentService.RESULT_NEW_KEY)); + + boolean isMasterKey = (mEditors.getChildCount() == 0); + + // take only the key from this ring + PGPSecretKey newKey = null; + @SuppressWarnings("unchecked") + Iterator<PGPSecretKey> it = newKeyRing.getSecretKeys(); + + if (isMasterKey) { + newKey = it.next(); + } else { + // first one is the master key + it.next(); + newKey = it.next(); + } + + // add view with new key + KeyEditor view = (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item, + mEditors, false); + view.setEditorListener(SectionView.this); + view.setValue(newKey, isMasterKey, -1); + mEditors.addView(view); + SectionView.this.updateEditorsVisible(); + } + }; + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + mGeneratingDialog.show(mActivity.getSupportFragmentManager(), "dialog"); + + // start service with intent + mActivity.startService(intent); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java new file mode 100644 index 000000000..da8ac94ee --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui.widget; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.R; + +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ListView; +import android.widget.TextView; + +public class SelectKeyCursorAdapter extends CursorAdapter { + + protected int mKeyType; + + private LayoutInflater mInflater; + private ListView mListView; + + public final static String PROJECTION_ROW_AVAILABLE = "available"; + public final static String PROJECTION_ROW_VALID = "valid"; + + @SuppressWarnings("deprecation") + public SelectKeyCursorAdapter(Context context, ListView listView, Cursor c, int keyType) { + super(context, c); + + mInflater = LayoutInflater.from(context); + mListView = listView; + mKeyType = keyType; + } + + public String getUserId(int position) { + mCursor.moveToPosition(position); + return mCursor.getString(mCursor.getColumnIndex(UserIds.USER_ID)); + } + + public long getMasterKeyId(int position) { + mCursor.moveToPosition(position); + return mCursor.getLong(mCursor.getColumnIndex(KeyRings.MASTER_KEY_ID)); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + boolean valid = cursor.getInt(cursor + .getColumnIndex(PROJECTION_ROW_VALID)) > 0; + + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + mainUserId.setText(R.string.unknownUserId); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + mainUserIdRest.setText(""); + TextView keyId = (TextView) view.findViewById(R.id.keyId); + keyId.setText(R.string.noKey); + TextView status = (TextView) view.findViewById(R.id.status); + status.setText(R.string.unknownStatus); + + String userId = cursor.getString(cursor.getColumnIndex(UserIds.USER_ID)); + if (userId != null) { + String[] userIdSplit = OtherHelper.splitUserId(userId); + + if (userIdSplit[1] != null) { + mainUserIdRest.setText(userIdSplit[1]); + } + mainUserId.setText(userIdSplit[0]); + } + + long masterKeyId = cursor.getLong(cursor.getColumnIndex(KeyRings.MASTER_KEY_ID)); + keyId.setText(PgpHelper.getSmallFingerPrint(masterKeyId)); + + if (mainUserIdRest.getText().length() == 0) { + mainUserIdRest.setVisibility(View.GONE); + } + + if (valid) { + if (mKeyType == Id.type.public_key) { + status.setText(R.string.canEncrypt); + } else { + status.setText(R.string.canSign); + } + } else { + if (cursor.getInt(cursor + .getColumnIndex(PROJECTION_ROW_AVAILABLE)) > 0) { + // has some CAN_ENCRYPT keys, but col(ROW_VALID) = 0, so must be revoked or + // expired + status.setText(R.string.expired); + } else { + status.setText(R.string.noKey); + } + } + + CheckBox selected = (CheckBox) view.findViewById(R.id.selected); + if (mKeyType == Id.type.public_key) { + selected.setVisibility(View.VISIBLE); + + if (!valid) { + mListView.setItemChecked(cursor.getPosition(), false); + } + + selected.setChecked(mListView.isItemChecked(cursor.getPosition())); + selected.setEnabled(valid); + } else { + selected.setVisibility(View.GONE); + } + + status.setText(status.getText() + " "); + + view.setEnabled(valid); + mainUserId.setEnabled(valid); + mainUserIdRest.setEnabled(valid); + keyId.setEnabled(valid); + status.setEnabled(valid); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.select_key_item, null); + } + +}
\ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java new file mode 100644 index 000000000..2215d29e7 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui.widget; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.sufficientlysecure.keychain.R; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.RadioButton; + +public class UserIdEditor extends LinearLayout implements Editor, OnClickListener { + private EditorListener mEditorListener = null; + + private ImageButton mDeleteButton; + private RadioButton mIsMainUserId; + private EditText mName; + private EditText mEmail; + private EditText mComment; + + // see http://www.regular-expressions.info/email.html + // RFC 2822 if we omit the syntax using double quotes and square brackets + // android.util.Patterns.EMAIL_ADDRESS is only available as of Android 2.2+ + private static final Pattern EMAIL_PATTERN = Pattern + .compile( + "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", + Pattern.CASE_INSENSITIVE); + + public static class NoNameException extends Exception { + static final long serialVersionUID = 0xf812773343L; + + public NoNameException(String message) { + super(message); + } + } + + public static class NoEmailException extends Exception { + static final long serialVersionUID = 0xf812773344L; + + public NoEmailException(String message) { + super(message); + } + } + + public static class InvalidEmailException extends Exception { + static final long serialVersionUID = 0xf812773345L; + + public InvalidEmailException(String message) { + super(message); + } + } + + public UserIdEditor(Context context) { + super(context); + } + + public UserIdEditor(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + setDrawingCacheEnabled(true); + setAlwaysDrawnWithCacheEnabled(true); + + mDeleteButton = (ImageButton) findViewById(R.id.delete); + mDeleteButton.setOnClickListener(this); + mIsMainUserId = (RadioButton) findViewById(R.id.isMainUserId); + mIsMainUserId.setOnClickListener(this); + + mName = (EditText) findViewById(R.id.name); + mEmail = (EditText) findViewById(R.id.email); + mComment = (EditText) findViewById(R.id.comment); + + super.onFinishInflate(); + } + + public void setValue(String userId) { + mName.setText(""); + mComment.setText(""); + mEmail.setText(""); + + Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$"); + Matcher matcher = withComment.matcher(userId); + if (matcher.matches()) { + mName.setText(matcher.group(1)); + mComment.setText(matcher.group(2)); + mEmail.setText(matcher.group(3)); + return; + } + + Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); + matcher = withoutComment.matcher(userId); + if (matcher.matches()) { + mName.setText(matcher.group(1)); + mEmail.setText(matcher.group(2)); + return; + } + } + + public String getValue() throws NoNameException, NoEmailException, InvalidEmailException { + String name = ("" + mName.getText()).trim(); + String email = ("" + mEmail.getText()).trim(); + String comment = ("" + mComment.getText()).trim(); + + if (email.length() > 0) { + Matcher emailMatcher = EMAIL_PATTERN.matcher(email); + if (!emailMatcher.matches()) { + throw new InvalidEmailException(getContext().getString(R.string.error_invalidEmail, + email)); + } + } + + String userId = name; + if (comment.length() > 0) { + userId += " (" + comment + ")"; + } + if (email.length() > 0) { + userId += " <" + email + ">"; + } + + if (userId.equals("")) { + // ok, empty one... + return userId; + } + + // otherwise make sure that name and email exist + if (name.equals("")) { + throw new NoNameException("need a name"); + } + + if (email.equals("")) { + throw new NoEmailException("need an email"); + } + + return userId; + } + + public void onClick(View v) { + final ViewGroup parent = (ViewGroup) getParent(); + if (v == mDeleteButton) { + boolean wasMainUserId = mIsMainUserId.isChecked(); + parent.removeView(this); + if (mEditorListener != null) { + mEditorListener.onDeleted(this); + } + if (wasMainUserId && parent.getChildCount() > 0) { + UserIdEditor editor = (UserIdEditor) parent.getChildAt(0); + editor.setIsMainUserId(true); + } + } else if (v == mIsMainUserId) { + for (int i = 0; i < parent.getChildCount(); ++i) { + UserIdEditor editor = (UserIdEditor) parent.getChildAt(i); + if (editor == this) { + editor.setIsMainUserId(true); + } else { + editor.setIsMainUserId(false); + } + } + } + } + + public void setIsMainUserId(boolean value) { + mIsMainUserId.setChecked(value); + } + + public boolean isMainUserId() { + return mIsMainUserId.isChecked(); + } + + public void setEditorListener(EditorListener listener) { + mEditorListener = listener; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Choice.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Choice.java new file mode 100644 index 000000000..9e0042c00 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Choice.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.util; + +public class Choice { + private String mName; + private int mId; + + public Choice() { + mId = -1; + mName = ""; + } + + public Choice(int id, String name) { + mId = id; + mName = name; + } + + public int getId() { + return mId; + } + + public String getName() { + return mName; + } + + @Override + public String toString() { + return mName; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/HkpKeyServer.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/HkpKeyServer.java new file mode 100644 index 000000000..586a03498 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/HkpKeyServer.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2011 Senecaso + * + * 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.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.sufficientlysecure.keychain.helper.PgpHelper; +import org.sufficientlysecure.keychain.helper.PgpMain; + +import android.text.Html; + +public class HkpKeyServer extends KeyServer { + private static class HttpError extends Exception { + private static final long serialVersionUID = 1718783705229428893L; + private int mCode; + private String mData; + + public HttpError(int code, String data) { + super("" + code + ": " + data); + mCode = code; + mData = data; + } + + public int getCode() { + return mCode; + } + + public String getData() { + return mData; + } + } + + private String mHost; + private short mPort = 11371; + + // example: + // pub 2048R/<a href="/pks/lookup?op=get&search=0x887DF4BE9F5C9090">9F5C9090</a> 2009-08-17 <a + // href="/pks/lookup?op=vindex&search=0x887DF4BE9F5C9090">Jörg Runge + // <joerg@joergrunge.de></a> + public static Pattern PUB_KEY_LINE = Pattern + .compile( + "pub +([0-9]+)([a-z]+)/.*?0x([0-9a-z]+).*? +([0-9-]+) +(.+)[\n\r]+((?: +.+[\n\r]+)*)", + Pattern.CASE_INSENSITIVE); + public static Pattern USER_ID_LINE = Pattern.compile("^ +(.+)$", Pattern.MULTILINE + | Pattern.CASE_INSENSITIVE); + + public HkpKeyServer(String host) { + mHost = host; + } + + public HkpKeyServer(String host, short port) { + mHost = host; + mPort = port; + } + + static private String readAll(InputStream in, String encoding) throws IOException { + ByteArrayOutputStream raw = new ByteArrayOutputStream(); + + byte buffer[] = new byte[1 << 16]; + int n = 0; + while ((n = in.read(buffer)) != -1) { + raw.write(buffer, 0, n); + } + + if (encoding == null) { + encoding = "utf8"; + } + return raw.toString(encoding); + } + + private String query(String request) throws QueryException, HttpError { + InetAddress ips[]; + try { + ips = InetAddress.getAllByName(mHost); + } catch (UnknownHostException e) { + throw new QueryException(e.toString()); + } + for (int i = 0; i < ips.length; ++i) { + try { + String url = "http://" + ips[i].getHostAddress() + ":" + mPort + request; + URL realUrl = new URL(url); + HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(25000); + conn.connect(); + int response = conn.getResponseCode(); + if (response >= 200 && response < 300) { + return readAll(conn.getInputStream(), conn.getContentEncoding()); + } else { + String data = readAll(conn.getErrorStream(), conn.getContentEncoding()); + throw new HttpError(response, data); + } + } catch (MalformedURLException e) { + // nothing to do, try next IP + } catch (IOException e) { + // nothing to do, try next IP + } + } + + throw new QueryException("querying server(s) for '" + mHost + "' failed"); + } + + @Override + public ArrayList<KeyInfo> search(String query) throws QueryException, TooManyResponses, + InsufficientQuery { + ArrayList<KeyInfo> results = new ArrayList<KeyInfo>(); + + if (query.length() < 3) { + throw new InsufficientQuery(); + } + + String encodedQuery; + try { + encodedQuery = URLEncoder.encode(query, "utf8"); + } catch (UnsupportedEncodingException e) { + return null; + } + String request = "/pks/lookup?op=index&search=" + encodedQuery; + + String data = null; + try { + data = query(request); + } catch (HttpError e) { + if (e.getCode() == 404) { + return results; + } else { + if (e.getData().toLowerCase().contains("no keys found")) { + return results; + } else if (e.getData().toLowerCase().contains("too many")) { + throw new TooManyResponses(); + } else if (e.getData().toLowerCase().contains("insufficient")) { + throw new InsufficientQuery(); + } + } + throw new QueryException("querying server(s) for '" + mHost + "' failed"); + } + + Matcher matcher = PUB_KEY_LINE.matcher(data); + while (matcher.find()) { + KeyInfo info = new KeyInfo(); + info.size = Integer.parseInt(matcher.group(1)); + info.algorithm = matcher.group(2); + info.keyId = PgpHelper.keyFromHex(matcher.group(3)); + info.fingerPrint = PgpHelper.getSmallFingerPrint(info.keyId); + String chunks[] = matcher.group(4).split("-"); + info.date = new GregorianCalendar(Integer.parseInt(chunks[0]), + Integer.parseInt(chunks[1]), Integer.parseInt(chunks[2])).getTime(); + info.userIds = new ArrayList<String>(); + if (matcher.group(5).startsWith("*** KEY")) { + info.revoked = matcher.group(5); + } else { + String tmp = matcher.group(5).replaceAll("<.*?>", ""); + tmp = Html.fromHtml(tmp).toString(); + info.userIds.add(tmp); + } + if (matcher.group(6).length() > 0) { + Matcher matcher2 = USER_ID_LINE.matcher(matcher.group(6)); + while (matcher2.find()) { + String tmp = matcher2.group(1).replaceAll("<.*?>", ""); + tmp = Html.fromHtml(tmp).toString(); + info.userIds.add(tmp); + } + } + results.add(info); + } + + return results; + } + + @Override + public String get(long keyId) throws QueryException { + HttpClient client = new DefaultHttpClient(); + try { + HttpGet get = new HttpGet("http://" + mHost + ":" + mPort + + "/pks/lookup?op=get&search=0x" + PgpHelper.keyToHex(keyId)); + + HttpResponse response = client.execute(get); + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + throw new QueryException("not found"); + } + + HttpEntity entity = response.getEntity(); + InputStream is = entity.getContent(); + String data = readAll(is, EntityUtils.getContentCharSet(entity)); + Matcher matcher = PgpMain.PGP_PUBLIC_KEY.matcher(data); + if (matcher.find()) { + return matcher.group(1); + } + } catch (IOException e) { + // nothing to do, better luck on the next keyserver + } finally { + client.getConnectionManager().shutdown(); + } + + return null; + } + + @Override + public void add(String armouredText) throws AddKeyException { + HttpClient client = new DefaultHttpClient(); + try { + HttpPost post = new HttpPost("http://" + mHost + ":" + mPort + "/pks/add"); + + List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2); + nameValuePairs.add(new BasicNameValuePair("keytext", armouredText)); + post.setEntity(new UrlEncodedFormEntity(nameValuePairs)); + + HttpResponse response = client.execute(post); + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + throw new AddKeyException(); + } + } catch (IOException e) { + // nothing to do, better luck on the next keyserver + } finally { + client.getConnectionManager().shutdown(); + } + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/InputData.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/InputData.java new file mode 100644 index 000000000..f5bcbf88a --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/InputData.java @@ -0,0 +1,40 @@ +/* + * 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.util; + +import java.io.InputStream; + + +public class InputData { + private PositionAwareInputStream mInputStream; + private long mSize; + + public InputData(InputStream inputStream, long size) { + mInputStream = new PositionAwareInputStream(inputStream); + mSize = size; + } + + public InputStream getInputStream() { + return mInputStream; + } + + public long getSize() { + return mSize; + } + + public long getStreamPosition() { + return mInputStream.position(); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/IterableIterator.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/IterableIterator.java new file mode 100644 index 000000000..caaa07524 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/IterableIterator.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.util; + +import java.util.Iterator; + +public class IterableIterator<T> implements Iterable<T> { + private Iterator<T> mIter; + + public IterableIterator(Iterator<T> iter) { + mIter = iter; + } + + public Iterator<T> iterator() { + return mIter; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/KeyServer.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/KeyServer.java new file mode 100644 index 000000000..cdc188887 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/KeyServer.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 Senecaso + * + * 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.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import android.os.Parcel; +import android.os.Parcelable; + +public abstract class KeyServer { + static public class QueryException extends Exception { + private static final long serialVersionUID = 2703768928624654512L; + + public QueryException(String message) { + super(message); + } + } + + static public class TooManyResponses extends Exception { + private static final long serialVersionUID = 2703768928624654513L; + } + + static public class InsufficientQuery extends Exception { + private static final long serialVersionUID = 2703768928624654514L; + } + + static public class AddKeyException extends Exception { + private static final long serialVersionUID = -507574859137295530L; + } + + static public class KeyInfo implements Serializable, Parcelable { + private static final long serialVersionUID = -7797972113284992662L; + public ArrayList<String> userIds; + public String revoked; + public Date date; + public String fingerPrint; + public long keyId; + public int size; + public String algorithm; + + public KeyInfo() { + userIds = new ArrayList<String>(); + } + + public KeyInfo(Parcel in) { + this(); + + in.readStringList(this.userIds); + this.revoked = in.readString(); + this.date = (Date) in.readSerializable(); + this.fingerPrint = in.readString(); + this.keyId = in.readLong(); + this.size = in.readInt(); + this.algorithm = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringList(userIds); + dest.writeString(revoked); + dest.writeSerializable(date); + dest.writeString(fingerPrint); + dest.writeLong(keyId); + dest.writeInt(size); + dest.writeString(algorithm); + } + } + + abstract List<KeyInfo> search(String query) throws QueryException, TooManyResponses, + InsufficientQuery; + + abstract String get(long keyId) throws QueryException; + + abstract void add(String armouredText) throws AddKeyException; +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Log.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Log.java new file mode 100644 index 000000000..cad01d708 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Log.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de> + * + * 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.util; + +import org.sufficientlysecure.keychain.Constants; + +/** + * Wraps Android Logging to enable or disable debug output using Constants + * + */ +public final class Log { + + public static void v(String tag, String msg) { + if (Constants.DEBUG) { + android.util.Log.v(tag, msg); + } + } + + public static void v(String tag, String msg, Throwable tr) { + if (Constants.DEBUG) { + android.util.Log.v(tag, msg, tr); + } + } + + public static void d(String tag, String msg) { + if (Constants.DEBUG) { + android.util.Log.d(tag, msg); + } + } + + public static void d(String tag, String msg, Throwable tr) { + if (Constants.DEBUG) { + android.util.Log.d(tag, msg, tr); + } + } + + public static void i(String tag, String msg) { + if (Constants.DEBUG) { + android.util.Log.i(tag, msg); + } + } + + public static void i(String tag, String msg, Throwable tr) { + if (Constants.DEBUG) { + android.util.Log.i(tag, msg, tr); + } + } + + public static void w(String tag, String msg) { + android.util.Log.w(tag, msg); + } + + public static void w(String tag, String msg, Throwable tr) { + android.util.Log.w(tag, msg, tr); + } + + public static void w(String tag, Throwable tr) { + android.util.Log.w(tag, tr); + } + + public static void e(String tag, String msg) { + android.util.Log.e(tag, msg); + } + + public static void e(String tag, String msg, Throwable tr) { + android.util.Log.e(tag, msg, tr); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java new file mode 100644 index 000000000..a783d7820 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java @@ -0,0 +1,81 @@ +/* + * 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.util; + +import java.io.IOException; +import java.io.InputStream; + +public class PositionAwareInputStream extends InputStream { + private InputStream mStream; + private long mPosition; + + public PositionAwareInputStream(InputStream in) { + mStream = in; + mPosition = 0; + } + + @Override + public int read() throws IOException { + int ch = mStream.read(); + ++mPosition; + return ch; + } + + @Override + public int available() throws IOException { + return mStream.available(); + } + + @Override + public void close() throws IOException { + mStream.close(); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int read(byte[] b) throws IOException { + int result = mStream.read(b); + mPosition += result; + return result; + } + + @Override + public int read(byte[] b, int offset, int length) throws IOException { + int result = mStream.read(b, offset, length); + mPosition += result; + return result; + } + + @Override + public synchronized void reset() throws IOException { + mStream.reset(); + mPosition = 0; + } + + @Override + public long skip(long n) throws IOException { + long result = mStream.skip(n); + mPosition += result; + return result; + } + + public long position() { + return mPosition; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Primes.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Primes.java new file mode 100644 index 000000000..f503227a3 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Primes.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.util; + +import java.math.BigInteger; + +/** + * Primes for ElGamal + */ +public final class Primes { + // taken from http://www.ietf.org/rfc/rfc3526.txt + public static final String P1536 = + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" + + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" + + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" + + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" + + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" + + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" + + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + + "670C354E 4ABC9804 F1746C08 CA237327 FFFFFFFF FFFFFFFF"; + + public static final String P2048 = + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" + + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" + + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" + + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" + + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" + + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" + + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + + "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" + + "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" + + "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" + + "15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"; + + public static final String P3072 = + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" + + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" + + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" + + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" + + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" + + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" + + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + + "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" + + "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" + + "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" + + "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" + + "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" + + "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" + + "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + + "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" + + "43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF"; + + public static final String P4096 = + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" + + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" + + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" + + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" + + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" + + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" + + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + + "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" + + "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" + + "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" + + "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" + + "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" + + "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" + + "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + + "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" + + "43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7" + + "88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA" + + "2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6" + + "287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED" + + "1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9" + + "93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199" + + "FFFFFFFF FFFFFFFF"; + + public static final String P6144 = + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" + + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" + + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" + + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" + + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" + + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" + + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + + "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" + + "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" + + "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" + + "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" + + "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" + + "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" + + "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + + "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" + + "43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7" + + "88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA" + + "2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6" + + "287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED" + + "1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9" + + "93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" + + "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD" + + "F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831" + + "179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B" + + "DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF" + + "5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6" + + "D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3" + + "23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" + + "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328" + + "06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C" + + "DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE" + + "12BF2D5B 0B7474D6 E694F91E 6DCC4024 FFFFFFFF FFFFFFFF"; + + public static final String P8192 = + "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" + + "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" + + "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" + + "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" + + "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" + + "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" + + "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + + "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" + + "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" + + "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" + + "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" + + "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" + + "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" + + "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + + "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" + + "43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7" + + "88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA" + + "2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6" + + "287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED" + + "1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9" + + "93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" + + "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD" + + "F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831" + + "179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B" + + "DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF" + + "5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6" + + "D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3" + + "23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" + + "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328" + + "06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C" + + "DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE" + + "12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4" + + "38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300" + + "741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568" + + "3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9" + + "22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B" + + "4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A" + + "062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36" + + "4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1" + + "B1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92" + + "4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47" + + "9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71" + + "60C980DD 98EDD3DF FFFFFFFF FFFFFFFF"; + + public static BigInteger getBestPrime(int keySize) { + String primeString; + if (keySize >= (8192 + 6144) / 2) { + primeString = P8192; + } else if (keySize >= (6144 + 4096) / 2) { + primeString = P6144; + } else if (keySize >= (4096 + 3072) / 2) { + primeString = P4096; + } else if (keySize >= (3072 + 2048) / 2) { + primeString = P3072; + } else if (keySize >= (2048 + 1536) / 2) { + primeString = P2048; + } else { + primeString = P1536; + } + + return new BigInteger(primeString.replaceAll(" ", ""), 16); + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java new file mode 100644 index 000000000..26c05ad0a --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 Thialfihar <thi@thialfihar.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.util; + +public interface ProgressDialogUpdater { + void setProgress(String message, int current, int total); + + void setProgress(int resourceId, int current, int total); + + void setProgress(int current, int total); +} |