diff options
author | Dominik Schürmann <dominik@dominikschuermann.de> | 2014-09-17 21:51:25 +0200 |
---|---|---|
committer | Dominik Schürmann <dominik@dominikschuermann.de> | 2014-09-17 21:51:25 +0200 |
commit | b09d222f3416d155153a681ed256c46fbf5b386a (patch) | |
tree | 2fd26f521ebc2d5fc29a2bb0cb83c0d3b91319b1 /OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util | |
parent | a139be29ba556468ca282c96e5c985762c466b5b (diff) | |
download | open-keychain-b09d222f3416d155153a681ed256c46fbf5b386a.tar.gz open-keychain-b09d222f3416d155153a681ed256c46fbf5b386a.tar.bz2 open-keychain-b09d222f3416d155153a681ed256c46fbf5b386a.zip |
package reordering: merge util and helper, there were no real difference; created ui.util for everything related to formatting
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util')
12 files changed, 1661 insertions, 202 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java new file mode 100644 index 000000000..1e2a35be2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ContactHelper.java @@ -0,0 +1,467 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.util; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.annotation.TargetApi; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Build; +import android.provider.ContactsContract; +import android.util.Patterns; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.KeyRing; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.provider.KeychainContract; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ContactHelper { + + public static final String[] KEYS_TO_CONTACT_PROJECTION = new String[]{ + KeychainContract.KeyRings.USER_ID, + KeychainContract.KeyRings.FINGERPRINT, + KeychainContract.KeyRings.KEY_ID, + KeychainContract.KeyRings.MASTER_KEY_ID, + KeychainContract.KeyRings.EXPIRY, + KeychainContract.KeyRings.IS_REVOKED}; + public static final String[] USER_IDS_PROJECTION = new String[]{ + KeychainContract.UserIds.USER_ID + }; + + public static final String NON_REVOKED_SELECTION = KeychainContract.UserIds.IS_REVOKED + "=0"; + + public static final String[] ID_PROJECTION = new String[]{ContactsContract.RawContacts._ID}; + public static final String[] SOURCE_ID_PROJECTION = new String[]{ContactsContract.RawContacts.SOURCE_ID}; + + public static final String ACCOUNT_TYPE_AND_SOURCE_ID_SELECTION = + ContactsContract.RawContacts.ACCOUNT_TYPE + "=? AND " + ContactsContract.RawContacts.SOURCE_ID + "=?"; + public static final String ACCOUNT_TYPE_SELECTION = ContactsContract.RawContacts.ACCOUNT_TYPE + "=?"; + public static final String RAW_CONTACT_AND_MIMETYPE_SELECTION = + ContactsContract.Data.RAW_CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "=?"; + public static final String ID_SELECTION = ContactsContract.RawContacts._ID + "=?"; + + private static final Map<String, Bitmap> photoCache = new HashMap<String, Bitmap>(); + + public static List<String> getPossibleUserEmails(Context context) { + Set<String> accountMails = getAccountEmails(context); + accountMails.addAll(getMainProfileContactEmails(context)); + // now return the Set (without duplicates) as a List + return new ArrayList<String>(accountMails); + } + + public static List<String> getPossibleUserNames(Context context) { + Set<String> accountMails = getAccountEmails(context); + Set<String> names = getContactNamesFromEmails(context, accountMails); + names.addAll(getMainProfileContactName(context)); + return new ArrayList<String>(names); + } + + /** + * Get emails from AccountManager + * + * @param context + * @return + */ + private static Set<String> getAccountEmails(Context context) { + final Account[] accounts = AccountManager.get(context).getAccounts(); + final Set<String> emailSet = new HashSet<String>(); + for (Account account : accounts) { + if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { + emailSet.add(account.name); + } + } + return emailSet; + } + + /** + * Search for contact names based on a list of emails (to find out the names of the + * device owner based on the email addresses from AccountsManager) + * + * @param context + * @param emails + * @return + */ + private static Set<String> getContactNamesFromEmails(Context context, Set<String> emails) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + Set<String> names = new HashSet<String>(); + for (String email : emails) { + ContentResolver resolver = context.getContentResolver(); + Cursor profileCursor = resolver.query( + ContactsContract.CommonDataKinds.Email.CONTENT_URI, + new String[]{ContactsContract.CommonDataKinds.Email.ADDRESS, + ContactsContract.Contacts.DISPLAY_NAME}, + ContactsContract.CommonDataKinds.Email.ADDRESS + "=?", + new String[]{email}, null + ); + if (profileCursor == null) return null; + + Set<String> currNames = new HashSet<String>(); + while (profileCursor.moveToNext()) { + String name = profileCursor.getString(1); + Log.d(Constants.TAG, "name" + name); + if (name != null) { + currNames.add(name); + } + } + profileCursor.close(); + names.addAll(currNames); + } + return names; + } else { + return new HashSet<String>(); + } + } + + /** + * Retrieves the emails of the primary profile contact + * http://developer.android.com/reference/android/provider/ContactsContract.Profile.html + * + * @param context + * @return + */ + private static Set<String> getMainProfileContactEmails(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + ContentResolver resolver = context.getContentResolver(); + Cursor profileCursor = resolver.query( + Uri.withAppendedPath( + ContactsContract.Profile.CONTENT_URI, + ContactsContract.Contacts.Data.CONTENT_DIRECTORY), + new String[]{ContactsContract.CommonDataKinds.Email.ADDRESS, + ContactsContract.CommonDataKinds.Email.IS_PRIMARY}, + + // Selects only email addresses + ContactsContract.Contacts.Data.MIMETYPE + "=?", + new String[]{ + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE, + }, + // Show primary rows first. Note that there won't be a primary email address if the + // user hasn't specified one. + ContactsContract.Contacts.Data.IS_PRIMARY + " DESC" + ); + if (profileCursor == null) return null; + + Set<String> emails = new HashSet<String>(); + while (profileCursor.moveToNext()) { + String email = profileCursor.getString(0); + if (email != null) { + emails.add(email); + } + } + profileCursor.close(); + return emails; + } else { + return new HashSet<String>(); + } + } + + /** + * Retrieves the name of the primary profile contact + * http://developer.android.com/reference/android/provider/ContactsContract.Profile.html + * + * @param context + * @return + */ + private static List<String> getMainProfileContactName(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + ContentResolver resolver = context.getContentResolver(); + Cursor profileCursor = resolver.query(ContactsContract.Profile.CONTENT_URI, + new String[]{ContactsContract.Profile.DISPLAY_NAME}, + null, null, null); + if (profileCursor == null) return null; + + Set<String> names = new HashSet<String>(); + // should only contain one entry! + while (profileCursor.moveToNext()) { + String name = profileCursor.getString(0); + if (name != null) { + names.add(name); + } + } + profileCursor.close(); + return new ArrayList<String>(names); + } else { + return new ArrayList<String>(); + } + } + + public static List<String> getContactMails(Context context) { + ContentResolver resolver = context.getContentResolver(); + Cursor mailCursor = resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, + new String[]{ContactsContract.CommonDataKinds.Email.DATA}, + null, null, null); + if (mailCursor == null) return new ArrayList<String>(); + + Set<String> mails = new HashSet<String>(); + while (mailCursor.moveToNext()) { + String email = mailCursor.getString(0); + if (email != null) { + mails.add(email); + } + } + mailCursor.close(); + return new ArrayList<String>(mails); + } + + public static List<String> getContactNames(Context context) { + ContentResolver resolver = context.getContentResolver(); + Cursor cursor = resolver.query(ContactsContract.Contacts.CONTENT_URI, + new String[]{ContactsContract.Contacts.DISPLAY_NAME}, + null, null, null); + if (cursor == null) return new ArrayList<String>(); + + Set<String> names = new HashSet<String>(); + while (cursor.moveToNext()) { + String name = cursor.getString(0); + if (name != null) { + names.add(name); + } + } + cursor.close(); + return new ArrayList<String>(names); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public static Uri dataUriFromContactUri(Context context, Uri contactUri) { + Cursor contactMasterKey = context.getContentResolver().query(contactUri, + new String[]{ContactsContract.Data.DATA2}, null, null, null, null); + if (contactMasterKey != null) { + if (contactMasterKey.moveToNext()) { + return KeychainContract.KeyRings.buildGenericKeyRingUri(contactMasterKey.getLong(0)); + } + contactMasterKey.close(); + } + return null; + } + + public static Bitmap photoFromFingerprint(ContentResolver contentResolver, String fingerprint) { + if (fingerprint == null) return null; + if (!photoCache.containsKey(fingerprint)) { + photoCache.put(fingerprint, loadPhotoFromFingerprint(contentResolver, fingerprint)); + } + return photoCache.get(fingerprint); + } + + private static Bitmap loadPhotoFromFingerprint(ContentResolver contentResolver, String fingerprint) { + if (fingerprint == null) return null; + try { + int rawContactId = findRawContactId(contentResolver, fingerprint); + if (rawContactId == -1) return null; + Uri rawContactUri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId); + Uri contactUri = ContactsContract.RawContacts.getContactLookupUri(contentResolver, rawContactUri); + InputStream photoInputStream = + ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, contactUri); + if (photoInputStream == null) return null; + return BitmapFactory.decodeStream(photoInputStream); + } catch (Throwable ignored) { + return null; + } + } + + /** + * Write the current Keychain to the contact db + */ + public static void writeKeysToContacts(Context context) { + ContentResolver resolver = context.getContentResolver(); + Set<String> contactFingerprints = getRawContactFingerprints(resolver); + + // Load all Keys from OK + Cursor cursor = resolver.query(KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), KEYS_TO_CONTACT_PROJECTION, + null, null, null); + if (cursor != null) { + while (cursor.moveToNext()) { + String[] primaryUserId = KeyRing.splitUserId(cursor.getString(0)); + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(cursor.getBlob(1)); + contactFingerprints.remove(fingerprint); + String keyIdShort = KeyFormattingUtils.convertKeyIdToHexShort(cursor.getLong(2)); + long masterKeyId = cursor.getLong(3); + boolean isExpired = !cursor.isNull(4) && new Date(cursor.getLong(4) * 1000).before(new Date()); + boolean isRevoked = cursor.getInt(5) > 0; + int rawContactId = findRawContactId(resolver, fingerprint); + ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); + + // Do not store expired or revoked keys in contact db - and remove them if they already exist + if (isExpired || isRevoked) { + if (rawContactId != -1) { + resolver.delete(ContactsContract.RawContacts.CONTENT_URI, ID_SELECTION, + new String[]{Integer.toString(rawContactId)}); + } + } else { + + // Create a new rawcontact with corresponding key if it does not exist yet + if (rawContactId == -1) { + insertContact(ops, context, fingerprint); + writeContactKey(ops, context, rawContactId, masterKeyId, keyIdShort); + } + + // We always update the display name (which is derived from primary user id) + // and email addresses from user id + writeContactDisplayName(ops, rawContactId, primaryUserId[0]); + writeContactEmail(ops, resolver, rawContactId, masterKeyId); + try { + resolver.applyBatch(ContactsContract.AUTHORITY, ops); + } catch (Exception e) { + Log.w(Constants.TAG, e); + } + } + } + cursor.close(); + } + + // Delete fingerprints that are no longer present in OK + for (String fingerprint : contactFingerprints) { + resolver.delete(ContactsContract.RawContacts.CONTENT_URI, ACCOUNT_TYPE_AND_SOURCE_ID_SELECTION, + new String[]{Constants.ACCOUNT_TYPE, fingerprint}); + } + + } + + /** + * @return a set of all key fingerprints currently present in the contact db + */ + private static Set<String> getRawContactFingerprints(ContentResolver resolver) { + HashSet<String> result = new HashSet<String>(); + Cursor fingerprints = resolver.query(ContactsContract.RawContacts.CONTENT_URI, SOURCE_ID_PROJECTION, + ACCOUNT_TYPE_SELECTION, new String[]{Constants.ACCOUNT_TYPE}, null); + if (fingerprints != null) { + while (fingerprints.moveToNext()) { + result.add(fingerprints.getString(0)); + } + fingerprints.close(); + } + return result; + } + + /** + * This will search the contact db for a raw contact with a given fingerprint + * + * @return raw contact id or -1 if not found + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private static int findRawContactId(ContentResolver resolver, String fingerprint) { + int rawContactId = -1; + Cursor raw = resolver.query(ContactsContract.RawContacts.CONTENT_URI, ID_PROJECTION, + ACCOUNT_TYPE_AND_SOURCE_ID_SELECTION, new String[]{Constants.ACCOUNT_TYPE, fingerprint}, null, null); + if (raw != null) { + if (raw.moveToNext()) { + rawContactId = raw.getInt(0); + } + raw.close(); + } + return rawContactId; + } + + /** + * Creates a empty raw contact with a given fingerprint + */ + private static void insertContact(ArrayList<ContentProviderOperation> ops, Context context, String fingerprint) { + ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) + .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, Constants.ACCOUNT_NAME) + .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE) + .withValue(ContactsContract.RawContacts.SOURCE_ID, fingerprint) + .build()); + } + + /** + * Adds a key id to the given raw contact. + * <p/> + * This creates the link to OK in contact details + */ + private static void writeContactKey(ArrayList<ContentProviderOperation> ops, Context context, int rawContactId, + long masterKeyId, String keyIdShort) { + ops.add(referenceRawContact(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI), rawContactId) + .withValue(ContactsContract.Data.MIMETYPE, Constants.CUSTOM_CONTACT_DATA_MIME_TYPE) + .withValue(ContactsContract.Data.DATA1, context.getString(R.string.contact_show_key, keyIdShort)) + .withValue(ContactsContract.Data.DATA2, masterKeyId) + .build()); + } + + /** + * Write all known email addresses of a key (derived from user ids) to a given raw contact + */ + private static void writeContactEmail(ArrayList<ContentProviderOperation> ops, ContentResolver resolver, + int rawContactId, long masterKeyId) { + ops.add(selectByRawContactAndItemType(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI), + rawContactId, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE).build()); + Cursor ids = resolver.query(KeychainContract.UserIds.buildUserIdsUri(masterKeyId), + USER_IDS_PROJECTION, NON_REVOKED_SELECTION, null, null); + if (ids != null) { + while (ids.moveToNext()) { + String[] userId = KeyRing.splitUserId(ids.getString(0)); + if (userId[1] != null) { + ops.add(referenceRawContact(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI), + rawContactId) + .withValue(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Email.DATA, userId[1]) + .build()); + } + } + ids.close(); + } + } + + private static void writeContactDisplayName(ArrayList<ContentProviderOperation> ops, int rawContactId, + String displayName) { + if (displayName != null) { + ops.add(insertOrUpdateForRawContact(ContactsContract.Data.CONTENT_URI, rawContactId, + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) + .build()); + } + } + + private static ContentProviderOperation.Builder referenceRawContact(ContentProviderOperation.Builder builder, + int rawContactId) { + return rawContactId == -1 ? + builder.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) : + builder.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactId); + } + + private static ContentProviderOperation.Builder insertOrUpdateForRawContact(Uri uri, int rawContactId, + String itemType) { + if (rawContactId == -1) { + return referenceRawContact(ContentProviderOperation.newInsert(uri), rawContactId).withValue( + ContactsContract.Data.MIMETYPE, itemType); + } else { + return selectByRawContactAndItemType(ContentProviderOperation.newUpdate(uri), rawContactId, itemType); + } + } + + private static ContentProviderOperation.Builder selectByRawContactAndItemType( + ContentProviderOperation.Builder builder, int rawContactId, String itemType) { + return builder.withSelection(RAW_CONTACT_AND_MIMETYPE_SELECTION, + new String[]{Integer.toString(rawContactId), itemType}); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java new file mode 100644 index 000000000..33541718e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.util; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Messenger; + +import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; +import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; +import org.sufficientlysecure.keychain.keyimport.Keyserver; +import org.sufficientlysecure.keychain.service.KeychainIntentService; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class EmailKeyHelper { + + public static void importContacts(Context context, Messenger messenger) { + importAll(context, messenger, ContactHelper.getContactMails(context)); + } + + public static void importAll(Context context, Messenger messenger, List<String> mails) { + Set<ImportKeysListEntry> keys = new HashSet<ImportKeysListEntry>(); + for (String mail : mails) { + keys.addAll(getEmailKeys(context, mail)); + } + importKeys(context, messenger, new ArrayList<ImportKeysListEntry>(keys)); + } + + public static List<ImportKeysListEntry> getEmailKeys(Context context, String mail) { + Set<ImportKeysListEntry> keys = new HashSet<ImportKeysListEntry>(); + + // Try _hkp._tcp SRV record first + String[] mailparts = mail.split("@"); + if (mailparts.length == 2) { + HkpKeyserver hkp = HkpKeyserver.resolve(mailparts[1]); + if (hkp != null) { + keys.addAll(getEmailKeys(mail, hkp)); + } + } + + if (keys.isEmpty()) { + // Most users don't have the SRV record, so ask a default server as well + String server = Preferences.getPreferences(context).getPreferredKeyserver(); + if (server != null) { + HkpKeyserver hkp = new HkpKeyserver(server); + keys.addAll(getEmailKeys(mail, hkp)); + } + } + return new ArrayList<ImportKeysListEntry>(keys); + } + + private static void importKeys(Context context, Messenger messenger, List<ImportKeysListEntry> keys) { + Intent importIntent = new Intent(context, KeychainIntentService.class); + importIntent.setAction(KeychainIntentService.ACTION_DOWNLOAD_AND_IMPORT_KEYS); + Bundle importData = new Bundle(); + importData.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST, + new ArrayList<ImportKeysListEntry>(keys)); + importIntent.putExtra(KeychainIntentService.EXTRA_DATA, importData); + importIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + context.startService(importIntent); + } + + public static List<ImportKeysListEntry> getEmailKeys(String mail, Keyserver keyServer) { + Set<ImportKeysListEntry> keys = new HashSet<ImportKeysListEntry>(); + try { + for (ImportKeysListEntry key : keyServer.search(mail)) { + if (key.isRevoked() || key.isExpired()) continue; + for (String userId : key.getUserIds()) { + if (userId.toLowerCase().contains(mail.toLowerCase(Locale.ENGLISH))) { + keys.add(key); + } + } + } + } catch (Keyserver.QueryFailedException ignored) { + } catch (Keyserver.QueryNeedsRepairException ignored) { + } + return new ArrayList<ImportKeysListEntry>(keys); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java new file mode 100644 index 000000000..3c100e272 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.util; + +import android.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.support.v7.app.ActionBarActivity; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.exception.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.DeleteKeyDialogFragment; + +import java.io.File; + +public class ExportHelper { + protected File mExportFile; + + ActionBarActivity mActivity; + + public ExportHelper(ActionBarActivity activity) { + super(); + this.mActivity = activity; + } + + public void deleteKey(Uri dataUri, Handler deleteHandler) { + try { + long masterKeyId = new ProviderHelper(mActivity).getCachedPublicKeyRing(dataUri) + .extractOrGetMasterKeyId(); + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(deleteHandler); + DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger, + new long[]{ masterKeyId }); + deleteKeyDialog.show(mActivity.getSupportFragmentManager(), "deleteKeyDialog"); + } catch (PgpGeneralException e) { + Log.e(Constants.TAG, "key not found!", e); + } + } + + /** + * Show dialog where to export keys + */ + public void showExportKeysDialog(final long[] masterKeyIds, final File exportFile, + final boolean showSecretCheckbox) { + mExportFile = exportFile; + + String title = null; + if (masterKeyIds == null) { + // export all keys + title = mActivity.getString(R.string.title_export_keys); + } else { + // export only key specified at data uri + title = mActivity.getString(R.string.title_export_key); + } + + String message = mActivity.getString(R.string.specify_file_to_export_to); + String checkMsg = showSecretCheckbox ? + mActivity.getString(R.string.also_export_secret_keys) : null; + + FileHelper.saveFile(new FileHelper.FileDialogCallback() { + @Override + public void onFileSelected(File file, boolean checked) { + mExportFile = file; + exportKeys(masterKeyIds, checked); + } + }, mActivity.getSupportFragmentManager() ,title, message, exportFile, checkMsg); + } + + /** + * Export keys + */ + public void exportKeys(long[] masterKeyIds, boolean exportSecret) { + Log.d(Constants.TAG, "exportKeys started"); + + // Send all information needed to service to export key in other thread + final Intent intent = new Intent(mActivity, KeychainIntentService.class); + + intent.setAction(KeychainIntentService.ACTION_EXPORT_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + + data.putString(KeychainIntentService.EXPORT_FILENAME, mExportFile.getAbsolutePath()); + data.putBoolean(KeychainIntentService.EXPORT_SECRET, exportSecret); + + if (masterKeyIds == null) { + data.putBoolean(KeychainIntentService.EXPORT_ALL, true); + } else { + data.putLongArray(KeychainIntentService.EXPORT_KEY_RING_MASTER_KEY_ID, masterKeyIds); + } + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after exporting is done in KeychainIntentService + KeychainIntentServiceHandler exportHandler = new KeychainIntentServiceHandler(mActivity, + mActivity.getString(R.string.progress_exporting), + ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentServiceHandler 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 = mActivity.getString(R.string.key_exported); + } else if (exported > 0) { + toastMessage = mActivity.getString(R.string.keys_exported, exported); + } else { + toastMessage = mActivity.getString(R.string.no_keys_exported); + } + Toast.makeText(mActivity, 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(mActivity); + + // start service with intent + mActivity.startService(intent); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java new file mode 100644 index 000000000..677acb1b8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.util; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.provider.DocumentsContract; +import android.provider.OpenableColumns; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment; + +import java.io.File; +import java.text.DecimalFormat; + +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 fragment + * @param last default selected Uri, not supported by all file managers + * @param mimeType can be text/plain for example + * @param requestCode requestCode used to identify the result coming back from file manager to + * onActivityResult() in your activity + */ + public static void openFile(Fragment fragment, Uri last, String mimeType, int requestCode) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + + intent.setData(last); + intent.setType(mimeType); + + try { + fragment.startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + // No compatible file manager was found. + Toast.makeText(fragment.getActivity(), R.string.no_filemanager_installed, + Toast.LENGTH_SHORT).show(); + } + } + + public static void saveFile(final FileDialogCallback callback, final FragmentManager fragmentManager, + final String title, final String message, final File defaultFile, + final String checkMsg) { + // Message is received after file is selected + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == FileDialogFragment.MESSAGE_OKAY) { + callback.onFileSelected( + new File(message.getData().getString(FileDialogFragment.MESSAGE_DATA_FILE)), + message.getData().getBoolean(FileDialogFragment.MESSAGE_DATA_CHECKED)); + } + } + }; + + // Create a new Messenger for the communication back + final Messenger messenger = new Messenger(returnHandler); + + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + @Override + public void run() { + FileDialogFragment fileDialog = FileDialogFragment.newInstance(messenger, title, message, + defaultFile, checkMsg); + + fileDialog.show(fragmentManager, "fileDialog"); + } + }); + } + + public static void saveFile(Fragment fragment, String title, String message, File defaultFile, int requestCode) { + saveFile(fragment, title, message, defaultFile, requestCode, null); + } + + public static void saveFile(final Fragment fragment, String title, String message, File defaultFile, + final int requestCode, String checkMsg) { + saveFile(new FileDialogCallback() { + @Override + public void onFileSelected(File file, boolean checked) { + Intent intent = new Intent(); + intent.setData(Uri.fromFile(file)); + fragment.onActivityResult(requestCode, Activity.RESULT_OK, intent); + } + }, fragment.getActivity().getSupportFragmentManager(), title, message, defaultFile, checkMsg); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void openDocument(Fragment fragment, String mimeType, int requestCode) { + openDocument(fragment, mimeType, false, requestCode); + } + + /** + * Opens the storage browser on Android 4.4 or later for opening a file + * + * @param fragment + * @param mimeType can be text/plain for example + * @param multiple allow file chooser to return multiple files + * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void openDocument(Fragment fragment, String mimeType, boolean multiple, int requestCode) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(mimeType); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple); + + fragment.startActivityForResult(intent, requestCode); + } + + /** + * Opens the storage browser on Android 4.4 or later for saving a file + * + * @param fragment + * @param mimeType can be text/plain for example + * @param suggestedName a filename desirable for the file to be saved + * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + public static void saveDocument(Fragment fragment, String mimeType, String suggestedName, int requestCode) { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(mimeType); + intent.putExtra("android.content.extra.SHOW_ADVANCED", true); // Note: This is not documented, but works + intent.putExtra(Intent.EXTRA_TITLE, suggestedName); + fragment.startActivityForResult(intent, requestCode); + } + + public static String getFilename(Context context, Uri uri) { + String filename = null; + try { + Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null); + + if (cursor != null) { + if (cursor.moveToNext()) { + filename = cursor.getString(0); + } + cursor.close(); + } + } catch (Exception ignored) { + // This happens in rare cases (eg: document deleted since selection) and should not cause a failure + } + if (filename == null) { + String[] split = uri.toString().split("/"); + filename = split[split.length - 1]; + } + return filename; + } + + public static long getFileSize(Context context, Uri uri) { + return getFileSize(context, uri, -1); + } + + public static long getFileSize(Context context, Uri uri, long def) { + long size = def; + try { + Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null); + + if (cursor != null) { + if (cursor.moveToNext()) { + size = cursor.getLong(0); + } + cursor.close(); + } + } catch (Exception ignored) { + // This happens in rare cases (eg: document deleted since selection) and should not cause a failure + } + return size; + } + + /** + * Retrieve thumbnail of file, document api feature and thus KitKat only + */ + public static Bitmap getThumbnail(Context context, Uri uri, Point size) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return DocumentsContract.getDocumentThumbnail(context.getContentResolver(), uri, size, null); + } else { + return null; + } + } + + public static String readableFileSize(long size) { + if (size <= 0) return "0"; + final String[] units = new String[]{"B", "KB", "MB", "GB", "TB"}; + int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); + return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; + } + + public static interface FileDialogCallback { + public void onFileSelected(File file, boolean checked); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Highlighter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Highlighter.java deleted file mode 100644 index eeeacf465..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Highlighter.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2014 Thialfihar <thi@thialfihar.org> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.util; - -import android.content.Context; -import android.text.Spannable; -import android.text.style.ForegroundColorSpan; - -import org.sufficientlysecure.keychain.R; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Highlighter { - private Context mContext; - private String mQuery; - - public Highlighter(Context context, String query) { - mContext = context; - mQuery = query; - } - - public Spannable highlight(String text) { - Spannable highlight = Spannable.Factory.getInstance().newSpannable(text); - - if (mQuery == null) { - return highlight; - } - - Pattern pattern = Pattern.compile("(?i)(" + mQuery.trim().replaceAll("\\s+", "|") + ")"); - Matcher matcher = pattern.matcher(text); - while (matcher.find()) { - highlight.setSpan( - new ForegroundColorSpan(mContext.getResources().getColor(R.color.emphasis)), - matcher.start(), - matcher.end(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - return highlight; - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java new file mode 100644 index 000000000..357a9603c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyUpdateHelper.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2014 Daniel Albert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.util; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Messenger; + +import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; + +import java.util.ArrayList; +import java.util.List; + +public class KeyUpdateHelper { + + public void updateAllKeys(Context context, KeychainIntentServiceHandler finishedHandler) { + UpdateTask updateTask = new UpdateTask(context, finishedHandler); + updateTask.execute(); + } + + private class UpdateTask extends AsyncTask<Void, Void, Void> { + private Context mContext; + private KeychainIntentServiceHandler mHandler; + + public UpdateTask(Context context, KeychainIntentServiceHandler handler) { + this.mContext = context; + this.mHandler = handler; + } + + @Override + protected Void doInBackground(Void... voids) { + ProviderHelper providerHelper = new ProviderHelper(mContext); + List<ImportKeysListEntry> keys = new ArrayList<ImportKeysListEntry>(); + String[] servers = Preferences.getPreferences(mContext).getKeyServers(); + + if (servers != null && servers.length > 0) { + // Load all the fingerprints in the database and prepare to import them + for (String fprint : providerHelper.getAllFingerprints(KeychainContract.KeyRings.buildUnifiedKeyRingsUri())) { + ImportKeysListEntry key = new ImportKeysListEntry(); + key.setFingerprintHex(fprint); + key.setBitStrength(1337); + key.addOrigin(servers[0]); + keys.add(key); + } + + // Start the service and update the keys + Intent importIntent = new Intent(mContext, KeychainIntentService.class); + importIntent.setAction(KeychainIntentService.ACTION_DOWNLOAD_AND_IMPORT_KEYS); + + Bundle importData = new Bundle(); + importData.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST, + new ArrayList<ImportKeysListEntry>(keys)); + importIntent.putExtra(KeychainIntentService.EXTRA_DATA, importData); + + importIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, new Messenger(mHandler)); + + mContext.startService(importIntent); + } + return null; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Log.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Log.java index 10c0fc4d0..4dda74ace 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Log.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Log.java @@ -17,8 +17,13 @@ package org.sufficientlysecure.keychain.util; +import android.os.Bundle; + import org.sufficientlysecure.keychain.Constants; +import java.util.Iterator; +import java.util.Set; + /** * Wraps Android Logging to enable or disable debug output using Constants */ @@ -80,4 +85,35 @@ public final class Log { android.util.Log.e(tag, msg, tr); } + + /** + * 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"); + } + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Notify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Notify.java deleted file mode 100644 index 22e3f5d66..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Notify.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.util; - -import android.app.Activity; -import android.content.res.Resources; - -import com.github.johnpersano.supertoasts.SuperCardToast; -import com.github.johnpersano.supertoasts.SuperToast; - -/** - * @author danielhass - * Notify wrapper which allows a more easy use of different notification libraries - */ -public class Notify { - - public static enum Style {OK, WARN, INFO, ERROR} - - /** - * Shows a simple in-layout notification with the CharSequence given as parameter - * @param activity - * @param text Text to show - * @param style Notification styling - */ - public static void showNotify(Activity activity, CharSequence text, Style style) { - - SuperCardToast st = new SuperCardToast(activity); - st.setText(text); - st.setDuration(SuperToast.Duration.MEDIUM); - switch (style){ - case OK: - st.setBackground(SuperToast.Background.GREEN); - break; - case WARN: - st.setBackground(SuperToast.Background.ORANGE); - break; - case ERROR: - st.setBackground(SuperToast.Background.RED); - break; - } - st.show(); - - } - - /** - * Shows a simple in-layout notification with the resource text from given id - * @param activity - * @param resId ResourceId of notification text - * @param style Notification styling - * @throws Resources.NotFoundException - */ - public static void showNotify(Activity activity, int resId, Style style) throws Resources.NotFoundException { - showNotify(activity, activity.getResources().getText(resId), style); - } -}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java new file mode 100644 index 000000000..35570ef6e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; + +import org.spongycastle.bcpg.CompressionAlgorithmTags; +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Constants.Pref; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.ListIterator; +import java.util.Vector; + +/** + * Singleton Implementation of a Preference Helper + */ +public class Preferences { + private static Preferences sPreferences; + private SharedPreferences mSharedPreferences; + + public static synchronized Preferences getPreferences(Context context) { + return getPreferences(context, false); + } + + public static synchronized Preferences getPreferences(Context context, boolean forceNew) { + if (sPreferences == null || forceNew) { + sPreferences = new Preferences(context); + } else { + // to make it safe for multiple processes, call getSharedPreferences everytime + sPreferences.updateSharedPreferences(context); + } + return sPreferences; + } + + private Preferences(Context context) { + updateSharedPreferences(context); + } + + public void updateSharedPreferences(Context context) { + // multi-process preferences + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mSharedPreferences = context.getSharedPreferences("APG.main", Context.MODE_MULTI_PROCESS); + } else { + 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.PASSPHRASE_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.PASSPHRASE_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, + CompressionAlgorithmTags.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, + CompressionAlgorithmTags.UNCOMPRESSED); + } + + public void setDefaultFileCompression(int value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putInt(Constants.Pref.DEFAULT_FILE_COMPRESSION, value); + editor.commit(); + } + + public boolean getDefaultAsciiArmor() { + return mSharedPreferences.getBoolean(Constants.Pref.DEFAULT_ASCII_ARMOR, false); + } + + public void setDefaultAsciiArmor(boolean value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Constants.Pref.DEFAULT_ASCII_ARMOR, value); + editor.commit(); + } + + public boolean getShowAdvancedTabs() { + return mSharedPreferences.getBoolean(Pref.SHOW_ADVANCED_TABS, false); + } + + public void setShowAdvancedTabs(boolean value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.SHOW_ADVANCED_TABS, value); + editor.commit(); + } + + public boolean getCachedConsolidate() { + return mSharedPreferences.getBoolean(Pref.CACHED_CONSOLIDATE, false); + } + + public void setCachedConsolidate(boolean value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.CACHED_CONSOLIDATE, value); + editor.commit(); + } + + public int getCachedConsolidateNumPublics() { + return mSharedPreferences.getInt(Pref.CACHED_CONSOLIDATE_PUBLICS, -1); + } + + public void setCachedConsolidateNumPublics(int value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putInt(Pref.CACHED_CONSOLIDATE_PUBLICS, value); + editor.commit(); + } + + public int getCachedConsolidateNumSecrets() { + return mSharedPreferences.getInt(Pref.CACHED_CONSOLIDATE_SECRETS, -1); + } + + public void setCachedConsolidateNumSecrets(int value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putInt(Pref.CACHED_CONSOLIDATE_SECRETS, value); + editor.commit(); + } + + public boolean isFirstTime() { + return mSharedPreferences.getBoolean(Constants.Pref.FIRST_TIME, true); + } + + public boolean useDefaultYubikeyPin() { + return mSharedPreferences.getBoolean(Pref.USE_DEFAULT_YUBIKEY_PIN, true); + } + + public void setUseDefaultYubikeyPin(boolean useDefaultYubikeyPin) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.USE_DEFAULT_YUBIKEY_PIN, useDefaultYubikeyPin); + editor.commit(); + } + + public void setFirstTime(boolean value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Constants.Pref.FIRST_TIME, 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 (String c : chunks) { + String tmp = c.trim(); + if (tmp.length() > 0) { + servers.add(tmp); + } + } + return servers.toArray(chunks); + } + public String getPreferredKeyserver() { + return getKeyServers()[0]; + } + + public void setKeyServers(String[] value) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + String rawData = ""; + for (String v : value) { + String tmp = v.trim(); + if (tmp.length() == 0) { + continue; + } + if (!"".equals(rawData)) { + rawData += ","; + } + rawData += tmp; + } + editor.putString(Constants.Pref.KEY_SERVERS, rawData); + editor.commit(); + } + + public void setWriteVersionHeader(boolean conceal) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Constants.Pref.WRITE_VERSION_HEADER, conceal); + editor.commit(); + } + + public boolean getWriteVersionHeader() { + return mSharedPreferences.getBoolean(Constants.Pref.WRITE_VERSION_HEADER, false); + } + + public void setSearchKeyserver(boolean searchKeyserver) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.SEARCH_KEYSERVER, searchKeyserver); + editor.commit(); + } + public void setSearchKeybase(boolean searchKeybase) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.SEARCH_KEYBASE, searchKeybase); + editor.commit(); + } + + public CloudSearchPrefs getCloudSearchPrefs() { + return new CloudSearchPrefs(mSharedPreferences.getBoolean(Pref.SEARCH_KEYSERVER, true), + mSharedPreferences.getBoolean(Pref.SEARCH_KEYBASE, true), + getPreferredKeyserver()); + } + + public static class CloudSearchPrefs { + public final boolean searchKeyserver; + public final boolean searchKeybase; + public final String keyserver; + + public CloudSearchPrefs(boolean searchKeyserver, boolean searchKeybase, String keyserver) { + this.searchKeyserver = searchKeyserver; + this.searchKeybase = searchKeybase; + this.keyserver = keyserver; + } + } + + public void updatePreferences() { + if (mSharedPreferences.getInt(Constants.Pref.PREF_DEFAULT_VERSION, 0) != + Constants.Defaults.PREF_VERSION) { + switch (mSharedPreferences.getInt(Constants.Pref.PREF_DEFAULT_VERSION, 0)) { + case 1: + // fall through + case 2: + // fall through + case 3: { + // migrate keyserver to hkps + String[] serversArray = getKeyServers(); + ArrayList<String> servers = new ArrayList<String>(Arrays.asList(serversArray)); + ListIterator<String> it = servers.listIterator(); + while (it.hasNext()) { + String server = it.next(); + if (server == null) { + continue; + } + if (server.equals("pool.sks-keyservers.net")) { + // use HKPS! + it.set("hkps://hkps.pool.sks-keyservers.net"); + } else if (server.equals("pgp.mit.edu")) { + // use HKPS! + it.set("hkps://pgp.mit.edu"); + } else if (server.equals("subkeys.pgp.net")) { + // remove, because often down and no HKPS! + it.remove(); + } + + } + setKeyServers(servers.toArray(new String[servers.size()])); + + // migrate old uncompressed constant to new one + if (mSharedPreferences.getInt(Constants.Pref.DEFAULT_FILE_COMPRESSION, 0) + == 0x21070001) { + setDefaultFileCompression(CompressionAlgorithmTags.UNCOMPRESSED); + } + + // migrate away from MD5 + if (mSharedPreferences.getInt(Constants.Pref.DEFAULT_HASH_ALGORITHM, 0) + == HashAlgorithmTags.MD5) { + setDefaultHashAlgorithm(HashAlgorithmTags.SHA256); + } + } + // fall through + case 4: { + // for compatibility: change from SHA512 to SHA256 + if (mSharedPreferences.getInt(Constants.Pref.DEFAULT_HASH_ALGORITHM, 0) + == HashAlgorithmTags.SHA512) { + setDefaultHashAlgorithm(HashAlgorithmTags.SHA256); + } + } + } + + // write new preference version + mSharedPreferences.edit() + .putInt(Constants.Pref.PREF_DEFAULT_VERSION, Constants.Defaults.PREF_VERSION) + .commit(); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/QrCodeUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/QrCodeUtils.java deleted file mode 100644 index 28e567d76..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/QrCodeUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de> - * Copyright (C) 2011 Andreas Schildbach - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.util; - -import android.graphics.Bitmap; -import android.graphics.Color; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.EncodeHintType; -import com.google.zxing.WriterException; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.qrcode.QRCodeWriter; -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; - -import org.sufficientlysecure.keychain.Constants; - -import java.util.Hashtable; - -/** - * Copied from Bitcoin Wallet - */ -public class QrCodeUtils { - public static final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter(); - - /** - * Generate Bitmap with QR Code based on input. - * - * @param input - * @param size - * @return QR Code as Bitmap - */ - public static Bitmap getQRCodeBitmap(final String input, final int size) { - try { - final Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>(); - hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); - final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, - size, hints); - - final int width = result.getWidth(); - final int height = result.getHeight(); - final int[] pixels = new int[width * height]; - - for (int y = 0; y < height; y++) { - final int offset = y * width; - for (int x = 0; x < width; x++) { - pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT; - } - } - - final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - bitmap.setPixels(pixels, 0, width, 0, 0, width, height); - return bitmap; - } catch (final WriterException e) { - Log.e(Constants.TAG, "QrCodeUtils", e); - return null; - } - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java new file mode 100644 index 000000000..27f026f80 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java @@ -0,0 +1,101 @@ +package org.sufficientlysecure.keychain.util; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.LabeledIntent; +import android.content.pm.ResolveInfo; +import android.os.Build; +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class ShareHelper { + Context mContext; + + public ShareHelper(Context context) { + mContext = context; + } + + /** + * Create Intent Chooser but exclude OK's EncryptActivity. + * <p/> + * Put together from some stackoverflow posts... + */ + public Intent createChooserExcluding(Intent prototype, String title, String[] activityBlacklist) { + // Produced an empty list on Huawei U8860 with Android Version 4.0.3 and weird results on 2.3 + // TODO: test on 4.1, 4.2, 4.3, only tested on 4.4 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return Intent.createChooser(prototype, title); + } + + List<LabeledIntent> targetedShareIntents = new ArrayList<LabeledIntent>(); + + List<ResolveInfo> resInfoList = mContext.getPackageManager().queryIntentActivities(prototype, 0); + List<ResolveInfo> resInfoListFiltered = new ArrayList<ResolveInfo>(); + if (!resInfoList.isEmpty()) { + for (ResolveInfo resolveInfo : resInfoList) { + // do not add blacklisted ones + if (resolveInfo.activityInfo == null || Arrays.asList(activityBlacklist).contains(resolveInfo.activityInfo.name)) + continue; + + resInfoListFiltered.add(resolveInfo); + } + + if (!resInfoListFiltered.isEmpty()) { + // sorting for nice readability + Collections.sort(resInfoListFiltered, new Comparator<ResolveInfo>() { + @Override + public int compare(ResolveInfo first, ResolveInfo second) { + String firstName = first.loadLabel(mContext.getPackageManager()).toString(); + String secondName = second.loadLabel(mContext.getPackageManager()).toString(); + return firstName.compareToIgnoreCase(secondName); + } + }); + + // create the custom intent list + for (ResolveInfo resolveInfo : resInfoListFiltered) { + Intent targetedShareIntent = (Intent) prototype.clone(); + targetedShareIntent.setPackage(resolveInfo.activityInfo.packageName); + targetedShareIntent.setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); + + LabeledIntent lIntent = new LabeledIntent(targetedShareIntent, + resolveInfo.activityInfo.packageName, + resolveInfo.loadLabel(mContext.getPackageManager()), + resolveInfo.activityInfo.icon); + targetedShareIntents.add(lIntent); + } + + // Create chooser with only one Intent in it + Intent chooserIntent = Intent.createChooser(targetedShareIntents.remove(targetedShareIntents.size() - 1), title); + // append all other Intents + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedShareIntents.toArray(new Parcelable[]{})); + return chooserIntent; + } + + } + + // fallback to Android's default chooser + return Intent.createChooser(prototype, title); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java new file mode 100644 index 000000000..9946d81aa --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.util; + +import android.content.res.AssetManager; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +public class TlsHelper { + + public static class TlsHelperException extends Exception { + public TlsHelperException(Exception e) { + super(e); + } + } + + private static Map<String, byte[]> sStaticCA = new HashMap<String, byte[]>(); + + public static void addStaticCA(String domain, byte[] certificate) { + sStaticCA.put(domain, certificate); + } + + public static void addStaticCA(String domain, AssetManager assetManager, String name) { + try { + InputStream is = assetManager.open(name); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int reads = is.read(); + + while(reads != -1){ + baos.write(reads); + reads = is.read(); + } + + is.close(); + + addStaticCA(domain, baos.toByteArray()); + } catch (IOException e) { + Log.w(Constants.TAG, e); + } + } + + public static URLConnection openConnection(URL url) throws IOException, TlsHelperException { + if (url.getProtocol().equals("https")) { + for (String domain : sStaticCA.keySet()) { + if (url.getHost().endsWith(domain)) { + return openCAConnection(sStaticCA.get(domain), url); + } + } + } + return url.openConnection(); + } + + /** + * Opens a Connection that will only accept certificates signed with a specific CA and skips common name check. + * This is required for some distributed Keyserver networks like sks-keyservers.net + * + * @param certificate The X.509 certificate used to sign the servers certificate + * @param url Connection target + */ + public static HttpsURLConnection openCAConnection(byte[] certificate, URL url) + throws TlsHelperException, IOException { + try { + // Load CA + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate ca = cf.generateCertificate(new ByteArrayInputStream(certificate)); + + // Create a KeyStore containing our trusted CAs + String keyStoreType = KeyStore.getDefaultType(); + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(null, null); + keyStore.setCertificateEntry("ca", ca); + + // Create a TrustManager that trusts the CAs in our KeyStore + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init(keyStore); + + // Create an SSLContext that uses our TrustManager + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, tmf.getTrustManagers(), null); + + // Tell the URLConnection to use a SocketFactory from our SSLContext + HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); + urlConnection.setSSLSocketFactory(context.getSocketFactory()); + + return urlConnection; + } catch (CertificateException e) { + throw new TlsHelperException(e); + } catch (NoSuchAlgorithmException e) { + throw new TlsHelperException(e); + } catch (KeyStoreException e) { + throw new TlsHelperException(e); + } catch (KeyManagementException e) { + throw new TlsHelperException(e); + } + } +} |