From 6d1137190529dc7add74926cea52c377883319be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 6 Apr 2014 12:57:42 +0200 Subject: Rename folder structure from OpenPGP Keychain to OpenKeychain --- .../org/sufficientlysecure/keychain/Constants.java | 82 ++ .../java/org/sufficientlysecure/keychain/Id.java | 191 +++++ .../keychain/KeychainApplication.java | 75 ++ .../compatibility/ClipboardReflection.java | 95 +++ .../compatibility/DialogFragmentWorkaround.java | 68 ++ .../compatibility/ListFragmentWorkaround.java | 37 + .../keychain/helper/ActionBarHelper.java | 120 +++ .../keychain/helper/ContactHelper.java | 41 + .../keychain/helper/ExportHelper.java | 177 ++++ .../keychain/helper/FileHelper.java | 131 +++ .../keychain/helper/OtherHelper.java | 70 ++ .../keychain/helper/Preferences.java | 171 ++++ .../keychain/pgp/PgpConversionHelper.java | 200 +++++ .../keychain/pgp/PgpDecryptVerify.java | 836 +++++++++++++++++++ .../keychain/pgp/PgpDecryptVerifyResult.java | 93 +++ .../sufficientlysecure/keychain/pgp/PgpHelper.java | 218 +++++ .../keychain/pgp/PgpImportExport.java | 294 +++++++ .../keychain/pgp/PgpKeyHelper.java | 644 +++++++++++++++ .../keychain/pgp/PgpKeyOperation.java | 769 +++++++++++++++++ .../keychain/pgp/PgpSignEncrypt.java | 607 ++++++++++++++ .../sufficientlysecure/keychain/pgp/PgpToX509.java | 313 +++++++ .../exception/NoAsymmetricEncryptionException.java | 26 + .../pgp/exception/PgpGeneralException.java | 29 + .../pgp/exception/PgpGeneralMsgIdException.java | 35 + .../keychain/provider/KeychainContract.java | 297 +++++++ .../keychain/provider/KeychainDatabase.java | 301 +++++++ .../keychain/provider/KeychainProvider.java | 752 +++++++++++++++++ .../provider/KeychainServiceBlobContract.java | 40 + .../provider/KeychainServiceBlobDatabase.java | 47 ++ .../provider/KeychainServiceBlobProvider.java | 162 ++++ .../keychain/provider/ProviderHelper.java | 673 +++++++++++++++ .../keychain/remote/AccountSettings.java | 85 ++ .../keychain/remote/AppSettings.java | 50 ++ .../keychain/remote/OpenPgpService.java | 484 +++++++++++ .../keychain/remote/RemoteService.java | 261 ++++++ .../remote/WrongPackageSignatureException.java | 27 + .../remote/ui/AccountSettingsActivity.java | 109 +++ .../remote/ui/AccountSettingsFragment.java | 201 +++++ .../keychain/remote/ui/AccountsListFragment.java | 198 +++++ .../keychain/remote/ui/AppSettingsActivity.java | 134 +++ .../keychain/remote/ui/AppSettingsFragment.java | 108 +++ .../keychain/remote/ui/AppsListActivity.java | 35 + .../keychain/remote/ui/AppsListFragment.java | 174 ++++ .../keychain/remote/ui/RemoteServiceActivity.java | 303 +++++++ .../keychain/service/KeychainIntentService.java | 905 +++++++++++++++++++++ .../service/KeychainIntentServiceHandler.java | 130 +++ .../keychain/service/PassphraseCacheService.java | 380 +++++++++ .../keychain/service/SaveKeyringParcel.java | 107 +++ .../keychain/ui/CertifyKeyActivity.java | 388 +++++++++ .../keychain/ui/DecryptActivity.java | 182 +++++ .../keychain/ui/DecryptFileFragment.java | 261 ++++++ .../keychain/ui/DecryptFragment.java | 180 ++++ .../keychain/ui/DecryptMessageFragment.java | 189 +++++ .../keychain/ui/DrawerActivity.java | 295 +++++++ .../keychain/ui/EditKeyActivity.java | 760 +++++++++++++++++ .../keychain/ui/EncryptActivity.java | 256 ++++++ .../keychain/ui/EncryptActivityInterface.java | 30 + .../keychain/ui/EncryptAsymmetricFragment.java | 268 ++++++ .../keychain/ui/EncryptFileFragment.java | 380 +++++++++ .../keychain/ui/EncryptMessageFragment.java | 259 ++++++ .../keychain/ui/EncryptSymmetricFragment.java | 98 +++ .../keychain/ui/HelpAboutFragment.java | 75 ++ .../keychain/ui/HelpActivity.java | 79 ++ .../keychain/ui/HelpHtmlFragment.java | 75 ++ .../keychain/ui/ImportKeysActivity.java | 489 +++++++++++ .../keychain/ui/ImportKeysClipboardFragment.java | 87 ++ .../keychain/ui/ImportKeysFileFragment.java | 98 +++ .../keychain/ui/ImportKeysListFragment.java | 305 +++++++ .../keychain/ui/ImportKeysNFCFragment.java | 68 ++ .../keychain/ui/ImportKeysQrCodeFragment.java | 205 +++++ .../keychain/ui/ImportKeysServerFragment.java | 155 ++++ .../keychain/ui/KeyListActivity.java | 95 +++ .../keychain/ui/KeyListFragment.java | 700 ++++++++++++++++ .../keychain/ui/PreferencesActivity.java | 387 +++++++++ .../keychain/ui/PreferencesKeyServerActivity.java | 130 +++ .../keychain/ui/SelectPublicKeyActivity.java | 143 ++++ .../keychain/ui/SelectPublicKeyFragment.java | 350 ++++++++ .../keychain/ui/SelectSecretKeyActivity.java | 83 ++ .../keychain/ui/SelectSecretKeyFragment.java | 176 ++++ .../keychain/ui/SelectSecretKeyLayoutFragment.java | 211 +++++ .../keychain/ui/UploadKeyActivity.java | 127 +++ .../keychain/ui/ViewCertActivity.java | 253 ++++++ .../keychain/ui/ViewKeyActivity.java | 277 +++++++ .../keychain/ui/ViewKeyActivityJB.java | 124 +++ .../keychain/ui/ViewKeyCertsFragment.java | 311 +++++++ .../keychain/ui/ViewKeyMainFragment.java | 347 ++++++++ .../ui/adapter/AsyncTaskResultWrapper.java | 46 ++ .../ui/adapter/HighlightQueryCursorAdapter.java | 65 ++ .../keychain/ui/adapter/ImportKeysAdapter.java | 186 +++++ .../keychain/ui/adapter/ImportKeysListEntry.java | 274 +++++++ .../keychain/ui/adapter/ImportKeysListLoader.java | 167 ++++ .../ui/adapter/ImportKeysListServerLoader.java | 130 +++ .../ui/adapter/KeyValueSpinnerAdapter.java | 101 +++ .../keychain/ui/adapter/PagerTabStripAdapter.java | 70 ++ .../ui/adapter/SelectKeyCursorAdapter.java | 165 ++++ .../keychain/ui/adapter/TabsAdapter.java | 101 +++ .../keychain/ui/adapter/ViewKeyKeysAdapter.java | 175 ++++ .../keychain/ui/adapter/ViewKeyUserIdsAdapter.java | 181 +++++ .../ui/dialog/BadImportKeyDialogFragment.java | 67 ++ .../ui/dialog/CreateKeyDialogFragment.java | 159 ++++ .../ui/dialog/DeleteFileDialogFragment.java | 124 +++ .../ui/dialog/DeleteKeyDialogFragment.java | 174 ++++ .../keychain/ui/dialog/FileDialogFragment.java | 220 +++++ .../ui/dialog/PassphraseDialogFragment.java | 328 ++++++++ .../keychain/ui/dialog/ProgressDialogFragment.java | 154 ++++ .../ui/dialog/SetPassphraseDialogFragment.java | 186 +++++ .../keychain/ui/dialog/ShareNfcDialogFragment.java | 99 +++ .../ui/dialog/ShareQrCodeDialogFragment.java | 212 +++++ .../keychain/ui/widget/Editor.java | 27 + .../keychain/ui/widget/FixedListView.java | 55 ++ .../keychain/ui/widget/FoldableLinearLayout.java | 203 +++++ .../keychain/ui/widget/IntegerListPreference.java | 94 +++ .../keychain/ui/widget/KeyEditor.java | 377 +++++++++ .../keychain/ui/widget/KeyServerEditor.java | 82 ++ .../keychain/ui/widget/SectionView.java | 429 ++++++++++ .../keychain/ui/widget/UnderlineTextView.java | 69 ++ .../keychain/ui/widget/UserIdEditor.java | 265 ++++++ .../keychain/util/AlgorithmNames.java | 93 +++ .../sufficientlysecure/keychain/util/Choice.java | 45 + .../keychain/util/HkpKeyServer.java | 353 ++++++++ .../keychain/util/InputData.java | 41 + .../keychain/util/IntentIntegratorSupportV4.java | 46 ++ .../keychain/util/IterableIterator.java | 39 + .../keychain/util/KeyServer.java | 52 ++ .../keychain/util/KeychainServiceListener.java | 22 + .../org/sufficientlysecure/keychain/util/Log.java | 83 ++ .../keychain/util/PRNGFixes.java | 352 ++++++++ .../keychain/util/PausableThreadPoolExecutor.java | 94 +++ .../keychain/util/PositionAwareInputStream.java | 83 ++ .../sufficientlysecure/keychain/util/Primes.java | 188 +++++ .../keychain/util/ProgressDialogUpdater.java | 25 + .../keychain/util/ProgressScaler.java | 50 ++ .../keychain/util/QrCodeUtils.java | 72 ++ 133 files changed, 26804 insertions(+) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Id.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ActionBarHelper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ContactHelper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ExportHelper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/FileHelper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/OtherHelper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/Preferences.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyResult.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpImportExport.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncrypt.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpToX509.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/NoAsymmetricEncryptionException.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralException.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralMsgIdException.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AccountSettings.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/WrongPackageSignatureException.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountsListFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFileFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptMessageFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivityInterface.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptMessageFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptSymmetricFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpHtmlFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysNFCFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyLayoutFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivityJB.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/HighlightQueryCursorAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListServerLoader.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyValueSpinnerAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/TabsAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CreateKeyDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareQrCodeDialogFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FixedListView.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FoldableLinearLayout.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SectionView.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UnderlineTextView.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/AlgorithmNames.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Choice.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/HkpKeyServer.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/InputData.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IntentIntegratorSupportV4.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IterableIterator.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyServer.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeychainServiceListener.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Log.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PRNGFixes.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PausableThreadPoolExecutor.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Primes.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressScaler.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/QrCodeUtils.java (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure') diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java new file mode 100644 index 000000000..b3bfaa229 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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; + +import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.sufficientlysecure.keychain.remote.ui.AppsListActivity; +import org.sufficientlysecure.keychain.ui.DecryptActivity; +import org.sufficientlysecure.keychain.ui.EncryptActivity; +import org.sufficientlysecure.keychain.ui.ImportKeysActivity; +import org.sufficientlysecure.keychain.ui.KeyListActivity; + +public final class Constants { + + public static final boolean DEBUG = BuildConfig.DEBUG; + + public static final String TAG = "Keychain"; + + public static final String PACKAGE_NAME = "org.sufficientlysecure.keychain"; + + // as defined in http://tools.ietf.org/html/rfc3156, section 7 + public static final String NFC_MIME = "application/pgp-keys"; + + // used by QR Codes (Guardian Project, Monkeysphere compatiblity) + public static final String FINGERPRINT_SCHEME = "openpgp4fpr"; + + // Not BC due to the use of Spongy Castle for Android + public static final String SC = BouncyCastleProvider.PROVIDER_NAME; + public static final String BOUNCY_CASTLE_PROVIDER_NAME = SC; + + public static final String INTENT_PREFIX = PACKAGE_NAME + ".action."; + + public static final class Path { + public static final String APP_DIR = Environment.getExternalStorageDirectory() + + "/OpenKeychain"; + public static final String APP_DIR_FILE = APP_DIR + "/export.asc"; + } + + 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_ARMOR = "defaultAsciiArmor"; + public static final String DEFAULT_MESSAGE_COMPRESSION = "defaultMessageCompression"; + public static final String DEFAULT_FILE_COMPRESSION = "defaultFileCompression"; + public static final String PASSPHRASE_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"; + } + + public static final class DrawerItems { + public static final Class KEY_LIST = KeyListActivity.class; + public static final Class ENCRYPT = EncryptActivity.class; + public static final Class DECRYPT = DecryptActivity.class; + public static final Class REGISTERED_APPS_LIST = AppsListActivity.class; + public static final Class[] ARRAY = new Class[]{ + KEY_LIST, + ENCRYPT, + DECRYPT, + REGISTERED_APPS_LIST + }; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Id.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Id.java new file mode 100644 index 000000000..784ec340e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Id.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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 class menu { + + public static final class option { + public static final int new_passphrase = 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; + public static final int crypto_consumers = 0x21070023; + public static final int createExpert = 0x21070024; + } + } + + // 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; + } + + // 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 dialog { + public static final int passphrase = 0x21070001; + public static final int encrypting = 0x21070002; + public static final int decrypting = 0x21070003; + public static final int new_passphrase = 0x21070004; + public static final int passphrases_do_not_match = 0x21070005; + public static final int no_passphrase = 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 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 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 int public_secret_key = 0x21070005; + } + + 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java new file mode 100644 index 000000000..9b80e76f3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain; + +import android.app.Application; +import android.os.Environment; + +import org.spongycastle.jce.provider.BouncyCastleProvider; + +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.PRNGFixes; + +import java.io.File; +import java.security.Provider; +import java.security.Security; + +public class KeychainApplication extends Application { + + /** + * Called when the application is starting, before any activity, service, or receiver objects + * (excluding content providers) have been created. + */ + @Override + public void onCreate() { + super.onCreate(); + + /* + * Sets Bouncy (Spongy) Castle as preferred security provider + * + * insertProviderAt() position starts from 1 + */ + Security.insertProviderAt(new BouncyCastleProvider(), 1); + + /* + * apply RNG fixes + * + * among other things, executes Security.insertProviderAt(new + * LinuxPRNGSecureRandomProvider(), 1) for Android <= SDK 17 + */ + PRNGFixes.apply(); + Log.d(Constants.TAG, "Bouncy Castle set and PRNG Fixes applied!"); + + if (Constants.DEBUG) { + Provider[] providers = Security.getProviders(); + Log.d(Constants.TAG, "Installed Security Providers:"); + for (Provider p : providers) { + Log.d(Constants.TAG, "provider class: " + p.getClass().getName()); + } + } + + // 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java new file mode 100644 index 000000000..1cac5762d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.compatibility; + +import android.content.Context; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +import java.lang.reflect.Method; + +public class ClipboardReflection { + + private static final String clipboardLabel = "Keychain"; + + /** + * Wrapper around ClipboardManager based on Android version using Reflection API + * + * @param context + * @param text + */ + public static void copyToClipboard(Context context, String text) { + Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE); + try { + if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) { + Method methodSetText = clipboard.getClass() + .getMethod("setText", CharSequence.class); + methodSetText.invoke(clipboard, text); + } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) { + Class classClipData = Class.forName("android.content.ClipData"); + Method methodNewPlainText = classClipData.getMethod("newPlainText", + CharSequence.class, CharSequence.class); + Object clip = methodNewPlainText.invoke(null, clipboardLabel, text); + methodNewPlainText = clipboard.getClass() + .getMethod("setPrimaryClip", classClipData); + methodNewPlainText.invoke(clipboard, clip); + } + } catch (Exception e) { + Log.e(Constants.TAG, "There was an error copying the text to the clipboard", e); + } + } + + /** + * Wrapper around ClipboardManager based on Android version using Reflection API + * + * @param context + */ + public static CharSequence getClipboardText(Context context) { + Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE); + try { + if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) { + // CharSequence text = clipboard.getText(); + Method methodGetText = clipboard.getClass().getMethod("getText"); + Object text = methodGetText.invoke(clipboard); + + return (CharSequence) text; + } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) { + // ClipData clipData = clipboard.getPrimaryClip(); + Method methodGetPrimaryClip = clipboard.getClass().getMethod("getPrimaryClip"); + Object clipData = methodGetPrimaryClip.invoke(clipboard); + + // ClipData.Item clipDataItem = clipData.getItemAt(0); + Method methodGetItemAt = clipData.getClass().getMethod("getItemAt", int.class); + Object clipDataItem = methodGetItemAt.invoke(clipData, 0); + + // CharSequence text = clipDataItem.coerceToText(context); + Method methodGetString = clipDataItem.getClass().getMethod("coerceToText", + Context.class); + Object text = methodGetString.invoke(clipDataItem, context); + + return (CharSequence) text; + } else { + return null; + } + } catch (Exception e) { + Log.e(Constants.TAG, "There was an error getting the text from the clipboard", e); + return null; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java new file mode 100644 index 000000000..36f7fd23b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.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: + * DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + * public void run() { + * // show dialog... + * } + * }); + * + */ +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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java new file mode 100644 index 000000000..1cb91c688 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.compatibility; + +import android.support.v4.app.ListFragment; +import android.view.View; +import android.widget.ListView; + +/** + * 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 ListFragment { + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + l.setItemChecked(position, l.isItemChecked(position)); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ActionBarHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ActionBarHelper.java new file mode 100644 index 000000000..a26df556d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ActionBarHelper.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.helper; + +import android.app.Activity; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + +public class ActionBarHelper { + + /** + * Set actionbar without home button if called from another app + * + * @param activity + */ + public static void setBackButton(ActionBarActivity activity) { + 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); + } + } + + /** + * Sets custom view on ActionBar for Done/Cancel activities + * + * @param actionBar + * @param firstText + * @param firstDrawableId + * @param firstOnClickListener + * @param secondText + * @param secondDrawableId + * @param secondOnClickListener + */ + public static void setTwoButtonView(ActionBar actionBar, + int firstText, int firstDrawableId, OnClickListener firstOnClickListener, + int secondText, int secondDrawableId, OnClickListener secondOnClickListener) { + + // Inflate the custom action bar view + final LayoutInflater inflater = (LayoutInflater) actionBar.getThemedContext() + .getSystemService(Activity.LAYOUT_INFLATER_SERVICE); + final View customActionBarView = inflater.inflate( + R.layout.actionbar_custom_view_done_cancel, null); + + TextView firstTextView = ((TextView) customActionBarView.findViewById(R.id.actionbar_done_text)); + firstTextView.setText(firstText); + firstTextView.setCompoundDrawablesWithIntrinsicBounds(firstDrawableId, 0, 0, 0); + customActionBarView.findViewById(R.id.actionbar_done).setOnClickListener( + firstOnClickListener); + TextView secondTextView = ((TextView) customActionBarView.findViewById(R.id.actionbar_cancel_text)); + secondTextView.setText(secondText); + secondTextView.setCompoundDrawablesWithIntrinsicBounds(secondDrawableId, 0, 0, 0); + customActionBarView.findViewById(R.id.actionbar_cancel).setOnClickListener( + secondOnClickListener); + + // Show the custom action bar view and hide the normal Home icon and title. + actionBar.setDisplayShowTitleEnabled(false); + actionBar.setDisplayShowHomeEnabled(false); + actionBar.setDisplayShowCustomEnabled(true); + actionBar.setCustomView(customActionBarView, new ActionBar.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + + /** + * Sets custom view on ActionBar for Done activities + * + * @param actionBar + * @param firstText + * @param firstOnClickListener + */ + public static void setOneButtonView(ActionBar actionBar, int firstText, int firstDrawableId, + OnClickListener firstOnClickListener) { + // Inflate a "Done" custom action bar view to serve as the "Up" affordance. + final LayoutInflater inflater = (LayoutInflater) actionBar.getThemedContext() + .getSystemService(Activity.LAYOUT_INFLATER_SERVICE); + final View customActionBarView = inflater + .inflate(R.layout.actionbar_custom_view_done, null); + + TextView firstTextView = ((TextView) customActionBarView.findViewById(R.id.actionbar_done_text)); + firstTextView.setText(firstText); + firstTextView.setCompoundDrawablesWithIntrinsicBounds(firstDrawableId, 0, 0, 0); + customActionBarView.findViewById(R.id.actionbar_done).setOnClickListener( + firstOnClickListener); + + // Show the custom action bar view and hide the normal Home icon and title. + actionBar.setDisplayShowTitleEnabled(false); + actionBar.setDisplayShowHomeEnabled(false); + actionBar.setDisplayShowCustomEnabled(true); + actionBar.setCustomView(customActionBarView); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ContactHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ContactHelper.java new file mode 100644 index 000000000..f8ed21816 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ContactHelper.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * 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 android.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; +import android.util.Patterns; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ContactHelper { + + public static final List getMailAccounts(Context context) { + final Account[] accounts = AccountManager.get(context).getAccounts(); + final Set emailSet = new HashSet(); + for (Account account : accounts) { + if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { + emailSet.add(account.name); + } + } + return new ArrayList(emailSet); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ExportHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ExportHelper.java new file mode 100644 index 000000000..810f22d8e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/ExportHelper.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * 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 android.app.ProgressDialog; +import android.content.DialogInterface; +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.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +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 org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment; +import org.sufficientlysecure.keychain.util.Log; + +public class ExportHelper { + protected FileDialogFragment mFileDialog; + protected String mExportFilename; + + ActionBarActivity mActivity; + + public ExportHelper(ActionBarActivity activity) { + super(); + this.mActivity = activity; + } + + public void deleteKey(Uri dataUri, Handler deleteHandler) { + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(deleteHandler); + long masterKeyId = ProviderHelper.getMasterKeyId(mActivity, dataUri); + + DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger, + new long[]{ masterKeyId }); + deleteKeyDialog.show(mActivity.getSupportFragmentManager(), "deleteKeyDialog"); + } + + /** + * Show dialog where to export keys + */ + public void showExportKeysDialog(final long[] masterKeyIds, final String exportFilename, + final boolean showSecretCheckbox) { + mExportFilename = exportFilename; + + // 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(masterKeyIds, data.getBoolean(FileDialogFragment.MESSAGE_DATA_CHECKED)); + } + } + }; + + // 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 (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; + + mFileDialog = FileDialogFragment.newInstance(messenger, title, message, + exportFilename, checkMsg); + + mFileDialog.show(mActivity.getSupportFragmentManager(), "fileDialog"); + } + }); + } + + /** + * 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, mExportFilename); + 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, + true, + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + mActivity.stopService(intent); + } + }) { + 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/helper/FileHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/FileHelper.java new file mode 100644 index 000000000..d24aeca52 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/FileHelper.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.helper; + +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.support.v4.app.Fragment; +import android.widget.Toast; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + +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 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(Activity activity, String filename, String mimeType, int requestCode) { + Intent intent = buildFileIntent(filename, mimeType); + + try { + activity.startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + // No compatible file manager was found. + Toast.makeText(activity, R.string.no_filemanager_installed, Toast.LENGTH_SHORT).show(); + } + } + + public static void openFile(Fragment fragment, String filename, String mimeType, int requestCode) { + Intent intent = buildFileIntent(filename, 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(); + } + } + + private static Intent buildFileIntent(String filename, String mimeType) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + + intent.setData(Uri.parse("file://" + filename)); + intent.setType(mimeType); + + return intent; + } + + /** + * 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 columnIndex = cursor.getColumnIndexOrThrow("_data"); + if (cursor.moveToFirst()) { + return cursor.getString(columnIndex); + } + } catch (Exception e) { + // Eat it + } + } else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/OtherHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/OtherHelper.java new file mode 100644 index 000000000..b31a889f0 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/OtherHelper.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.helper; + +import android.os.Bundle; +import android.text.SpannableStringBuilder; +import android.text.Spanned; + +import android.text.style.StrikethroughSpan; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.Iterator; +import java.util.Set; + +public class OtherHelper { + + /** + * 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 ks = bundle.keySet(); + Iterator 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"); + } + } + } + + public static SpannableStringBuilder strikeOutText(CharSequence text) { + SpannableStringBuilder sb = new SpannableStringBuilder(text); + sb.setSpan(new StrikethroughSpan(), 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + return sb; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/Preferences.java new file mode 100644 index 000000000..ca5555fea --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/Preferences.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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 android.content.Context; +import android.content.SharedPreferences; +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; + +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); + } + return sPreferences; + } + + 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.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.SHA512); + } + + 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 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 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 servers = new Vector(); + String chunks[] = rawData.split(","); + for (String c : chunks) { + String tmp = c.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 (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(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java new file mode 100644 index 000000000..c6c62d649 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpConversionHelper.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.pgp; + +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPObjectFactory; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureList; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; + + +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 + * + * @param keysBytes + * @return + */ + public static ArrayList BytesToPGPSecretKeyList(byte[] keysBytes) { + PGPObjectFactory factory = new PGPObjectFactory(keysBytes); + Object obj = null; + ArrayList keys = new ArrayList(); + try { + while ((obj = factory.nextObject()) != null) { + PGPSecretKey secKey = null; + if (obj instanceof PGPSecretKey) { + secKey = (PGPSecretKey) obj; + if (secKey == null) { + Log.e(Constants.TAG, "No keys given!"); + } + keys.add(secKey); + } else if (obj instanceof PGPSecretKeyRing) { //master keys are sent as keyrings + PGPSecretKeyRing keyRing = null; + keyRing = (PGPSecretKeyRing) obj; + if (keyRing == null) { + Log.e(Constants.TAG, "No keys given!"); + } + @SuppressWarnings("unchecked") + Iterator itr = keyRing.getSecretKeys(); + while (itr.hasNext()) { + keys.add(itr.next()); + } + } + } + } catch (IOException e) { + } + + return keys; + } + + /** + * Convert from byte[] to PGPSecretKey + *

+ * Singles keys are encoded as keyRings with one single key in it by Bouncy Castle + * + * @param keyBytes + * @return + */ + public static PGPSecretKey BytesToPGPSecretKey(byte[] keyBytes) { + PGPObjectFactory factory = new PGPObjectFactory(keyBytes); + Object obj = null; + try { + obj = factory.nextObject(); + } catch (IOException e) { + Log.e(Constants.TAG, "Error while converting to PGPSecretKey!", e); + } + PGPSecretKey secKey = null; + if (obj instanceof PGPSecretKey) { + if ((secKey = (PGPSecretKey) obj) == null) { + Log.e(Constants.TAG, "No keys given!"); + } + } else if (obj instanceof PGPSecretKeyRing) { //master keys are sent as keyrings + PGPSecretKeyRing keyRing = null; + if ((keyRing = (PGPSecretKeyRing) obj) == null) { + Log.e(Constants.TAG, "No keys given!"); + } + secKey = keyRing.getSecretKey(); + } + + return secKey; + } + + /** + * Convert from byte[] to PGPSignature + * + * @param sigBytes + * @return + */ + public static PGPSignature BytesToPGPSignature(byte[] sigBytes) { + PGPObjectFactory factory = new PGPObjectFactory(sigBytes); + PGPSignatureList signatures = null; + try { + if ((signatures = (PGPSignatureList) factory.nextObject()) == null || signatures.isEmpty()) { + Log.e(Constants.TAG, "No signatures given!"); + return null; + } + } catch (IOException e) { + Log.e(Constants.TAG, "Error while converting to PGPSignature!", e); + return null; + } + + return signatures.get(0); + } + + /** + * Convert from ArrayList to byte[] + * + * @param keys + * @return + */ + public static byte[] PGPSecretKeyArrayListToBytes(ArrayList keys) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + for (PGPSecretKey key : keys) { + try { + key.encode(os); + } catch (IOException e) { + Log.e(Constants.TAG, "Error while converting ArrayList to byte[]!", e); + } + } + + return os.toByteArray(); + } + + /** + * Convert from PGPSecretKey to byte[] + * + * @param key + * @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 keyRing + * @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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java new file mode 100644 index 000000000..8a0bf99d7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java @@ -0,0 +1,836 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp; + +import android.content.Context; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.spongycastle.bcpg.ArmoredInputStream; +import org.spongycastle.bcpg.SignatureSubpacketTags; +import org.spongycastle.openpgp.PGPCompressedData; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.spongycastle.openpgp.PGPEncryptedDataList; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPLiteralData; +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.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureList; +import org.spongycastle.openpgp.PGPSignatureSubpacketVector; +import org.spongycastle.openpgp.PGPUtil; +import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ProgressDialogUpdater; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SignatureException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; + +/** + * This class uses a Builder pattern! + */ +public class PgpDecryptVerify { + private Context mContext; + private InputData mData; + private OutputStream mOutStream; + + private ProgressDialogUpdater mProgressDialogUpdater; + private boolean mAllowSymmetricDecryption; + private String mPassphrase; + private Set mAllowedKeyIds; + + private PgpDecryptVerify(Builder builder) { + // private Constructor can only be called from Builder + this.mContext = builder.mContext; + this.mData = builder.mData; + this.mOutStream = builder.mOutStream; + + this.mProgressDialogUpdater = builder.mProgressDialogUpdater; + this.mAllowSymmetricDecryption = builder.mAllowSymmetricDecryption; + this.mPassphrase = builder.mPassphrase; + this.mAllowedKeyIds = builder.mAllowedKeyIds; + } + + public static class Builder { + // mandatory parameter + private Context mContext; + private InputData mData; + private OutputStream mOutStream; + + // optional + private ProgressDialogUpdater mProgressDialogUpdater = null; + private boolean mAllowSymmetricDecryption = true; + private String mPassphrase = null; + private Set mAllowedKeyIds = null; + + public Builder(Context context, InputData data, OutputStream outStream) { + this.mContext = context; + this.mData = data; + this.mOutStream = outStream; + } + + public Builder progressDialogUpdater(ProgressDialogUpdater progressDialogUpdater) { + this.mProgressDialogUpdater = progressDialogUpdater; + return this; + } + + public Builder allowSymmetricDecryption(boolean allowSymmetricDecryption) { + this.mAllowSymmetricDecryption = allowSymmetricDecryption; + return this; + } + + public Builder passphrase(String passphrase) { + this.mPassphrase = passphrase; + return this; + } + + /** + * Allow these key ids alone for decryption. + * This means only ciphertexts encrypted for one of these private key can be decrypted. + * + * @param allowedKeyIds + * @return + */ + public Builder allowedKeyIds(Set allowedKeyIds) { + this.mAllowedKeyIds = allowedKeyIds; + return this; + } + + public PgpDecryptVerify build() { + return new PgpDecryptVerify(this); + } + } + + public void updateProgress(int message, int current, int total) { + if (mProgressDialogUpdater != null) { + mProgressDialogUpdater.setProgress(message, current, total); + } + } + + public void updateProgress(int current, int total) { + if (mProgressDialogUpdater != null) { + mProgressDialogUpdater.setProgress(current, total); + } + } + + /** + * Decrypts and/or verifies data based on parameters of class + * + * @return + * @throws IOException + * @throws PgpGeneralException + * @throws PGPException + * @throws SignatureException + */ + public PgpDecryptVerifyResult execute() + throws IOException, PgpGeneralException, PGPException, SignatureException { + // automatically works with ascii armor input and binary + InputStream in = PGPUtil.getDecoderStream(mData.getInputStream()); + if (in instanceof ArmoredInputStream) { + ArmoredInputStream aIn = (ArmoredInputStream) in; + // it is ascii armored + Log.d(Constants.TAG, "ASCII Armor Header Line: " + aIn.getArmorHeaderLine()); + + if (aIn.isClearText()) { + // a cleartext signature, verify it with the other method + return verifyCleartextSignature(aIn); + } + // else: ascii armored encryption! go on... + } + + return decryptVerify(in); + } + + /** + * Decrypt and/or verifies binary or ascii armored pgp + * + * @param in + * @return + * @throws IOException + * @throws PgpGeneralException + * @throws PGPException + * @throws SignatureException + */ + private PgpDecryptVerifyResult decryptVerify(InputStream in) + throws IOException, PgpGeneralException, PGPException, SignatureException { + PgpDecryptVerifyResult returnData = new PgpDecryptVerifyResult(); + + PGPObjectFactory pgpF = new PGPObjectFactory(in); + PGPEncryptedDataList enc; + Object o = pgpF.nextObject(); + + int currentProgress = 0; + updateProgress(R.string.progress_reading_data, currentProgress, 100); + + if (o instanceof PGPEncryptedDataList) { + enc = (PGPEncryptedDataList) o; + } else { + enc = (PGPEncryptedDataList) pgpF.nextObject(); + } + + if (enc == null) { + throw new PgpGeneralException(mContext.getString(R.string.error_invalid_data)); + } + + InputStream clear; + PGPEncryptedData encryptedData; + + currentProgress += 5; + + PGPPublicKeyEncryptedData encryptedDataAsymmetric = null; + PGPPBEEncryptedData encryptedDataSymmetric = null; + PGPSecretKey secretKey = null; + Iterator it = enc.getEncryptedDataObjects(); + boolean symmetricPacketFound = false; + // find secret key + while (it.hasNext()) { + Object obj = it.next(); + if (obj instanceof PGPPublicKeyEncryptedData) { + updateProgress(R.string.progress_finding_key, currentProgress, 100); + + PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj; + long masterKeyId = ProviderHelper.getMasterKeyId(mContext, + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(encData.getKeyID())) + ); + PGPSecretKeyRing secretKeyRing = ProviderHelper.getPGPSecretKeyRing(mContext, masterKeyId); + if (secretKeyRing == null) { + throw new PgpGeneralException(mContext.getString(R.string.error_no_secret_key_found)); + } + secretKey = secretKeyRing.getSecretKey(encData.getKeyID()); + if (secretKey == null) { + throw new PgpGeneralException(mContext.getString(R.string.error_no_secret_key_found)); + } + // secret key exists in database + + // allow only a specific key for decryption? + if (mAllowedKeyIds != null) { + Log.d(Constants.TAG, "encData.getKeyID():" + encData.getKeyID()); + Log.d(Constants.TAG, "allowedKeyIds: " + mAllowedKeyIds); + Log.d(Constants.TAG, "masterKeyId: " + masterKeyId); + + if (!mAllowedKeyIds.contains(masterKeyId)) { + throw new PgpGeneralException( + mContext.getString(R.string.error_no_secret_key_found)); + } + } + + encryptedDataAsymmetric = encData; + + // if no passphrase was explicitly set try to get it from the cache service + if (mPassphrase == null) { + // returns "" if key has no passphrase + mPassphrase = + PassphraseCacheService.getCachedPassphrase(mContext, masterKeyId); + + // if passphrase was not cached, return here + // indicating that a passphrase is missing! + if (mPassphrase == null) { + returnData.setKeyIdPassphraseNeeded(masterKeyId); + returnData.setStatus(PgpDecryptVerifyResult.KEY_PASSHRASE_NEEDED); + return returnData; + } + } + + // break out of while, only get first object here + // TODO???: There could be more pgp objects, which are not decrypted! + break; + } else if (mAllowSymmetricDecryption && obj instanceof PGPPBEEncryptedData) { + symmetricPacketFound = true; + + encryptedDataSymmetric = (PGPPBEEncryptedData) obj; + + // if no passphrase is given, return here + // indicating that a passphrase is missing! + if (mPassphrase == null) { + returnData.setStatus(PgpDecryptVerifyResult.SYMMETRIC_PASSHRASE_NEEDED); + return returnData; + } + + // break out of while, only get first object here + // TODO???: There could be more pgp objects, which are not decrypted! + break; + } + } + + if (symmetricPacketFound) { + updateProgress(R.string.progress_preparing_streams, currentProgress, 100); + + PGPDigestCalculatorProvider digestCalcProvider = new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(); + PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder( + digestCalcProvider).setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + mPassphrase.toCharArray()); + + clear = encryptedDataSymmetric.getDataStream(decryptorFactory); + + encryptedData = encryptedDataSymmetric; + currentProgress += 5; + } else { + if (secretKey == null) { + throw new PgpGeneralException(mContext.getString(R.string.error_no_secret_key_found)); + } + + currentProgress += 5; + updateProgress(R.string.progress_extracting_key, currentProgress, 100); + PGPPrivateKey privateKey; + try { + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + mPassphrase.toCharArray()); + privateKey = secretKey.extractPrivateKey(keyDecryptor); + } catch (PGPException e) { + throw new PGPException(mContext.getString(R.string.error_wrong_passphrase)); + } + if (privateKey == null) { + throw new PgpGeneralException( + mContext.getString(R.string.error_could_not_extract_private_key)); + } + currentProgress += 5; + updateProgress(R.string.progress_preparing_streams, currentProgress, 100); + + PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(privateKey); + + clear = encryptedDataAsymmetric.getDataStream(decryptorFactory); + + encryptedData = encryptedDataAsymmetric; + currentProgress += 5; + } + + PGPObjectFactory plainFact = new PGPObjectFactory(clear); + Object dataChunk = plainFact.nextObject(); + PGPOnePassSignature signature = null; + OpenPgpSignatureResult signatureResult = null; + PGPPublicKey signatureKey = null; + int signatureIndex = -1; + + if (dataChunk instanceof PGPCompressedData) { + updateProgress(R.string.progress_decompressing_data, currentProgress, 100); + + PGPObjectFactory fact = new PGPObjectFactory( + ((PGPCompressedData) dataChunk).getDataStream()); + dataChunk = fact.nextObject(); + plainFact = fact; + currentProgress += 10; + } + + long signatureKeyId = 0; + if (dataChunk instanceof PGPOnePassSignatureList) { + updateProgress(R.string.progress_processing_signature, currentProgress, 100); + + signatureResult = new OpenPgpSignatureResult(); + PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk; + for (int i = 0; i < sigList.size(); ++i) { + signature = sigList.get(i); + signatureKey = ProviderHelper + .getPGPPublicKeyRing(mContext, signature.getKeyID()).getPublicKey(); + if (signatureKeyId == 0) { + signatureKeyId = signature.getKeyID(); + } + if (signatureKey == null) { + signature = null; + } else { + signatureIndex = i; + signatureKeyId = signature.getKeyID(); + String userId = null; + PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingWithKeyId( + mContext, signatureKeyId); + if (signKeyRing != null) { + userId = PgpKeyHelper.getMainUserId(signKeyRing.getPublicKey()); + } + signatureResult.setUserId(userId); + break; + } + } + + signatureResult.setKeyId(signatureKeyId); + + if (signature != null) { + JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = + new JcaPGPContentVerifierBuilderProvider() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + + signature.init(contentVerifierBuilderProvider, signatureKey); + } else { + signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY); + } + + dataChunk = plainFact.nextObject(); + currentProgress += 10; + } + + if (dataChunk instanceof PGPSignatureList) { + dataChunk = plainFact.nextObject(); + } + + if (dataChunk instanceof PGPLiteralData) { + updateProgress(R.string.progress_decrypting, currentProgress, 100); + + PGPLiteralData literalData = (PGPLiteralData) dataChunk; + + 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; + // TODO: progress calculation is broken here! Try to rework it based on commented code! +// int progress = 0; + long startPos = mData.getStreamPosition(); + while ((n = dataIn.read(buffer)) > 0) { + mOutStream.write(buffer, 0, n); +// progress += n; + if (signature != null) { + try { + signature.update(buffer, 0, n); + } catch (SignatureException e) { + signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_ERROR); + signature = null; + } + } + // TODO: dead code?! + // unknown size, but try to at least have a moving, slowing down progress bar +// currentProgress = startProgress + (endProgress - startProgress) * progress +// / (progress + 100000); + if (mData.getSize() - startPos == 0) { + currentProgress = endProgress; + } else { + currentProgress = (int) (startProgress + (endProgress - startProgress) + * (mData.getStreamPosition() - startPos) / (mData.getSize() - startPos)); + } + updateProgress(currentProgress, 100); + } + + if (signature != null) { + updateProgress(R.string.progress_verifying_signature, 90, 100); + + PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject(); + PGPSignature messageSignature = signatureList.get(signatureIndex); + + // these are not cleartext signatures! + // TODO: what about binary signatures? + signatureResult.setSignatureOnly(false); + + //Now check binding signatures + boolean validKeyBinding = verifyKeyBinding(mContext, messageSignature, signatureKey); + boolean validSignature = signature.verify(messageSignature); + + // TODO: implement CERTIFIED! + if (validKeyBinding & validSignature) { + signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED); + } + } + } + + if (encryptedData.isIntegrityProtected()) { + updateProgress(R.string.progress_verifying_integrity, 95, 100); + + if (encryptedData.verify()) { + // passed + Log.d(Constants.TAG, "Integrity verification: success!"); + } else { + // failed + Log.d(Constants.TAG, "Integrity verification: failed!"); + throw new PgpGeneralException(mContext.getString(R.string.error_integrity_check_failed)); + } + } else { + // no integrity check + Log.e(Constants.TAG, "Encrypted data was not integrity protected!"); + // TODO: inform user? + } + + updateProgress(R.string.progress_done, 100, 100); + + returnData.setSignatureResult(signatureResult); + return returnData; + } + + /** + * This method verifies cleartext signatures + * as defined in http://tools.ietf.org/html/rfc4880#section-7 + *

+ * The method is heavily based on + * pg/src/main/java/org/spongycastle/openpgp/examples/ClearSignedFileProcessor.java + * + * @return + * @throws IOException + * @throws PgpGeneralException + * @throws PGPException + * @throws SignatureException + */ + private PgpDecryptVerifyResult verifyCleartextSignature(ArmoredInputStream aIn) + throws IOException, PgpGeneralException, PGPException, SignatureException { + PgpDecryptVerifyResult returnData = new PgpDecryptVerifyResult(); + OpenPgpSignatureResult signatureResult = new OpenPgpSignatureResult(); + // cleartext signatures are never encrypted ;) + signatureResult.setSignatureOnly(true); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + updateProgress(R.string.progress_done, 0, 100); + + 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(); + mOutStream.write(clearText); + + updateProgress(R.string.progress_processing_signature, 60, 100); + PGPObjectFactory pgpFact = new PGPObjectFactory(aIn); + + PGPSignatureList sigList = (PGPSignatureList) pgpFact.nextObject(); + if (sigList == null) { + throw new PgpGeneralException(mContext.getString(R.string.error_corrupt_data)); + } + PGPSignature signature = null; + long signatureKeyId = 0; + PGPPublicKey signatureKey = null; + for (int i = 0; i < sigList.size(); ++i) { + + signature = sigList.get(i); + signatureKeyId = signature.getKeyID(); + + // find data about this subkey + HashMap data = ProviderHelper.getGenericData(mContext, + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(signature.getKeyID())), + new String[] { KeyRings.MASTER_KEY_ID, KeyRings.USER_ID }, + new int[] { ProviderHelper.FIELD_TYPE_INTEGER, ProviderHelper.FIELD_TYPE_STRING }); + // any luck? otherwise, try next. + if(data.get(KeyRings.MASTER_KEY_ID) == null) { + signature = null; + // do NOT reset signatureKeyId, that one is shown when no known one is found! + continue; + } + + // this one can't fail now (yay database constraints) + signatureKey = ProviderHelper.getPGPPublicKeyRing(mContext, (Long) data.get(KeyRings.MASTER_KEY_ID)).getPublicKey(); + signatureResult.setUserId((String) data.get(KeyRings.USER_ID)); + + break; + } + + signatureResult.setKeyId(signatureKeyId); + + if (signature == null) { + signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY); + returnData.setSignatureResult(signatureResult); + + updateProgress(R.string.progress_done, 100, 100); + return returnData; + } + + JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = + new JcaPGPContentVerifierBuilderProvider() + .setProvider(Constants.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); + } + + //Now check binding signatures + boolean validKeyBinding = verifyKeyBinding(mContext, signature, signatureKey); + boolean validSignature = signature.verify(); + + if (validSignature & validKeyBinding) { + signatureResult.setStatus(OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED); + } + + // TODO: what about SIGNATURE_SUCCESS_CERTIFIED and SIGNATURE_ERROR???? + + returnData.setSignatureResult(signatureResult); + + updateProgress(R.string.progress_done, 100, 100); + return returnData; + } + + private static boolean verifyKeyBinding(Context context, + PGPSignature signature, PGPPublicKey signatureKey) { + long signatureKeyId = signature.getKeyID(); + boolean validKeyBinding = false; + + PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingWithKeyId(context, + signatureKeyId); + PGPPublicKey mKey = null; + if (signKeyRing != null) { + mKey = signKeyRing.getPublicKey(); + } + + if (signature.getKeyID() != mKey.getKeyID()) { + validKeyBinding = verifyKeyBinding(mKey, signatureKey); + } else { //if the key used to make the signature was the master key, no need to check binding sigs + validKeyBinding = true; + } + return validKeyBinding; + } + + private static boolean verifyKeyBinding(PGPPublicKey masterPublicKey, PGPPublicKey signingPublicKey) { + boolean validSubkeyBinding = false; + boolean validTempSubkeyBinding = false; + boolean validPrimaryKeyBinding = false; + + JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = + new JcaPGPContentVerifierBuilderProvider() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + + Iterator itr = signingPublicKey.getSignatures(); + + while (itr.hasNext()) { //what does gpg do if the subkey binding is wrong? + //gpg has an invalid subkey binding error on key import I think, but doesn't shout + //about keys without subkey signing. Can't get it to import a slightly broken one + //either, so we will err on bad subkey binding here. + PGPSignature sig = itr.next(); + if (sig.getKeyID() == masterPublicKey.getKeyID() && + sig.getSignatureType() == PGPSignature.SUBKEY_BINDING) { + //check and if ok, check primary key binding. + try { + sig.init(contentVerifierBuilderProvider, masterPublicKey); + validTempSubkeyBinding = sig.verifyCertification(masterPublicKey, signingPublicKey); + } catch (PGPException e) { + continue; + } catch (SignatureException e) { + continue; + } + + if (validTempSubkeyBinding) { + validSubkeyBinding = true; + } + if (validTempSubkeyBinding) { + validPrimaryKeyBinding = verifyPrimaryKeyBinding(sig.getUnhashedSubPackets(), + masterPublicKey, signingPublicKey); + if (validPrimaryKeyBinding) { + break; + } + validPrimaryKeyBinding = verifyPrimaryKeyBinding(sig.getHashedSubPackets(), + masterPublicKey, signingPublicKey); + if (validPrimaryKeyBinding) { + break; + } + } + } + } + return (validSubkeyBinding & validPrimaryKeyBinding); + } + + private static boolean verifyPrimaryKeyBinding(PGPSignatureSubpacketVector pkts, + PGPPublicKey masterPublicKey, + PGPPublicKey signingPublicKey) { + boolean validPrimaryKeyBinding = false; + JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = + new JcaPGPContentVerifierBuilderProvider() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + PGPSignatureList eSigList; + + if (pkts.hasSubpacket(SignatureSubpacketTags.EMBEDDED_SIGNATURE)) { + try { + eSigList = pkts.getEmbeddedSignatures(); + } catch (IOException e) { + return false; + } catch (PGPException e) { + return false; + } + for (int j = 0; j < eSigList.size(); ++j) { + PGPSignature emSig = eSigList.get(j); + if (emSig.getSignatureType() == PGPSignature.PRIMARYKEY_BINDING) { + try { + emSig.init(contentVerifierBuilderProvider, signingPublicKey); + validPrimaryKeyBinding = emSig.verifyCertification(masterPublicKey, signingPublicKey); + if (validPrimaryKeyBinding) { + break; + } + } catch (PGPException e) { + continue; + } catch (SignatureException e) { + continue; + } + } + } + } + + return validPrimaryKeyBinding; + } + + /** + * Mostly taken from ClearSignedFileProcessor in Bouncy Castle + * + * @param sig + * @param line + * @throws SignatureException + */ + private static void processLine(PGPSignature sig, byte[] line) + throws SignatureException { + 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; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyResult.java new file mode 100644 index 000000000..ad240e834 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyResult.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.pgp; + +import android.os.Parcel; +import android.os.Parcelable; + +import org.openintents.openpgp.OpenPgpSignatureResult; + +public class PgpDecryptVerifyResult implements Parcelable { + public static final int SUCCESS = 1; + public static final int KEY_PASSHRASE_NEEDED = 2; + public static final int SYMMETRIC_PASSHRASE_NEEDED = 3; + + int mStatus; + long mKeyIdPassphraseNeeded; + + OpenPgpSignatureResult mSignatureResult; + + public int getStatus() { + return mStatus; + } + + public void setStatus(int mStatus) { + this.mStatus = mStatus; + } + + public long getKeyIdPassphraseNeeded() { + return mKeyIdPassphraseNeeded; + } + + public void setKeyIdPassphraseNeeded(long mKeyIdPassphraseNeeded) { + this.mKeyIdPassphraseNeeded = mKeyIdPassphraseNeeded; + } + + public OpenPgpSignatureResult getSignatureResult() { + return mSignatureResult; + } + + public void setSignatureResult(OpenPgpSignatureResult signatureResult) { + this.mSignatureResult = signatureResult; + } + + public PgpDecryptVerifyResult() { + + } + + public PgpDecryptVerifyResult(PgpDecryptVerifyResult b) { + this.mStatus = b.mStatus; + this.mKeyIdPassphraseNeeded = b.mKeyIdPassphraseNeeded; + this.mSignatureResult = b.mSignatureResult; + } + + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mStatus); + dest.writeLong(mKeyIdPassphraseNeeded); + dest.writeParcelable(mSignatureResult, 0); + } + + public static final Creator CREATOR = new Creator() { + public PgpDecryptVerifyResult createFromParcel(final Parcel source) { + PgpDecryptVerifyResult vr = new PgpDecryptVerifyResult(); + vr.mStatus = source.readInt(); + vr.mKeyIdPassphraseNeeded = source.readLong(); + vr.mSignatureResult = source.readParcelable(OpenPgpSignatureResult.class.getClassLoader()); + return vr; + } + + public PgpDecryptVerifyResult[] newArray(final int size) { + return new PgpDecryptVerifyResult[size]; + } + }; +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java new file mode 100644 index 000000000..f884b1776 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; + +import org.spongycastle.openpgp.PGPEncryptedDataList; +import org.spongycastle.openpgp.PGPObjectFactory; +import org.spongycastle.openpgp.PGPPublicKeyEncryptedData; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPUtil; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.exception.NoAsymmetricEncryptionException; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ProgressDialogUpdater; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.security.SecureRandom; +import java.util.Iterator; +import java.util.regex.Pattern; + +public class PgpHelper { + + public static final Pattern PGP_MESSAGE = Pattern.compile( + ".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL); + + public static final Pattern PGP_CLEARTEXT_SIGNATURE = Pattern + .compile(".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----" + + "BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*", + Pattern.DOTALL); + + public static final Pattern PGP_PUBLIC_KEY = Pattern.compile( + ".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----).*", + Pattern.DOTALL); + + 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 "OpenPGP Keychain v" + getVersion(context); + } + + 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_invalid_data)); + } + + // 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.getPGPSecretKeyRing(context, pbe.getKeyID()).getSecretKey(); + if (secretKey != null) { + break; + } + } + } + + if (!gotAsymmetricEncryption) { + throw new NoAsymmetricEncryptionException(); + } + + if (secretKey == null) { + return Id.key.none; + } + + return secretKey.getKeyID(); + } + + 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; + } + + /** + * 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 IOException + */ + public static void deleteFileSecurely(Context context, ProgressDialogUpdater progress, File file) + throws 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_deleting_securely, 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpImportExport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpImportExport.java new file mode 100644 index 000000000..d03f3ccc2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpImportExport.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp; + +import android.content.Context; +import android.os.Bundle; +import android.os.Environment; + +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.spongycastle.openpgp.PGPException; +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.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +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.ui.adapter.ImportKeysListEntry; +import org.sufficientlysecure.keychain.util.HkpKeyServer; +import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.KeyServer.AddKeyException; +import org.sufficientlysecure.keychain.util.KeychainServiceListener; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ProgressDialogUpdater; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +public class PgpImportExport { + + private Context mContext; + private ProgressDialogUpdater mProgress; + + private KeychainServiceListener mKeychainServiceListener; + + public PgpImportExport(Context context, ProgressDialogUpdater progress) { + super(); + this.mContext = context; + this.mProgress = progress; + } + + public PgpImportExport(Context context, + ProgressDialogUpdater progress, KeychainServiceListener keychainListener) { + super(); + this.mContext = context; + this.mProgress = progress; + this.mKeychainServiceListener = keychainListener; + } + + public void updateProgress(int message, int current, int total) { + if (mProgress != null) { + mProgress.setProgress(message, current, total); + } + } + + public void updateProgress(String message, int current, int total) { + if (mProgress != null) { + mProgress.setProgress(message, current, total); + } + } + + public void updateProgress(int current, int total) { + if (mProgress != null) { + mProgress.setProgress(current, total); + } + } + + public boolean uploadKeyRingToServer(HkpKeyServer server, PGPPublicKeyRing keyring) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ArmoredOutputStream aos = null; + try { + aos = new ArmoredOutputStream(bos); + aos.write(keyring.getEncoded()); + aos.close(); + + String armoredKey = bos.toString("UTF-8"); + server.add(armoredKey); + + return true; + } catch (IOException e) { + return false; + } catch (AddKeyException e) { + // TODO: tell the user? + return false; + } finally { + try { + if (aos != null) { aos.close(); } + if (bos != null) { bos.close(); } + } catch (IOException e) { + } + } + } + + /** + * Imports keys from given data. If keyIds is given only those are imported + */ + public Bundle importKeyRings(List entries) + throws PgpGeneralException, PGPException, IOException { + Bundle returnData = new Bundle(); + + updateProgress(R.string.progress_importing, 0, 100); + + int newKeys = 0; + int oldKeys = 0; + int badKeys = 0; + + int position = 0; + try { + for (ImportKeysListEntry entry : entries) { + Object obj = PgpConversionHelper.BytesToPGPKeyRing(entry.getBytes()); + + if (obj instanceof PGPKeyRing) { + PGPKeyRing keyring = (PGPKeyRing) obj; + + int status = storeKeyRingInCache(keyring); + + if (status == Id.return_value.error) { + throw new PgpGeneralException( + mContext.getString(R.string.error_saving_keys)); + } + + // 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; + } + } else { + Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!"); + } + + position++; + updateProgress(position / entries.size() * 100, 100); + } + } 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); + + return returnData; + } + + public Bundle exportKeyRings(ArrayList publicKeyRingMasterIds, + ArrayList secretKeyRingMasterIds, + OutputStream outStream) throws PgpGeneralException, + PGPException, IOException { + Bundle returnData = new Bundle(); + + int masterKeyIdsSize = publicKeyRingMasterIds.size() + secretKeyRingMasterIds.size(); + int progress = 0; + + updateProgress( + mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, + masterKeyIdsSize), 0, 100); + + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + throw new PgpGeneralException( + mContext.getString(R.string.error_external_storage_not_ready)); + } + // For each public masterKey id + for (long pubKeyMasterId : publicKeyRingMasterIds) { + progress++; + // Create an output stream + ArmoredOutputStream arOutStream = new ArmoredOutputStream(outStream); + arOutStream.setHeader("Version", PgpHelper.getFullVersion(mContext)); + + updateProgress(progress * 100 / masterKeyIdsSize, 100); + PGPPublicKeyRing publicKeyRing = + ProviderHelper.getPGPPublicKeyRing(mContext, pubKeyMasterId); + + if (publicKeyRing != null) { + publicKeyRing.encode(arOutStream); + } + + if (mKeychainServiceListener.hasServiceStopped()) { + arOutStream.close(); + return null; + } + + arOutStream.close(); + } + + // For each secret masterKey id + for (long secretKeyMasterId : secretKeyRingMasterIds) { + progress++; + // Create an output stream + ArmoredOutputStream arOutStream = new ArmoredOutputStream(outStream); + arOutStream.setHeader("Version", PgpHelper.getFullVersion(mContext)); + + updateProgress(progress * 100 / masterKeyIdsSize, 100); + PGPSecretKeyRing secretKeyRing = + ProviderHelper.getPGPSecretKeyRing(mContext, secretKeyMasterId); + + if (secretKeyRing != null) { + secretKeyRing.encode(arOutStream); + } + if (mKeychainServiceListener.hasServiceStopped()) { + arOutStream.close(); + return null; + } + + arOutStream.close(); + } + + returnData.putInt(KeychainIntentService.RESULT_EXPORT, masterKeyIdsSize); + + updateProgress(R.string.progress_done, 100, 100); + + return returnData; + } + + /** + * TODO: implement Id.return_value.updated as status when key already existed + */ + @SuppressWarnings("unchecked") + public int storeKeyRingInCache(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; + + for (PGPSecretKey testSecretKey : new IterableIterator( + secretKeyRing.getSecretKeys())) { + if (!testSecretKey.isMasterKey()) { + if (testSecretKey.isPrivateKeyEmpty()) { + // this is bad, something is very wrong... + save = false; + status = Id.return_value.bad; + } + } + } + + if (save) { + // TODO: preserve certifications + // (http://osdir.com/ml/encryption.bouncy-castle.devel/2007-01/msg00054.html ?) + PGPPublicKeyRing newPubRing = null; + for (PGPPublicKey key : new IterableIterator( + secretKeyRing.getPublicKeys())) { + if (newPubRing == null) { + newPubRing = new PGPPublicKeyRing(key.getEncoded(), + new JcaKeyFingerprintCalculator()); + } + newPubRing = PGPPublicKeyRing.insertPublicKey(newPubRing, key); + } + if (newPubRing != null) { + ProviderHelper.saveKeyRing(mContext, newPubRing); + } + ProviderHelper.saveKeyRing(mContext, secretKeyRing); + // TODO: remove status returns, use exceptions! + status = Id.return_value.ok; + } + } else if (keyring instanceof PGPPublicKeyRing) { + PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring; + ProviderHelper.saveKeyRing(mContext, publicKeyRing); + // TODO: remove status returns, use exceptions! + status = Id.return_value.ok; + } + } catch (IOException e) { + status = Id.return_value.error; + } + + return status; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java new file mode 100644 index 000000000..4c786f555 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java @@ -0,0 +1,644 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp; + +import android.content.Context; +import android.graphics.Color; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.ForegroundColorSpan; + +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.spongycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.Log; + +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.Vector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PgpKeyHelper { + + private static final Pattern USER_ID_PATTERN = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$"); + + public static Date getCreationDate(PGPPublicKey key) { + return key.getCreationTime(); + } + + public static Date getCreationDate(PGPSecretKey key) { + return key.getPublicKey().getCreationTime(); + } + + @SuppressWarnings("unchecked") + public static PGPSecretKey getKeyNum(PGPSecretKeyRing keyRing, long num) { + long cnt = 0; + if (keyRing == null) { + return null; + } + for (PGPSecretKey key : new IterableIterator(keyRing.getSecretKeys())) { + if (cnt == num) { + return key; + } + cnt++; + } + + return null; + } + + @SuppressWarnings("unchecked") + public static Vector getEncryptKeys(PGPPublicKeyRing keyRing) { + Vector encryptKeys = new Vector(); + + for (PGPPublicKey key : new IterableIterator(keyRing.getPublicKeys())) { + if (isEncryptionKey(key)) { + encryptKeys.add(key); + } + } + + return encryptKeys; + } + + @SuppressWarnings("unchecked") + public static Vector getSigningKeys(PGPSecretKeyRing keyRing) { + Vector signingKeys = new Vector(); + + for (PGPSecretKey key : new IterableIterator(keyRing.getSecretKeys())) { + if (isSigningKey(key)) { + signingKeys.add(key); + } + } + + return signingKeys; + } + + @SuppressWarnings("unchecked") + public static Vector getCertificationKeys(PGPSecretKeyRing keyRing) { + Vector signingKeys = new Vector(); + + for (PGPSecretKey key : new IterableIterator(keyRing.getSecretKeys())) { + if (isCertificationKey(key)) { + signingKeys.add(key); + } + } + + return signingKeys; + } + + public static Vector getUsableEncryptKeys(PGPPublicKeyRing keyRing) { + Vector usableKeys = new Vector(); + Vector encryptKeys = getEncryptKeys(keyRing); + PGPPublicKey masterKey = null; + for (int i = 0; i < encryptKeys.size(); ++i) { + PGPPublicKey key = encryptKeys.get(i); + if (!isExpired(key) && !key.isRevoked()) { + 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 Vector getUsableCertificationKeys(PGPSecretKeyRing keyRing) { + Vector usableKeys = new Vector(); + Vector signingKeys = getCertificationKeys(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 Vector getUsableSigningKeys(PGPSecretKeyRing keyRing) { + Vector usableKeys = new Vector(); + Vector 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()); + + return calendar.getTime(); + } + + public static Date getExpiryDate(PGPSecretKey key) { + return getExpiryDate(key.getPublicKey()); + } + + public static PGPPublicKey getEncryptPublicKey(Context context, long masterKeyId) { + PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRing(context, masterKeyId); + if (keyRing == null) { + Log.e(Constants.TAG, "keyRing is null!"); + return null; + } + Vector encryptKeys = getUsableEncryptKeys(keyRing); + if (encryptKeys.size() == 0) { + Log.e(Constants.TAG, "encryptKeys is null!"); + return null; + } + return encryptKeys.get(0); + } + + public static PGPSecretKey getCertificationKey(Context context, long masterKeyId) { + PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRing(context, masterKeyId); + if (keyRing == null) { + return null; + } + Vector signingKeys = getUsableCertificationKeys(keyRing); + if (signingKeys.size() == 0) { + return null; + } + return signingKeys.get(0); + } + + public static PGPSecretKey getSigningKey(Context context, long masterKeyId) { + PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRing(context, masterKeyId); + if (keyRing == null) { + return null; + } + Vector 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(key.getUserIDs())) { + return userId; + } + return null; + } + + @SuppressWarnings("unchecked") + public static String getMainUserId(PGPSecretKey key) { + for (String userId : new IterableIterator(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.user_id_no_name); + } + return userId; + } + + public static String getMainUserIdSafe(Context context, PGPSecretKey key) { + String userId = getMainUserId(key); + if (userId == null || userId.equals("")) { + userId = context.getString(R.string.user_id_no_name); + } + return userId; + } + + public static int getKeyUsage(PGPSecretKey key) { + return getKeyUsage(key.getPublicKey()); + } + + @SuppressWarnings("unchecked") + private static int getKeyUsage(PGPPublicKey key) { + int usage = 0; + if (key.getVersion() >= 4) { + for (PGPSignature sig : new IterableIterator(key.getSignatures())) { + if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) { + continue; + } + + PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets(); + if (hashed != null) { + usage |= hashed.getKeyFlags(); + } + + PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets(); + if (unhashed != null) { + usage |= unhashed.getKeyFlags(); + } + } + } + return usage; + } + + @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(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(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()); + } + + @SuppressWarnings("unchecked") + public static boolean isCertificationKey(PGPPublicKey key) { + if (key.getVersion() <= 3) { + return true; + } + + for (PGPSignature sig : new IterableIterator(key.getSignatures())) { + if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) { + continue; + } + PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets(); + + if (hashed != null && (hashed.getKeyFlags() & KeyFlags.CERTIFY_OTHER) != 0) { + return true; + } + + PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets(); + + if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.CERTIFY_OTHER) != 0) { + return true; + } + } + + return false; + } + + public static boolean isAuthenticationKey(PGPSecretKey key) { + return isAuthenticationKey(key.getPublicKey()); + } + + @SuppressWarnings("unchecked") + public static boolean isAuthenticationKey(PGPPublicKey key) { + if (key.getVersion() <= 3) { + return true; + } + + for (PGPSignature sig : new IterableIterator(key.getSignatures())) { + if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) { + continue; + } + PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets(); + + if (hashed != null && (hashed.getKeyFlags() & KeyFlags.AUTHENTICATION) != 0) { + return true; + } + + PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets(); + + if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.AUTHENTICATION) != 0) { + return true; + } + } + + return false; + } + + public static boolean isCertificationKey(PGPSecretKey key) { + return isCertificationKey(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; + + 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 = "Unknown"; + break; + } + } + if(keySize > 0) + return algorithmStr + ", " + keySize + " bit"; + else + return algorithmStr; + } + + /** + * Converts fingerprint to hex (optional: with whitespaces after 4 characters) + *

+ * Fingerprint is shown using lowercase characters. Studies have shown that humans can + * better differentiate between numbers and letters when letters are lowercase. + * + * @param fingerprint + * @return + */ + public static String convertFingerprintToHex(byte[] fingerprint) { + String hexString = Hex.toHexString(fingerprint); + + return hexString; + } + + /** + * Convert key id from long to 64 bit hex string + *

+ * V4: "The Key ID is the low-order 64 bits of the fingerprint" + *

+ * see http://tools.ietf.org/html/rfc4880#section-12.2 + * + * @param keyId + * @return + */ + public static String convertKeyIdToHex(long keyId) { + long upper = keyId >> 32; + if (upper == 0) { + // this is a short key id + return convertKeyIdToHexShort(keyId); + } + return "0x" + convertKeyIdToHex32bit(keyId >> 32) + convertKeyIdToHex32bit(keyId); + } + + public static String convertKeyIdToHexShort(long keyId) { + return "0x" + convertKeyIdToHex32bit(keyId); + } + + private static String convertKeyIdToHex32bit(long keyId) { + String hexString = Long.toHexString(keyId & 0xffffffffL).toLowerCase(Locale.US); + while (hexString.length() < 8) { + hexString = "0" + hexString; + } + return hexString; + } + + + public static SpannableStringBuilder colorizeFingerprint(String fingerprint) { + // split by 4 characters + fingerprint = fingerprint.replaceAll("(.{4})(?!$)", "$1 "); + + // add line breaks to have a consistent "image" that can be recognized + char[] chars = fingerprint.toCharArray(); + chars[24] = '\n'; + fingerprint = String.valueOf(chars); + + SpannableStringBuilder sb = new SpannableStringBuilder(fingerprint); + try { + // for each 4 characters of the fingerprint + 1 space + for (int i = 0; i < fingerprint.length(); i += 5) { + int spanEnd = Math.min(i + 4, fingerprint.length()); + String fourChars = fingerprint.substring(i, spanEnd); + + int raw = Integer.parseInt(fourChars, 16); + byte[] bytes = {(byte) ((raw >> 8) & 0xff - 128), (byte) (raw & 0xff - 128)}; + int[] color = getRgbForData(bytes); + int r = color[0]; + int g = color[1]; + int b = color[2]; + + // we cannot change black by multiplication, so adjust it to an almost-black grey, + // which will then be brightened to the minimal brightness level + if (r == 0 && g == 0 && b == 0) { + r = 1; + g = 1; + b = 1; + } + + // Convert rgb to brightness + double brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b; + + // If a color is too dark to be seen on black, + // then brighten it up to a minimal brightness. + if (brightness < 80) { + double factor = 80.0 / brightness; + r = Math.min(255, (int) (r * factor)); + g = Math.min(255, (int) (g * factor)); + b = Math.min(255, (int) (b * factor)); + + // If it is too light, then darken it to a respective maximal brightness. + } else if (brightness > 180) { + double factor = 180.0 / brightness; + r = (int) (r * factor); + g = (int) (g * factor); + b = (int) (b * factor); + } + + // Create a foreground color with the 3 digest integers as RGB + // and then converting that int to hex to use as a color + sb.setSpan(new ForegroundColorSpan(Color.rgb(r, g, b)), + i, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE); + } + } catch (Exception e) { + Log.e(Constants.TAG, "Colorization failed", e); + // if anything goes wrong, then just display the fingerprint without colour, + // instead of partially correct colour or wrong colours + return new SpannableStringBuilder(fingerprint); + } + + return sb; + } + + /** + * Converts the given bytes to a unique RGB color using SHA1 algorithm + * + * @param bytes + * @return an integer array containing 3 numeric color representations (Red, Green, Black) + * @throws java.security.NoSuchAlgorithmException + * @throws java.security.DigestException + */ + private static int[] getRgbForData(byte[] bytes) throws NoSuchAlgorithmException, DigestException { + MessageDigest md = MessageDigest.getInstance("SHA1"); + + md.update(bytes); + byte[] digest = md.digest(); + + int[] result = {((int) digest[0] + 256) % 256, + ((int) digest[1] + 256) % 256, + ((int) digest[2] + 256) % 256}; + return result; + } + + /** + * Splits userId string into naming part, email part, and comment part + * + * @param userId + * @return array with naming (0), email (1), comment (2) + */ + public static String[] splitUserId(String userId) { + String[] result = new String[]{null, null, null}; + + if (userId == null || userId.equals("")) { + return result; + } + + /* + * User ID matching: + * http://fiddle.re/t4p6f + * + * test cases: + * "Max Mustermann (this is a comment) " + * "Max Mustermann " + * "Max Mustermann (this is a comment)" + * "Max Mustermann [this is nothing]" + */ + Matcher matcher = USER_ID_PATTERN.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(3); + result[2] = matcher.group(2); + } + + return result; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java new file mode 100644 index 000000000..48b959738 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java @@ -0,0 +1,769 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp; + +import android.util.Pair; + +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.spec.ElGamalParameterSpec; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPKeyPair; +import org.spongycastle.openpgp.PGPKeyRingGenerator; +import org.spongycastle.openpgp.PGPPrivateKey; +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.PGPSignatureGenerator; +import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.spongycastle.openpgp.PGPSignatureSubpacketVector; +import org.spongycastle.openpgp.PGPUtil; +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.jcajce.JcaPGPContentSignerBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyPair; +import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralMsgIdException; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.Primes; +import org.sufficientlysecure.keychain.util.ProgressDialogUpdater; + +import java.io.IOException; +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.SignatureException; +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Iterator; +import java.util.List; +import java.util.TimeZone; + +/** This class is the single place where ALL operations that actually modify a PGP public or secret + * key take place. + * + * Note that no android specific stuff should be done here, ie no imports from com.android. + * + * All operations support progress reporting to a ProgressDialogUpdater passed on initialization. + * This indicator may be null. + * + */ +public class PgpKeyOperation { + private ProgressDialogUpdater mProgress; + + 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 PgpKeyOperation(ProgressDialogUpdater progress) { + super(); + this.mProgress = progress; + } + + void updateProgress(int message, int current, int total) { + if (mProgress != null) { + mProgress.setProgress(message, current, total); + } + } + + void updateProgress(int current, int total) { + if (mProgress != null) { + mProgress.setProgress(current, total); + } + } + + /** + * Creates new secret key. + * + * @param algorithmChoice + * @param keySize + * @param passphrase + * @param isMasterKey + * @return A newly created PGPSecretKey + * @throws NoSuchAlgorithmException + * @throws PGPException + * @throws NoSuchProviderException + * @throws PgpGeneralMsgIdException + * @throws InvalidAlgorithmParameterException + */ + + // TODO: key flags? + public PGPSecretKey createKey(int algorithmChoice, int keySize, String passphrase, + boolean isMasterKey) + throws NoSuchAlgorithmException, PGPException, NoSuchProviderException, + PgpGeneralMsgIdException, InvalidAlgorithmParameterException { + + if (keySize < 512) { + throw new PgpGeneralMsgIdException(R.string.error_key_size_minimum512bit); + } + + if (passphrase == null) { + passphrase = ""; + } + + int algorithm; + KeyPairGenerator keyGen; + + switch (algorithmChoice) { + case Id.choice.algorithm.dsa: { + keyGen = KeyPairGenerator.getInstance("DSA", Constants.BOUNCY_CASTLE_PROVIDER_NAME); + keyGen.initialize(keySize, new SecureRandom()); + algorithm = PGPPublicKey.DSA; + break; + } + + case Id.choice.algorithm.elgamal: { + if (isMasterKey) { + throw new PgpGeneralMsgIdException(R.string.error_master_key_must_not_be_el_gamal); + } + keyGen = KeyPairGenerator.getInstance("ElGamal", Constants.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", Constants.BOUNCY_CASTLE_PROVIDER_NAME); + keyGen.initialize(keySize, new SecureRandom()); + + algorithm = PGPPublicKey.RSA_GENERAL; + break; + } + + default: { + throw new PgpGeneralMsgIdException(R.string.error_unknown_algorithm_choice); + } + } + + // 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(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray()); + + return new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(), + sha1Calc, isMasterKey, keyEncryptor); + } + + public PGPSecretKeyRing changeSecretKeyPassphrase(PGPSecretKeyRing keyRing, String oldPassphrase, + String newPassphrase) + throws IOException, PGPException, NoSuchProviderException { + + updateProgress(R.string.progress_building_key, 0, 100); + if (oldPassphrase == null) { + oldPassphrase = ""; + } + if (newPassphrase == null) { + newPassphrase = ""; + } + + PGPSecretKeyRing newKeyRing = PGPSecretKeyRing.copyWithNewPassword( + keyRing, + new JcePBESecretKeyDecryptorBuilder(new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build()).setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassphrase.toCharArray()), + new JcePBESecretKeyEncryptorBuilder(keyRing.getSecretKey() + .getKeyEncryptionAlgorithm()).build(newPassphrase.toCharArray())); + + return newKeyRing; + + } + + private Pair buildNewSecretKey( + ArrayList userIds, ArrayList keys, + ArrayList keysExpiryDates, + ArrayList keysUsages, + String newPassphrase, String oldPassphrase) + throws PgpGeneralMsgIdException, PGPException, SignatureException, IOException { + + int usageId = keysUsages.get(0); + boolean canSign; + String mainUserId = userIds.get(0); + + PGPSecretKey masterKey = keys.get(0); + + // this removes all userIds and certifications previously attached to the masterPublicKey + PGPPublicKey masterPublicKey = masterKey.getPublicKey(); + + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassphrase.toCharArray()); + PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor); + + updateProgress(R.string.progress_certifying_master_key, 20, 100); + + for (String userId : userIds) { + PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( + masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1) + .setProvider(Constants.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); + } + + PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey); + + PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator(); + PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator(); + + hashedPacketsGen.setKeyFlags(true, usageId); + + hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS); + hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS); + hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS); + + if (keysExpiryDates.get(0) != null) { + GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + creationDate.setTime(masterPublicKey.getCreationTime()); + GregorianCalendar expiryDate = keysExpiryDates.get(0); + //note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c + //here we purposefully ignore partial days in each date - long type has no fractional part! + long numDays = (expiryDate.getTimeInMillis() / 86400000) - + (creationDate.getTimeInMillis() / 86400000); + if (numDays <= 0) { + throw new PgpGeneralMsgIdException(R.string.error_expiry_must_come_after_creation); + } + hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400); + } else { + hashedPacketsGen.setKeyExpirationTime(false, 0); + // do this explicitly, although since we're rebuilding, + // this happens anyway + } + + updateProgress(R.string.progress_building_master_key, 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(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + newPassphrase.toCharArray()); + + PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, + masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(), + unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor); + + updateProgress(R.string.progress_adding_sub_keys, 40, 100); + + for (int i = 1; i < keys.size(); ++i) { + updateProgress(40 + 40 * (i - 1) / (keys.size() - 1), 100); + + PGPSecretKey subKey = keys.get(i); + PGPPublicKey subPublicKey = subKey.getPublicKey(); + + PBESecretKeyDecryptor keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder() + .setProvider(Constants.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(); + + usageId = keysUsages.get(i); + canSign = (usageId & KeyFlags.SIGN_DATA) > 0; //todo - separate function for this + if (canSign) { + Date todayDate = new Date(); //both sig times the same + // cross-certify signing keys + hashedPacketsGen.setSignatureCreationTime(false, todayDate); //set outer creation time + PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator(); + subHashedPacketsGen.setSignatureCreationTime(false, todayDate); //set inner creation time + PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( + subPublicKey.getAlgorithm(), PGPUtil.SHA1) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder); + sGen.init(PGPSignature.PRIMARYKEY_BINDING, subPrivateKey); + sGen.setHashedSubpackets(subHashedPacketsGen.generate()); + PGPSignature certification = sGen.generateCertification(masterPublicKey, + subPublicKey); + unhashedPacketsGen.setEmbeddedSignature(false, certification); + } + hashedPacketsGen.setKeyFlags(false, usageId); + + if (keysExpiryDates.get(i) != null) { + GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + creationDate.setTime(subPublicKey.getCreationTime()); + GregorianCalendar expiryDate = keysExpiryDates.get(i); + //note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c + //here we purposefully ignore partial days in each date - long type has no fractional part! + long numDays = (expiryDate.getTimeInMillis() / 86400000) - + (creationDate.getTimeInMillis() / 86400000); + if (numDays <= 0) { + throw new PgpGeneralMsgIdException(R.string.error_expiry_must_come_after_creation); + } + hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400); + } else { + hashedPacketsGen.setKeyExpirationTime(false, 0); + // do this explicitly, although since we're rebuilding, + // this happens anyway + } + + keyGen.addSubKey(subKeyPair, hashedPacketsGen.generate(), unhashedPacketsGen.generate()); + } + + PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing(); + PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing(); + + return new Pair(secretKeyRing, publicKeyRing); + + } + + public Pair buildSecretKey(PGPSecretKeyRing mKR, + PGPPublicKeyRing pKR, + SaveKeyringParcel saveParcel) + throws PgpGeneralMsgIdException, PGPException, SignatureException, IOException { + + updateProgress(R.string.progress_building_key, 0, 100); + PGPSecretKey masterKey = saveParcel.keys.get(0); + + if (saveParcel.oldPassphrase == null) { + saveParcel.oldPassphrase = ""; + } + if (saveParcel.newPassphrase == null) { + saveParcel.newPassphrase = ""; + } + + if (mKR == null) { + return buildNewSecretKey(saveParcel.userIDs, saveParcel.keys, saveParcel.keysExpiryDates, + saveParcel.keysUsages, saveParcel.newPassphrase, saveParcel.oldPassphrase); //new Keyring + } + + /* + IDs - NB This might not need to happen later, if we change the way the primary ID is chosen + remove deleted ids + if the primary ID changed we need to: + remove all of the IDs from the keyring, saving their certifications + add them all in again, updating certs of IDs which have changed + else + remove changed IDs and add in with new certs + + if the master key changed, we need to remove the primary ID certification, so we can add + the new one when it is generated, and they don't conflict + + Keys + remove deleted keys + if a key is modified, re-sign it + do we need to remove and add in? + + Todo + identify more things which need to be preserved - e.g. trust levels? + user attributes + */ + + if (saveParcel.deletedKeys != null) { + for (PGPSecretKey dKey : saveParcel.deletedKeys) { + mKR = PGPSecretKeyRing.removeSecretKey(mKR, dKey); + } + } + + masterKey = mKR.getSecretKey(); + PGPPublicKey masterPublicKey = masterKey.getPublicKey(); + + int usageId = saveParcel.keysUsages.get(0); + boolean canSign; + String mainUserId = saveParcel.userIDs.get(0); + + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(saveParcel.oldPassphrase.toCharArray()); + PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor); + + updateProgress(R.string.progress_certifying_master_key, 20, 100); + + boolean anyIDChanged = false; + for (String delID : saveParcel.deletedIDs) { + anyIDChanged = true; + masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, delID); + } + + int userIDIndex = 0; + + PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator(); + PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator(); + + hashedPacketsGen.setKeyFlags(true, usageId); + + hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS); + hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS); + hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS); + + if (saveParcel.keysExpiryDates.get(0) != null) { + GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + creationDate.setTime(masterPublicKey.getCreationTime()); + GregorianCalendar expiryDate = saveParcel.keysExpiryDates.get(0); + //note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c + //here we purposefully ignore partial days in each date - long type has no fractional part! + long numDays = (expiryDate.getTimeInMillis() / 86400000) - + (creationDate.getTimeInMillis() / 86400000); + if (numDays <= 0) { + throw new PgpGeneralMsgIdException(R.string.error_expiry_must_come_after_creation); + } + hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400); + } else { + hashedPacketsGen.setKeyExpirationTime(false, 0); + // do this explicitly, although since we're rebuilding, + // this happens anyway + } + + if (saveParcel.primaryIDChanged || + !saveParcel.originalIDs.get(0).equals(saveParcel.userIDs.get(0))) { + anyIDChanged = true; + ArrayList> sigList = new ArrayList>(); + for (String userId : saveParcel.userIDs) { + String origID = saveParcel.originalIDs.get(userIDIndex); + if (origID.equals(userId) && !saveParcel.newIDs[userIDIndex] && + !userId.equals(saveParcel.originalPrimaryID) && userIDIndex != 0) { + Iterator origSigs = masterPublicKey.getSignaturesForID(origID); + // TODO: make sure this iterator only has signatures we are interested in + while (origSigs.hasNext()) { + PGPSignature origSig = origSigs.next(); + sigList.add(new Pair(origID, origSig)); + } + } else { + PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( + masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder); + + sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey); + if (userIDIndex == 0) { + sGen.setHashedSubpackets(hashedPacketsGen.generate()); + sGen.setUnhashedSubpackets(unhashedPacketsGen.generate()); + } + PGPSignature certification = sGen.generateCertification(userId, masterPublicKey); + sigList.add(new Pair(userId, certification)); + } + if (!saveParcel.newIDs[userIDIndex]) { + masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, origID); + } + userIDIndex++; + } + for (Pair toAdd : sigList) { + masterPublicKey = + PGPPublicKey.addCertification(masterPublicKey, toAdd.first, toAdd.second); + } + } else { + for (String userId : saveParcel.userIDs) { + String origID = saveParcel.originalIDs.get(userIDIndex); + if (!origID.equals(userId) || saveParcel.newIDs[userIDIndex]) { + anyIDChanged = true; + PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( + masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder); + + sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey); + if (userIDIndex == 0) { + sGen.setHashedSubpackets(hashedPacketsGen.generate()); + sGen.setUnhashedSubpackets(unhashedPacketsGen.generate()); + } + PGPSignature certification = sGen.generateCertification(userId, masterPublicKey); + if (!saveParcel.newIDs[userIDIndex]) { + masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, origID); + } + masterPublicKey = + PGPPublicKey.addCertification(masterPublicKey, userId, certification); + } + userIDIndex++; + } + } + + ArrayList> sigList = new ArrayList>(); + if (saveParcel.moddedKeys[0]) { + userIDIndex = 0; + for (String userId : saveParcel.userIDs) { + String origID = saveParcel.originalIDs.get(userIDIndex); + if (!(origID.equals(saveParcel.originalPrimaryID) && !saveParcel.primaryIDChanged)) { + Iterator sigs = masterPublicKey.getSignaturesForID(userId); + // TODO: make sure this iterator only has signatures we are interested in + while (sigs.hasNext()) { + PGPSignature sig = sigs.next(); + sigList.add(new Pair(userId, sig)); + } + } + masterPublicKey = PGPPublicKey.removeCertification(masterPublicKey, userId); + userIDIndex++; + } + anyIDChanged = true; + } + + //update the keyring with the new ID information + if (anyIDChanged) { + pKR = PGPPublicKeyRing.insertPublicKey(pKR, masterPublicKey); + mKR = PGPSecretKeyRing.replacePublicKeys(mKR, pKR); + } + + PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey); + + updateProgress(R.string.progress_building_master_key, 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 encryptor based on old passphrase, as some keys may be unchanged + PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder( + PGPEncryptedData.CAST5, sha1Calc) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + saveParcel.oldPassphrase.toCharArray()); + + //this generates one more signature than necessary... + PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, + masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(), + unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor); + + for (int i = 1; i < saveParcel.keys.size(); ++i) { + updateProgress(40 + 50 * i / saveParcel.keys.size(), 100); + if (saveParcel.moddedKeys[i]) { + PGPSecretKey subKey = saveParcel.keys.get(i); + PGPPublicKey subPublicKey = subKey.getPublicKey(); + + PBESecretKeyDecryptor keyDecryptor2; + if (saveParcel.newKeys[i]) { + keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + "".toCharArray()); + } else { + keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + saveParcel.oldPassphrase.toCharArray()); + } + PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(keyDecryptor2); + PGPKeyPair subKeyPair = new PGPKeyPair(subPublicKey, subPrivateKey); + + hashedPacketsGen = new PGPSignatureSubpacketGenerator(); + unhashedPacketsGen = new PGPSignatureSubpacketGenerator(); + + usageId = saveParcel.keysUsages.get(i); + canSign = (usageId & KeyFlags.SIGN_DATA) > 0; //todo - separate function for this + if (canSign) { + Date todayDate = new Date(); //both sig times the same + // cross-certify signing keys + hashedPacketsGen.setSignatureCreationTime(false, todayDate); //set outer creation time + PGPSignatureSubpacketGenerator subHashedPacketsGen = new PGPSignatureSubpacketGenerator(); + subHashedPacketsGen.setSignatureCreationTime(false, todayDate); //set inner creation time + PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( + subPublicKey.getAlgorithm(), PGPUtil.SHA1) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder); + sGen.init(PGPSignature.PRIMARYKEY_BINDING, subPrivateKey); + sGen.setHashedSubpackets(subHashedPacketsGen.generate()); + PGPSignature certification = sGen.generateCertification(masterPublicKey, + subPublicKey); + unhashedPacketsGen.setEmbeddedSignature(false, certification); + } + hashedPacketsGen.setKeyFlags(false, usageId); + + if (saveParcel.keysExpiryDates.get(i) != null) { + GregorianCalendar creationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + creationDate.setTime(subPublicKey.getCreationTime()); + GregorianCalendar expiryDate = saveParcel.keysExpiryDates.get(i); + // note that the below, (a/c) - (b/c) is *not* the same as (a - b) /c + // here we purposefully ignore partial days in each date - long type has + // no fractional part! + long numDays = (expiryDate.getTimeInMillis() / 86400000) - + (creationDate.getTimeInMillis() / 86400000); + if (numDays <= 0) { + throw new PgpGeneralMsgIdException(R.string.error_expiry_must_come_after_creation); + } + hashedPacketsGen.setKeyExpirationTime(false, numDays * 86400); + } else { + hashedPacketsGen.setKeyExpirationTime(false, 0); + // do this explicitly, although since we're rebuilding, + // this happens anyway + } + + keyGen.addSubKey(subKeyPair, hashedPacketsGen.generate(), unhashedPacketsGen.generate()); + // certifications will be discarded if the key is changed, because I think, for a start, + // they will be invalid. Binding certs are regenerated anyway, and other certs which + // need to be kept are on IDs and attributes + // TODO: don't let revoked keys be edited, other than removed - changing one would + // result in the revocation being wrong? + } + } + + PGPSecretKeyRing updatedSecretKeyRing = keyGen.generateSecretKeyRing(); + //finally, update the keyrings + Iterator itr = updatedSecretKeyRing.getSecretKeys(); + while (itr.hasNext()) { + PGPSecretKey theNextKey = itr.next(); + if ((theNextKey.isMasterKey() && saveParcel.moddedKeys[0]) || !theNextKey.isMasterKey()) { + mKR = PGPSecretKeyRing.insertSecretKey(mKR, theNextKey); + pKR = PGPPublicKeyRing.insertPublicKey(pKR, theNextKey.getPublicKey()); + } + } + + //replace lost IDs + if (saveParcel.moddedKeys[0]) { + masterPublicKey = mKR.getPublicKey(); + for (Pair toAdd : sigList) { + masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, toAdd.first, toAdd.second); + } + pKR = PGPPublicKeyRing.insertPublicKey(pKR, masterPublicKey); + mKR = PGPSecretKeyRing.replacePublicKeys(mKR, pKR); + } + + // Build key encryptor based on new passphrase + PBESecretKeyEncryptor keyEncryptorNew = new JcePBESecretKeyEncryptorBuilder( + PGPEncryptedData.CAST5, sha1Calc) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + saveParcel.newPassphrase.toCharArray()); + + //update the passphrase + mKR = PGPSecretKeyRing.copyWithNewPassword(mKR, keyDecryptor, keyEncryptorNew); + + /* additional handy debug info + + Log.d(Constants.TAG, " ------- in private key -------"); + + for(String uid : new IterableIterator(secretKeyRing.getPublicKey().getUserIDs())) { + for(PGPSignature sig : new IterableIterator( + secretKeyRing.getPublicKey().getSignaturesForID(uid))) { + Log.d(Constants.TAG, "sig: " + + PgpKeyHelper.convertKeyIdToHex(sig.getKeyID()) + " for " + uid); + } + + } + + Log.d(Constants.TAG, " ------- in public key -------"); + + for(String uid : new IterableIterator(publicKeyRing.getPublicKey().getUserIDs())) { + for(PGPSignature sig : new IterableIterator( + publicKeyRing.getPublicKey().getSignaturesForID(uid))) { + Log.d(Constants.TAG, "sig: " + + PgpKeyHelper.convertKeyIdToHex(sig.getKeyID()) + " for " + uid); + } + } + + */ + + return new Pair(mKR, pKR); + + } + + /** + * Certify the given pubkeyid with the given masterkeyid. + * + * @param certificationKey Certifying key + * @param publicKey public key to certify + * @param userIds User IDs to certify, must not be null or empty + * @param passphrase Passphrase of the secret key + * @return A keyring with added certifications + */ + public PGPPublicKey certifyKey(PGPSecretKey certificationKey, PGPPublicKey publicKey, + List userIds, String passphrase) + throws PgpGeneralMsgIdException, NoSuchAlgorithmException, NoSuchProviderException, + PGPException, SignatureException { + + // create a signatureGenerator from the supplied masterKeyId and passphrase + PGPSignatureGenerator signatureGenerator; { + + if (certificationKey == null) { + throw new PgpGeneralMsgIdException(R.string.error_signature_failed); + } + + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray()); + PGPPrivateKey signaturePrivateKey = certificationKey.extractPrivateKey(keyDecryptor); + if (signaturePrivateKey == null) { + throw new PgpGeneralMsgIdException(R.string.error_could_not_extract_private_key); + } + + // TODO: SHA256 fixed? + JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder( + certificationKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + + signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); + signatureGenerator.init(PGPSignature.DEFAULT_CERTIFICATION, signaturePrivateKey); + } + + { // supply signatureGenerator with a SubpacketVector + PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); + PGPSignatureSubpacketVector packetVector = spGen.generate(); + signatureGenerator.setHashedSubpackets(packetVector); + } + + // fetch public key ring, add the certification and return it + for (String userId : new IterableIterator(userIds.iterator())) { + PGPSignature sig = signatureGenerator.generateCertification(userId, publicKey); + publicKey = PGPPublicKey.addCertification(publicKey, userId, sig); + } + + return publicKey; + } + + /** Simple static subclass that stores two values. + * + * This is only used to return a pair of values in one function above. We specifically don't use + * com.android.Pair to keep this class free from android dependencies. + */ + public static class Pair { + public final K first; + public final V second; + public Pair(K first, V second) { + this.first = first; + this.second = second; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncrypt.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncrypt.java new file mode 100644 index 000000000..a864a165d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncrypt.java @@ -0,0 +1,607 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp; + +import android.content.Context; + +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.spongycastle.bcpg.BCPGOutputStream; +import org.spongycastle.openpgp.PGPCompressedDataGenerator; +import org.spongycastle.openpgp.PGPEncryptedDataGenerator; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPLiteralData; +import org.spongycastle.openpgp.PGPLiteralDataGenerator; +import org.spongycastle.openpgp.PGPPrivateKey; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureGenerator; +import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.spongycastle.openpgp.PGPV3SignatureGenerator; +import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; +import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ProgressDialogUpdater; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.util.Date; + +/** + * This class uses a Builder pattern! + */ +public class PgpSignEncrypt { + private Context mContext; + private InputData mData; + private OutputStream mOutStream; + + private ProgressDialogUpdater mProgress; + private boolean mEnableAsciiArmorOutput; + private int mCompressionId; + private long[] mEncryptionKeyIds; + private String mSymmetricPassphrase; + private int mSymmetricEncryptionAlgorithm; + private long mSignatureKeyId; + private int mSignatureHashAlgorithm; + private boolean mSignatureForceV3; + private String mSignaturePassphrase; + + private PgpSignEncrypt(Builder builder) { + // private Constructor can only be called from Builder + this.mContext = builder.mContext; + this.mData = builder.mData; + this.mOutStream = builder.mOutStream; + + this.mProgress = builder.mProgress; + this.mEnableAsciiArmorOutput = builder.mEnableAsciiArmorOutput; + this.mCompressionId = builder.mCompressionId; + this.mEncryptionKeyIds = builder.mEncryptionKeyIds; + this.mSymmetricPassphrase = builder.mSymmetricPassphrase; + this.mSymmetricEncryptionAlgorithm = builder.mSymmetricEncryptionAlgorithm; + this.mSignatureKeyId = builder.mSignatureKeyId; + this.mSignatureHashAlgorithm = builder.mSignatureHashAlgorithm; + this.mSignatureForceV3 = builder.mSignatureForceV3; + this.mSignaturePassphrase = builder.mSignaturePassphrase; + } + + public static class Builder { + // mandatory parameter + private Context mContext; + private InputData mData; + private OutputStream mOutStream; + + // optional + private ProgressDialogUpdater mProgress = null; + private boolean mEnableAsciiArmorOutput = false; + private int mCompressionId = Id.choice.compression.none; + private long[] mEncryptionKeyIds = null; + private String mSymmetricPassphrase = null; + private int mSymmetricEncryptionAlgorithm = 0; + private long mSignatureKeyId = Id.key.none; + private int mSignatureHashAlgorithm = 0; + private boolean mSignatureForceV3 = false; + private String mSignaturePassphrase = null; + + public Builder(Context context, InputData data, OutputStream outStream) { + this.mContext = context; + this.mData = data; + this.mOutStream = outStream; + } + + public Builder progress(ProgressDialogUpdater progress) { + this.mProgress = progress; + return this; + } + + public Builder enableAsciiArmorOutput(boolean enableAsciiArmorOutput) { + this.mEnableAsciiArmorOutput = enableAsciiArmorOutput; + return this; + } + + public Builder compressionId(int compressionId) { + this.mCompressionId = compressionId; + return this; + } + + public Builder encryptionKeyIds(long[] encryptionKeyIds) { + this.mEncryptionKeyIds = encryptionKeyIds; + return this; + } + + public Builder symmetricPassphrase(String symmetricPassphrase) { + this.mSymmetricPassphrase = symmetricPassphrase; + return this; + } + + public Builder symmetricEncryptionAlgorithm(int symmetricEncryptionAlgorithm) { + this.mSymmetricEncryptionAlgorithm = symmetricEncryptionAlgorithm; + return this; + } + + public Builder signatureKeyId(long signatureKeyId) { + this.mSignatureKeyId = signatureKeyId; + return this; + } + + public Builder signatureHashAlgorithm(int signatureHashAlgorithm) { + this.mSignatureHashAlgorithm = signatureHashAlgorithm; + return this; + } + + public Builder signatureForceV3(boolean signatureForceV3) { + this.mSignatureForceV3 = signatureForceV3; + return this; + } + + public Builder signaturePassphrase(String signaturePassphrase) { + this.mSignaturePassphrase = signaturePassphrase; + return this; + } + + public PgpSignEncrypt build() { + return new PgpSignEncrypt(this); + } + } + + public void updateProgress(int message, int current, int total) { + if (mProgress != null) { + mProgress.setProgress(message, current, total); + } + } + + public void updateProgress(int current, int total) { + if (mProgress != null) { + mProgress.setProgress(current, total); + } + } + + /** + * Signs and/or encrypts data based on parameters of class + * + * @throws IOException + * @throws PgpGeneralException + * @throws PGPException + * @throws NoSuchProviderException + * @throws NoSuchAlgorithmException + * @throws SignatureException + */ + public void execute() + throws IOException, PgpGeneralException, PGPException, NoSuchProviderException, + NoSuchAlgorithmException, SignatureException { + + boolean enableSignature = mSignatureKeyId != Id.key.none; + boolean enableEncryption = ((mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) + || mSymmetricPassphrase != null); + boolean enableCompression = (enableEncryption && mCompressionId != Id.choice.compression.none); + + Log.d(Constants.TAG, "enableSignature:" + enableSignature + + "\nenableEncryption:" + enableEncryption + + "\nenableCompression:" + enableCompression + + "\nenableAsciiArmorOutput:" + mEnableAsciiArmorOutput); + + int signatureType; + if (mEnableAsciiArmorOutput && enableSignature && !enableEncryption && !enableCompression) { + // for sign-only ascii text + signatureType = PGPSignature.CANONICAL_TEXT_DOCUMENT; + } else { + signatureType = PGPSignature.BINARY_DOCUMENT; + } + + ArmoredOutputStream armorOut = null; + OutputStream out; + if (mEnableAsciiArmorOutput) { + armorOut = new ArmoredOutputStream(mOutStream); + armorOut.setHeader("Version", PgpHelper.getFullVersion(mContext)); + out = armorOut; + } else { + out = mOutStream; + } + + /* Get keys for signature generation for later usage */ + PGPSecretKey signingKey = null; + PGPSecretKeyRing signingKeyRing = null; + PGPPrivateKey signaturePrivateKey = null; + if (enableSignature) { + signingKeyRing = ProviderHelper.getPGPSecretKeyRingWithKeyId(mContext, mSignatureKeyId); + signingKey = PgpKeyHelper.getSigningKey(mContext, mSignatureKeyId); + if (signingKey == null) { + throw new PgpGeneralException(mContext.getString(R.string.error_signature_failed)); + } + + if (mSignaturePassphrase == null) { + throw new PgpGeneralException( + mContext.getString(R.string.error_no_signature_passphrase)); + } + + updateProgress(R.string.progress_extracting_signature_key, 0, 100); + + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mSignaturePassphrase.toCharArray()); + signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor); + if (signaturePrivateKey == null) { + throw new PgpGeneralException( + mContext.getString(R.string.error_could_not_extract_private_key)); + } + } + updateProgress(R.string.progress_preparing_streams, 5, 100); + + /* Initialize PGPEncryptedDataGenerator for later usage */ + PGPEncryptedDataGenerator cPk = null; + if (enableEncryption) { + // has Integrity packet enabled! + JcePGPDataEncryptorBuilder encryptorBuilder = + new JcePGPDataEncryptorBuilder(mSymmetricEncryptionAlgorithm) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME) + .setWithIntegrityPacket(true); + + cPk = new PGPEncryptedDataGenerator(encryptorBuilder); + + if (mSymmetricPassphrase != null) { + // Symmetric encryption + Log.d(Constants.TAG, "encryptionKeyIds length is 0 -> symmetric encryption"); + + JcePBEKeyEncryptionMethodGenerator symmetricEncryptionGenerator = + new JcePBEKeyEncryptionMethodGenerator(mSymmetricPassphrase.toCharArray()); + cPk.addMethod(symmetricEncryptionGenerator); + } else { + // Asymmetric encryption + for (long id : mEncryptionKeyIds) { + PGPPublicKey key = PgpKeyHelper.getEncryptPublicKey(mContext, id); + if (key != null) { + JcePublicKeyKeyEncryptionMethodGenerator pubKeyEncryptionGenerator = + new JcePublicKeyKeyEncryptionMethodGenerator(key); + cPk.addMethod(pubKeyEncryptionGenerator); + } + } + } + } + + /* Initialize signature generator object for later usage */ + PGPSignatureGenerator signatureGenerator = null; + PGPV3SignatureGenerator signatureV3Generator = null; + if (enableSignature) { + updateProgress(R.string.progress_preparing_signature, 10, 100); + + // content signer based on signing key algorithm and chosen hash algorithm + JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder( + signingKey.getPublicKey().getAlgorithm(), mSignatureHashAlgorithm) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + + if (mSignatureForceV3) { + signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder); + signatureV3Generator.init(signatureType, signaturePrivateKey); + } else { + signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); + signatureGenerator.init(signatureType, signaturePrivateKey); + + String userId = PgpKeyHelper.getMainUserId(signingKeyRing.getSecretKey()); + PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); + spGen.setSignerUserID(false, userId); + signatureGenerator.setHashedSubpackets(spGen.generate()); + } + } + + PGPCompressedDataGenerator compressGen = null; + OutputStream pOut; + OutputStream encryptionOut = null; + BCPGOutputStream bcpgOut; + if (enableEncryption) { + /* actual encryption */ + + encryptionOut = cPk.open(out, new byte[1 << 16]); + + if (enableCompression) { + compressGen = new PGPCompressedDataGenerator(mCompressionId); + bcpgOut = new BCPGOutputStream(compressGen.open(encryptionOut)); + } else { + bcpgOut = new BCPGOutputStream(encryptionOut); + } + + if (enableSignature) { + if (mSignatureForceV3) { + signatureV3Generator.generateOnePassVersion(false).encode(bcpgOut); + } else { + signatureGenerator.generateOnePassVersion(false).encode(bcpgOut); + } + } + + PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator(); + // file name not needed, so empty string + pOut = literalGen.open(bcpgOut, PGPLiteralData.BINARY, "", new Date(), + new byte[1 << 16]); + updateProgress(R.string.progress_encrypting, 20, 100); + + long progress = 0; + int n; + byte[] buffer = new byte[1 << 16]; + InputStream in = mData.getInputStream(); + while ((n = in.read(buffer)) > 0) { + pOut.write(buffer, 0, n); + + // update signature buffer if signature is requested + if (enableSignature) { + if (mSignatureForceV3) { + signatureV3Generator.update(buffer, 0, n); + } else { + signatureGenerator.update(buffer, 0, n); + } + } + + progress += n; + if (mData.getSize() != 0) { + updateProgress((int) (20 + (95 - 20) * progress / mData.getSize()), 100); + } + } + + literalGen.close(); + } else if (mEnableAsciiArmorOutput && enableSignature && !enableEncryption && !enableCompression) { + /* sign-only of ascii text */ + + updateProgress(R.string.progress_signing, 40, 100); + + // write directly on armor output stream + armorOut.beginClearText(mSignatureHashAlgorithm); + + InputStream in = mData.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + + final byte[] newline = "\r\n".getBytes("UTF-8"); + + if (mSignatureForceV3) { + processLine(reader.readLine(), armorOut, signatureV3Generator); + } else { + processLine(reader.readLine(), armorOut, signatureGenerator); + } + + while (true) { + String line = reader.readLine(); + + if (line == null) { + armorOut.write(newline); + break; + } + + armorOut.write(newline); + + // update signature buffer with input line + if (mSignatureForceV3) { + signatureV3Generator.update(newline); + processLine(line, armorOut, signatureV3Generator); + } else { + signatureGenerator.update(newline); + processLine(line, armorOut, signatureGenerator); + } + } + + armorOut.endClearText(); + + pOut = new BCPGOutputStream(armorOut); + } else { + // TODO: implement sign-only for files! + pOut = null; + Log.e(Constants.TAG, "not supported!"); + } + + if (enableSignature) { + updateProgress(R.string.progress_generating_signature, 95, 100); + if (mSignatureForceV3) { + signatureV3Generator.generate().encode(pOut); + } else { + signatureGenerator.generate().encode(pOut); + } + } + + // closing outputs + // NOTE: closing needs to be done in the correct order! + // TODO: closing bcpgOut and pOut??? + if (enableEncryption) { + if (enableCompression) { + compressGen.close(); + } + + encryptionOut.close(); + } + if (mEnableAsciiArmorOutput) { + armorOut.close(); + } + + out.close(); + mOutStream.close(); + + updateProgress(R.string.progress_done, 100, 100); + } + + // TODO: merge this into execute method! + // TODO: allow binary input for this class + public void generateSignature() + throws PgpGeneralException, PGPException, IOException, NoSuchAlgorithmException, + SignatureException { + + OutputStream out; + if (mEnableAsciiArmorOutput) { + // Ascii Armor (Radix-64) + ArmoredOutputStream armorOut = new ArmoredOutputStream(mOutStream); + armorOut.setHeader("Version", PgpHelper.getFullVersion(mContext)); + out = armorOut; + } else { + out = mOutStream; + } + + if (mSignatureKeyId == 0) { + throw new PgpGeneralException(mContext.getString(R.string.error_no_signature_key)); + } + + PGPSecretKeyRing signingKeyRing = + ProviderHelper.getPGPSecretKeyRingWithKeyId(mContext, mSignatureKeyId); + PGPSecretKey signingKey = PgpKeyHelper.getSigningKey(mContext, mSignatureKeyId); + if (signingKey == null) { + throw new PgpGeneralException(mContext.getString(R.string.error_signature_failed)); + } + + if (mSignaturePassphrase == null) { + throw new PgpGeneralException(mContext.getString(R.string.error_no_signature_passphrase)); + } + + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(mSignaturePassphrase.toCharArray()); + PGPPrivateKey signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor); + if (signaturePrivateKey == null) { + throw new PgpGeneralException( + mContext.getString(R.string.error_could_not_extract_private_key)); + } + updateProgress(R.string.progress_preparing_streams, 0, 100); + + updateProgress(R.string.progress_preparing_signature, 30, 100); + + int type = PGPSignature.CANONICAL_TEXT_DOCUMENT; +// if (binary) { +// type = PGPSignature.BINARY_DOCUMENT; +// } + + // content signer based on signing key algorithm and chosen hash algorithm + JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(signingKey + .getPublicKey().getAlgorithm(), mSignatureHashAlgorithm) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + + PGPSignatureGenerator signatureGenerator = null; + PGPV3SignatureGenerator signatureV3Generator = null; + if (mSignatureForceV3) { + signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder); + signatureV3Generator.init(type, signaturePrivateKey); + } else { + signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); + signatureGenerator.init(type, signaturePrivateKey); + + PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); + String userId = PgpKeyHelper.getMainUserId(signingKeyRing.getSecretKey()); + spGen.setSignerUserID(false, userId); + signatureGenerator.setHashedSubpackets(spGen.generate()); + } + + updateProgress(R.string.progress_signing, 40, 100); + + InputStream inStream = mData.getInputStream(); +// if (binary) { +// byte[] buffer = new byte[1 << 16]; +// int n = 0; +// while ((n = inStream.read(buffer)) > 0) { +// if (signatureForceV3) { +// 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"); + + String line; + while ((line = reader.readLine()) != null) { + if (mSignatureForceV3) { + processLine(line, null, signatureV3Generator); + signatureV3Generator.update(newline); + } else { + processLine(line, null, signatureGenerator); + signatureGenerator.update(newline); + } + } +// } + + BCPGOutputStream bOut = new BCPGOutputStream(out); + if (mSignatureForceV3) { + signatureV3Generator.generate().encode(bOut); + } else { + signatureGenerator.generate().encode(bOut); + } + out.close(); + mOutStream.close(); + + updateProgress(R.string.progress_done, 100, 100); + } + + + 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); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpToX509.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpToX509.java new file mode 100644 index 000000000..5bb1665b6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpToX509.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp; + +import org.spongycastle.asn1.DERObjectIdentifier; +import org.spongycastle.asn1.x509.AuthorityKeyIdentifier; +import org.spongycastle.asn1.x509.BasicConstraints; +import org.spongycastle.asn1.x509.GeneralName; +import org.spongycastle.asn1.x509.GeneralNames; +import org.spongycastle.asn1.x509.SubjectKeyIdentifier; +import org.spongycastle.asn1.x509.X509Extensions; +import org.spongycastle.asn1.x509.X509Name; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPPrivateKey; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.x509.X509V3CertificateGenerator; +import org.spongycastle.x509.extension.AuthorityKeyIdentifierStructure; +import org.spongycastle.x509.extension.SubjectKeyIdentifierStructure; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.util.Date; +import java.util.Iterator; +import java.util.Vector; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +public class PgpToX509 { + public static final String DN_COMMON_PART_O = "OpenPGP to X.509 Bridge"; + public static final String DN_COMMON_PART_OU = "OpenPGP Keychain cert"; + + /** + * Creates a self-signed certificate from a public and private key. The (critical) key-usage + * extension is set up with: digital signature, non-repudiation, key-encipherment, key-agreement + * and certificate-signing. The (non-critical) Netscape extension is set up with: SSL client and + * S/MIME. A URI subjectAltName may also be set up. + * + * @param pubKey public key + * @param privKey private key + * @param subject subject (and issuer) DN for this certificate, RFC 2253 format preferred. + * @param startDate date from which the certificate will be valid (defaults to current date and time + * if null) + * @param endDate date until which the certificate will be valid (defaults to current date and time + * if null) * + * @param subjAltNameURI URI to be placed in subjectAltName + * @return self-signed certificate + * @throws InvalidKeyException + * @throws SignatureException + * @throws NoSuchAlgorithmException + * @throws IllegalStateException + * @throws NoSuchProviderException + * @throws CertificateException + * @throws Exception + * @author Bruno Harbulot + */ + public static X509Certificate createSelfSignedCert( + PublicKey pubKey, PrivateKey privKey, X509Name subject, Date startDate, Date endDate, + String subjAltNameURI) + throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, + SignatureException, CertificateException, NoSuchProviderException { + + X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator(); + + certGenerator.reset(); + /* + * Sets up the subject distinguished name. Since it's a self-signed certificate, issuer and + * subject are the same. + */ + certGenerator.setIssuerDN(subject); + certGenerator.setSubjectDN(subject); + + /* + * Sets up the validity dates. + */ + if (startDate == null) { + startDate = new Date(System.currentTimeMillis()); + } + certGenerator.setNotBefore(startDate); + if (endDate == null) { + endDate = new Date(startDate.getTime() + (365L * 24L * 60L * 60L * 1000L)); + Log.d(Constants.TAG, "end date is=" + DateFormat.getDateInstance().format(endDate)); + } + + certGenerator.setNotAfter(endDate); + + /* + * The serial-number of this certificate is 1. It makes sense because it's self-signed. + */ + certGenerator.setSerialNumber(BigInteger.ONE); + /* + * Sets the public-key to embed in this certificate. + */ + certGenerator.setPublicKey(pubKey); + /* + * Sets the signature algorithm. + */ + String pubKeyAlgorithm = pubKey.getAlgorithm(); + if (pubKeyAlgorithm.equals("DSA")) { + certGenerator.setSignatureAlgorithm("SHA1WithDSA"); + } else if (pubKeyAlgorithm.equals("RSA")) { + certGenerator.setSignatureAlgorithm("SHA1WithRSAEncryption"); + } else { + RuntimeException re = new RuntimeException("Algorithm not recognised: " + + pubKeyAlgorithm); + Log.e(Constants.TAG, re.getMessage(), re); + throw re; + } + + /* + * Adds the Basic Constraint (CA: true) extension. + */ + certGenerator.addExtension(X509Extensions.BasicConstraints, true, + new BasicConstraints(true)); + + /* + * Adds the subject key identifier extension. + */ + SubjectKeyIdentifier subjectKeyIdentifier = new SubjectKeyIdentifierStructure(pubKey); + certGenerator + .addExtension(X509Extensions.SubjectKeyIdentifier, false, subjectKeyIdentifier); + + /* + * Adds the authority key identifier extension. + */ + AuthorityKeyIdentifier authorityKeyIdentifier = new AuthorityKeyIdentifierStructure(pubKey); + certGenerator.addExtension(X509Extensions.AuthorityKeyIdentifier, false, + authorityKeyIdentifier); + + /* + * Adds the subject alternative-name extension. + */ + if (subjAltNameURI != null) { + GeneralNames subjectAltNames = new GeneralNames(new GeneralName( + GeneralName.uniformResourceIdentifier, subjAltNameURI)); + certGenerator.addExtension(X509Extensions.SubjectAlternativeName, false, + subjectAltNames); + } + + /* + * Creates and sign this certificate with the private key corresponding to the public key of + * the certificate (hence the name "self-signed certificate"). + */ + X509Certificate cert = certGenerator.generate(privKey); + + /* + * Checks that this certificate has indeed been correctly signed. + */ + cert.verify(pubKey); + + return cert; + } + + /** + * Creates a self-signed certificate from a PGP Secret Key. + * + * @param pgpSecKey PGP Secret Key (from which one can extract the public and private + * keys and other attributes). + * @param pgpPrivKey PGP Private Key corresponding to the Secret Key (password callbacks + * should be done before calling this method) + * @param subjAltNameURI optional URI to embed in the subject alternative-name + * @return self-signed certificate + * @throws PGPException + * @throws NoSuchProviderException + * @throws InvalidKeyException + * @throws NoSuchAlgorithmException + * @throws SignatureException + * @throws CertificateException + * @author Bruno Harbulot + */ + public static X509Certificate createSelfSignedCert( + PGPSecretKey pgpSecKey, PGPPrivateKey pgpPrivKey, String subjAltNameURI) + throws PGPException, NoSuchProviderException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException, CertificateException { + // get public key from secret key + PGPPublicKey pgpPubKey = pgpSecKey.getPublicKey(); + + // LOGGER.info("Key ID: " + Long.toHexString(pgpPubKey.getKeyID() & 0xffffffffL)); + + /* + * The X.509 Name to be the subject DN is prepared. The CN is extracted from the Secret Key + * user ID. + */ + Vector x509NameOids = new Vector(); + Vector x509NameValues = new Vector(); + + x509NameOids.add(X509Name.O); + x509NameValues.add(DN_COMMON_PART_O); + + x509NameOids.add(X509Name.OU); + x509NameValues.add(DN_COMMON_PART_OU); + + for (@SuppressWarnings("unchecked") + Iterator it = (Iterator) pgpSecKey.getUserIDs(); it.hasNext(); ) { + Object attrib = it.next(); + x509NameOids.add(X509Name.CN); + x509NameValues.add("CryptoCall"); + // x509NameValues.add(attrib.toString()); + } + + /* + * Currently unused. + */ + Log.d(Constants.TAG, "User attributes: "); + for (@SuppressWarnings("unchecked") + Iterator it = (Iterator) pgpSecKey.getUserAttributes(); it.hasNext(); ) { + Object attrib = it.next(); + Log.d(Constants.TAG, " - " + attrib + " -- " + attrib.getClass()); + } + + X509Name x509name = new X509Name(x509NameOids, x509NameValues); + + Log.d(Constants.TAG, "Subject DN: " + x509name); + + /* + * To check the signature from the certificate on the recipient side, the creation time + * needs to be embedded in the certificate. It seems natural to make this creation time be + * the "not-before" date of the X.509 certificate. Unlimited PGP keys have a validity of 0 + * second. In this case, the "not-after" date will be the same as the not-before date. This + * is something that needs to be checked by the service receiving this certificate. + */ + Date creationTime = pgpPubKey.getCreationTime(); + Log.d(Constants.TAG, + "pgp pub key creation time=" + DateFormat.getDateInstance().format(creationTime)); + Log.d(Constants.TAG, "pgp valid seconds=" + pgpPubKey.getValidSeconds()); + Date validTo = null; + if (pgpPubKey.getValidSeconds() > 0) { + validTo = new Date(creationTime.getTime() + 1000L * pgpPubKey.getValidSeconds()); + } + + X509Certificate selfSignedCert = createSelfSignedCert( + pgpPubKey.getKey(Constants.BOUNCY_CASTLE_PROVIDER_NAME), pgpPrivKey.getKey(), + x509name, creationTime, validTo, subjAltNameURI); + + return selfSignedCert; + } + + /** + * This is a password callback handler that will fill in a password automatically. Useful to + * configure passwords in advance, but should be used with caution depending on how much you + * allow passwords to be stored within your application. + * + * @author Bruno Harbulot. + */ + public static final class PredefinedPasswordCallbackHandler implements CallbackHandler { + + private char[] mPassword; + private String mPrompt; + + public PredefinedPasswordCallbackHandler(String password) { + this(password == null ? null : password.toCharArray(), null); + } + + public PredefinedPasswordCallbackHandler(char[] password) { + this(password, null); + } + + public PredefinedPasswordCallbackHandler(String password, String prompt) { + this(password == null ? null : password.toCharArray(), prompt); + } + + public PredefinedPasswordCallbackHandler(char[] password, String prompt) { + this.mPassword = password; + this.mPrompt = prompt; + } + + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof PasswordCallback) { + PasswordCallback pwCallback = (PasswordCallback) callback; + if ((this.mPrompt == null) || (this.mPrompt.equals(pwCallback.getPrompt()))) { + pwCallback.setPassword(this.mPassword); + } + } else { + throw new UnsupportedCallbackException(callback, "Unrecognised callback."); + } + } + } + + protected final Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException(); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/NoAsymmetricEncryptionException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/NoAsymmetricEncryptionException.java new file mode 100644 index 000000000..23c4bbbd9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/NoAsymmetricEncryptionException.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp.exception; + +public class NoAsymmetricEncryptionException extends Exception { + static final long serialVersionUID = 0xf812773343L; + + public NoAsymmetricEncryptionException() { + super(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralException.java new file mode 100644 index 000000000..418445367 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp.exception; + +public class PgpGeneralException extends Exception { + static final long serialVersionUID = 0xf812773342L; + + public PgpGeneralException(String message) { + super(message); + } + public PgpGeneralException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralMsgIdException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralMsgIdException.java new file mode 100644 index 000000000..caa7842db --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/exception/PgpGeneralMsgIdException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.pgp.exception; + +import android.content.Context; + +public class PgpGeneralMsgIdException extends Exception { + static final long serialVersionUID = 0xf812773343L; + + private final int mMessageId; + + public PgpGeneralMsgIdException(int messageId) { + super("msg[" + messageId + "]"); + mMessageId = messageId; + } + + public PgpGeneralException getContextualized(Context context) { + return new PgpGeneralException(context.getString(mMessageId), this); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java new file mode 100644 index 000000000..fc25faecd --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.provider; + +import android.net.Uri; +import android.provider.BaseColumns; + +import org.sufficientlysecure.keychain.Constants; + +public class KeychainContract { + + interface KeyRingsColumns { + String MASTER_KEY_ID = "master_key_id"; // not a database id + String KEY_RING_DATA = "key_ring_data"; // PGPPublicKeyRing / PGPSecretKeyRing blob + } + + interface KeysColumns { + String MASTER_KEY_ID = "master_key_id"; // not a database id + String RANK = "rank"; + + String KEY_ID = "key_id"; // not a database id + String ALGORITHM = "algorithm"; + String FINGERPRINT = "fingerprint"; + + String KEY_SIZE = "key_size"; + String CAN_SIGN = "can_sign"; + String CAN_ENCRYPT = "can_encrypt"; + String CAN_CERTIFY = "can_certify"; + String IS_REVOKED = "is_revoked"; + + String CREATION = "creation"; + String EXPIRY = "expiry"; + } + + interface UserIdsColumns { + String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID + String USER_ID = "user_id"; // not a database id + String RANK = "rank"; // ONLY used for sorting! no key, no nothing! + String IS_PRIMARY = "is_primary"; + String IS_REVOKED = "is_revoked"; + } + + interface CertsColumns { + String MASTER_KEY_ID = "master_key_id"; + String RANK = "rank"; + String KEY_ID_CERTIFIER = "key_id_certifier"; + String TYPE = "type"; + String VERIFIED = "verified"; + String CREATION = "creation"; + String DATA = "data"; + } + + interface ApiAppsColumns { + String PACKAGE_NAME = "package_name"; + String PACKAGE_SIGNATURE = "package_signature"; + } + + interface ApiAppsAccountsColumns { + String ACCOUNT_NAME = "account_name"; + String KEY_ID = "key_id"; // not a database id + String ENCRYPTION_ALGORITHM = "encryption_algorithm"; + String HASH_ALORITHM = "hash_algorithm"; + String COMPRESSION = "compression"; + String PACKAGE_NAME = "package_name"; // foreign key to api_apps.package_name + } + + public static final class KeyTypes { + public static final int PUBLIC = 0; + public static final int SECRET = 1; + } + + public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".provider"; + + private static final Uri BASE_CONTENT_URI_INTERNAL = Uri + .parse("content://" + CONTENT_AUTHORITY); + + public static final String BASE_KEY_RINGS = "key_rings"; + public static final String BASE_DATA = "data"; + + public static final String PATH_UNIFIED = "unified"; + + public static final String PATH_FIND = "find"; + public static final String PATH_BY_EMAIL = "email"; + public static final String PATH_BY_SUBKEY = "subkey"; + + public static final String PATH_PUBLIC = "public"; + public static final String PATH_SECRET = "secret"; + public static final String PATH_USER_IDS = "user_ids"; + public static final String PATH_KEYS = "keys"; + public static final String PATH_CERTS = "certs"; + + public static final String BASE_API_APPS = "api_apps"; + public static final String PATH_ACCOUNTS = "accounts"; + + public static class KeyRings implements BaseColumns, KeysColumns, UserIdsColumns { + public static final String MASTER_KEY_ID = KeysColumns.MASTER_KEY_ID; + public static final String IS_REVOKED = KeysColumns.IS_REVOKED; + public static final String VERIFIED = CertsColumns.VERIFIED; + public static final String HAS_SECRET = "has_secret"; + + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_KEY_RINGS).build(); + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.key_ring"; + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.key_ring"; + + public static Uri buildUnifiedKeyRingsUri() { + return CONTENT_URI.buildUpon().appendPath(PATH_UNIFIED).build(); + } + + public static Uri buildGenericKeyRingUri(String masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(masterKeyId).build(); + } + public static Uri buildUnifiedKeyRingUri(String masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_UNIFIED).build(); + } + public static Uri buildUnifiedKeyRingUri(Uri uri) { + return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_UNIFIED).build(); + } + + public static Uri buildUnifiedKeyRingsFindByEmailUri(String email) { + return CONTENT_URI.buildUpon().appendPath(PATH_FIND).appendPath(PATH_BY_EMAIL).appendPath(email).build(); + } + public static Uri buildUnifiedKeyRingsFindBySubkeyUri(String subkey) { + return CONTENT_URI.buildUpon().appendPath(PATH_FIND).appendPath(PATH_BY_SUBKEY).appendPath(subkey).build(); + } + + } + + public static class KeyRingData implements KeyRingsColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_KEY_RINGS).build(); + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.key_ring_data"; + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.key_ring_data"; + + public static Uri buildPublicKeyRingUri() { + return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).build(); + } + public static Uri buildPublicKeyRingUri(String masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_PUBLIC).build(); + } + public static Uri buildPublicKeyRingUri(Uri uri) { + return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_PUBLIC).build(); + } + + public static Uri buildSecretKeyRingUri() { + return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).build(); + } + public static Uri buildSecretKeyRingUri(String masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_SECRET).build(); + } + public static Uri buildSecretKeyRingUri(Uri uri) { + return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_SECRET).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.sufficientlysecure.openkeychain.key"; + + /** + * Use if a single item is returned + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.key"; + + public static Uri buildKeysUri(String masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_KEYS).build(); + } + public static Uri buildKeysUri(Uri uri) { + return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_KEYS).build(); + } + + } + + public static class UserIds implements UserIdsColumns, BaseColumns { + public static final String VERIFIED = "verified"; + 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.sufficientlysecure.openkeychain.user_id"; + + /** + * Use if a single item is returned + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.user_id"; + + public static Uri buildUserIdsUri(String masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_USER_IDS).build(); + } + public static Uri buildUserIdsUri(Uri uri) { + return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build(); + } + } + + public static class ApiApps implements ApiAppsColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_API_APPS).build(); + + /** + * Use if multiple items get returned + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.api_apps"; + + /** + * Use if a single item is returned + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.api_app"; + + public static Uri buildByPackageNameUri(String packageName) { + return CONTENT_URI.buildUpon().appendEncodedPath(packageName).build(); + } + } + + public static class ApiAccounts implements ApiAppsAccountsColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_API_APPS).build(); + + /** + * Use if multiple items get returned + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.sufficientlysecure.openkeychain.api_app.accounts"; + + /** + * Use if a single item is returned + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.sufficientlysecure.openkeychain.api_app.account"; + + public static Uri buildBaseUri(String packageName) { + return CONTENT_URI.buildUpon().appendEncodedPath(packageName).appendPath(PATH_ACCOUNTS) + .build(); + } + + public static Uri buildByPackageAndAccountUri(String packageName, String accountName) { + return CONTENT_URI.buildUpon().appendEncodedPath(packageName).appendPath(PATH_ACCOUNTS) + .appendEncodedPath(accountName).build(); + } + } + + public static class Certs implements CertsColumns, BaseColumns { + public static final String USER_ID = UserIdsColumns.USER_ID; + public static final String SIGNER_UID = "signer_user_id"; + + public static final int VERIFIED_SECRET = 1; + public static final int VERIFIED_SELF = 2; + + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_KEY_RINGS).build(); + + public static Uri buildCertsUri(String masterKeyId) { + return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_CERTS).build(); + } + public static Uri buildCertsSpecificUri(String masterKeyId, String rank, String certifier) { + return CONTENT_URI.buildUpon().appendPath(masterKeyId).appendPath(PATH_CERTS).appendPath(rank).appendPath(certifier).build(); + } + public static Uri buildCertsUri(Uri uri) { + return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_CERTS).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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java new file mode 100644 index 000000000..8674ad94b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.provider; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; + +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.pgp.PgpConversionHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsAccountsColumns; +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.provider.KeychainContract.CertsColumns; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.IOException; + +public class KeychainDatabase extends SQLiteOpenHelper { + private static final String DATABASE_NAME = "openkeychain.db"; + private static final int DATABASE_VERSION = 1; + static Boolean apgHack = false; + + public interface Tables { + String KEY_RINGS_PUBLIC = "keyrings_public"; + String KEY_RINGS_SECRET = "keyrings_secret"; + String KEYS = "keys"; + String USER_IDS = "user_ids"; + String CERTS = "certs"; + String API_APPS = "api_apps"; + String API_ACCOUNTS = "api_accounts"; + } + + private static final String CREATE_KEYRINGS_PUBLIC = + "CREATE TABLE IF NOT EXISTS keyrings_public (" + + KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY," + + KeyRingsColumns.KEY_RING_DATA + " BLOB" + + ")"; + + private static final String CREATE_KEYRINGS_SECRET = + "CREATE TABLE IF NOT EXISTS keyrings_secret (" + + KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY," + + KeyRingsColumns.KEY_RING_DATA + " BLOB," + + "FOREIGN KEY(" + KeyRingsColumns.MASTER_KEY_ID + ") " + + "REFERENCES keyrings_public(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" + + ")"; + + private static final String CREATE_KEYS = + "CREATE TABLE IF NOT EXISTS " + Tables.KEYS + " (" + + KeysColumns.MASTER_KEY_ID + " INTEGER, " + + KeysColumns.RANK + " INTEGER, " + + + KeysColumns.KEY_ID + " INTEGER, " + + KeysColumns.KEY_SIZE + " INTEGER, " + + KeysColumns.ALGORITHM + " INTEGER, " + + KeysColumns.FINGERPRINT + " BLOB, " + + + KeysColumns.CAN_CERTIFY + " BOOLEAN, " + + KeysColumns.CAN_SIGN + " BOOLEAN, " + + KeysColumns.CAN_ENCRYPT + " BOOLEAN, " + + KeysColumns.IS_REVOKED + " BOOLEAN, " + + + KeysColumns.CREATION + " INTEGER, " + + KeysColumns.EXPIRY + " INTEGER, " + + + "PRIMARY KEY(" + KeysColumns.MASTER_KEY_ID + ", " + KeysColumns.RANK + ")," + + "FOREIGN KEY(" + KeysColumns.MASTER_KEY_ID + ") REFERENCES " + + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" + + ")"; + + private static final String CREATE_USER_IDS = + "CREATE TABLE IF NOT EXISTS " + Tables.USER_IDS + "(" + + UserIdsColumns.MASTER_KEY_ID + " INTEGER, " + + UserIdsColumns.USER_ID + " CHARMANDER, " + + + UserIdsColumns.IS_PRIMARY + " BOOLEAN, " + + UserIdsColumns.IS_REVOKED + " BOOLEAN, " + + UserIdsColumns.RANK+ " INTEGER, " + + + "PRIMARY KEY(" + UserIdsColumns.MASTER_KEY_ID + ", " + UserIdsColumns.USER_ID + "), " + + "UNIQUE (" + UserIdsColumns.MASTER_KEY_ID + ", " + UserIdsColumns.RANK + "), " + + "FOREIGN KEY(" + UserIdsColumns.MASTER_KEY_ID + ") REFERENCES " + + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" + + ")"; + + private static final String CREATE_CERTS = + "CREATE TABLE IF NOT EXISTS " + Tables.CERTS + "(" + + CertsColumns.MASTER_KEY_ID + " INTEGER," + + CertsColumns.RANK + " INTEGER, " // rank of certified uid + + + CertsColumns.KEY_ID_CERTIFIER + " INTEGER, " // certifying key + + CertsColumns.TYPE + " INTEGER, " + + CertsColumns.VERIFIED + " INTEGER, " + + CertsColumns.CREATION + " INTEGER, " + + + CertsColumns.DATA + " BLOB, " + + + "PRIMARY KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ", " + + CertsColumns.KEY_ID_CERTIFIER + "), " + + "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ") REFERENCES " + + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE," + + "FOREIGN KEY(" + CertsColumns.MASTER_KEY_ID + ", " + CertsColumns.RANK + ") REFERENCES " + + Tables.USER_IDS + "(" + UserIdsColumns.MASTER_KEY_ID + ", " + UserIdsColumns.RANK + ") ON DELETE CASCADE" + + ")"; + + private static final String CREATE_API_APPS = "CREATE TABLE IF NOT EXISTS " + Tables.API_APPS + + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + ApiAppsColumns.PACKAGE_NAME + " TEXT NOT NULL UNIQUE, " + + ApiAppsColumns.PACKAGE_SIGNATURE + " BLOB)"; + + private static final String CREATE_API_APPS_ACCOUNTS = "CREATE TABLE IF NOT EXISTS " + Tables.API_ACCOUNTS + + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + ApiAppsAccountsColumns.ACCOUNT_NAME + " TEXT NOT NULL, " + + ApiAppsAccountsColumns.KEY_ID + " INT64, " + + ApiAppsAccountsColumns.ENCRYPTION_ALGORITHM + " INTEGER, " + + ApiAppsAccountsColumns.HASH_ALORITHM + " INTEGER, " + + ApiAppsAccountsColumns.COMPRESSION + " INTEGER, " + + ApiAppsAccountsColumns.PACKAGE_NAME + " TEXT NOT NULL, " + + "UNIQUE(" + ApiAppsAccountsColumns.ACCOUNT_NAME + ", " + + ApiAppsAccountsColumns.PACKAGE_NAME + "), " + + "FOREIGN KEY(" + ApiAppsAccountsColumns.PACKAGE_NAME + ") REFERENCES " + + Tables.API_APPS + "(" + ApiAppsColumns.PACKAGE_NAME + ") ON DELETE CASCADE)"; + + KeychainDatabase(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + + // make sure this is only done once, on the first instance! + boolean iAmIt = false; + synchronized(apgHack) { + if(!apgHack) { + iAmIt = true; + apgHack = true; + } + } + // if it's us, do the import + if(iAmIt) + checkAndImportApg(context); + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.w(Constants.TAG, "Creating database..."); + + db.execSQL(CREATE_KEYRINGS_PUBLIC); + db.execSQL(CREATE_KEYRINGS_SECRET); + db.execSQL(CREATE_KEYS); + db.execSQL(CREATE_USER_IDS); + db.execSQL(CREATE_CERTS); + db.execSQL(CREATE_API_APPS); + db.execSQL(CREATE_API_APPS_ACCOUNTS); + } + + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + if (!db.isReadOnly()) { + // Enable foreign key constraints + db.execSQL("PRAGMA foreign_keys=ON;"); + // TODO remove, once we remove the "always migrate" debug stuff + // db.execSQL("DROP TABLE certs;"); + // db.execSQL("DROP TABLE user_ids;"); + db.execSQL(CREATE_USER_IDS); + db.execSQL(CREATE_CERTS); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int old, int nu) { + // don't care (this is version 1) + } + + /** This method tries to import data from a provided database. + * + * The sole assumptions made on this db are that there is a key_rings table + * with a key_ring_data, a master_key_id and a type column, the latter of + * which should be 1 for secret keys and 0 for public keys. + */ + public void checkAndImportApg(Context context) { + + boolean hasApgDb = false; { + // It's the Java way =( + String[] dbs = context.databaseList(); + for(String db : dbs) { + if(db.equals("apg.db")) { + hasApgDb = true; + break; + } + } + } + + if(!hasApgDb) + return; + + Log.d(Constants.TAG, "apg.db exists! Importing..."); + + SQLiteDatabase db = new SQLiteOpenHelper(context, "apg.db", null, 1) { + @Override + public void onCreate(SQLiteDatabase db) { + // should never happen + assert false; + } + @Override + public void onDowngrade(SQLiteDatabase db, int old, int nu) { + // don't care + } + @Override + public void onUpgrade(SQLiteDatabase db, int old, int nu) { + // don't care either + } + }.getReadableDatabase(); + + // kill current! + { // TODO don't kill current. + Log.d(Constants.TAG, "Truncating db..."); + SQLiteDatabase d = getWritableDatabase(); + d.execSQL("DELETE FROM keyrings_public"); + d.close(); + Log.d(Constants.TAG, "Ok."); + } + + Cursor c = null; + try { + // we insert in two steps: first, all public keys that have secret keys + c = db.rawQuery("SELECT key_ring_data FROM key_rings WHERE type = 1 OR EXISTS (" + + " SELECT 1 FROM key_rings d2 WHERE key_rings.master_key_id = d2.master_key_id" + + " AND d2.type = 1) ORDER BY type ASC", null); + Log.d(Constants.TAG, "Importing " + c.getCount() + " secret keyrings from apg.db..."); + for(int i = 0; i < c.getCount(); i++) { + c.moveToPosition(i); + byte[] data = c.getBlob(0); + PGPKeyRing ring = PgpConversionHelper.BytesToPGPKeyRing(data); + if(ring instanceof PGPPublicKeyRing) + ProviderHelper.saveKeyRing(context, (PGPPublicKeyRing) ring); + else if(ring instanceof PGPSecretKeyRing) + ProviderHelper.saveKeyRing(context, (PGPSecretKeyRing) ring); + else { + Log.e(Constants.TAG, "Unknown blob data type!"); + } + } + + // afterwards, insert all keys, starting with public keys that have secret keys, then + // secret keys, then all others. this order is necessary to ensure all certifications + // are recognized properly. + c = db.rawQuery("SELECT key_ring_data FROM key_rings ORDER BY (type = 0 AND EXISTS (" + + " SELECT 1 FROM key_rings d2 WHERE key_rings.master_key_id = d2.master_key_id AND" + + " d2.type = 1)) DESC, type DESC", null); + // import from old database + Log.d(Constants.TAG, "Importing " + c.getCount() + " keyrings from apg.db..."); + for(int i = 0; i < c.getCount(); i++) { + c.moveToPosition(i); + byte[] data = c.getBlob(0); + PGPKeyRing ring = PgpConversionHelper.BytesToPGPKeyRing(data); + if(ring instanceof PGPPublicKeyRing) + ProviderHelper.saveKeyRing(context, (PGPPublicKeyRing) ring); + else if(ring instanceof PGPSecretKeyRing) + ProviderHelper.saveKeyRing(context, (PGPSecretKeyRing) ring); + else { + Log.e(Constants.TAG, "Unknown blob data type!"); + } + } + + } catch(IOException e) { + Log.e(Constants.TAG, "Error importing apg db!", e); + return; + } finally { + if(c != null) + c.close(); + if(db != null) + db.close(); + } + + // TODO delete old db, if we are sure this works + // context.deleteDatabase("apg.db"); + Log.d(Constants.TAG, "All done, (not) deleting apg.db"); + + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java new file mode 100644 index 000000000..9b9e4991d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -0,0 +1,752 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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.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.text.TextUtils; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.Arrays; +import java.util.HashMap; + +public class KeychainProvider extends ContentProvider { + + private static final int KEY_RINGS_UNIFIED = 101; + private static final int KEY_RINGS_PUBLIC = 102; + private static final int KEY_RINGS_SECRET = 103; + + private static final int KEY_RING_UNIFIED = 200; + private static final int KEY_RING_KEYS = 201; + private static final int KEY_RING_USER_IDS = 202; + private static final int KEY_RING_PUBLIC = 203; + private static final int KEY_RING_SECRET = 204; + private static final int KEY_RING_CERTS = 205; + private static final int KEY_RING_CERTS_SPECIFIC = 206; + + private static final int API_APPS = 301; + private static final int API_APPS_BY_PACKAGE_NAME = 303; + private static final int API_ACCOUNTS = 304; + private static final int API_ACCOUNTS_BY_ACCOUNT_NAME = 306; + + private static final int KEY_RINGS_FIND_BY_EMAIL = 400; + private static final int KEY_RINGS_FIND_BY_SUBKEY = 401; + + // private static final int DATA_STREAM = 501; + + protected UriMatcher mUriMatcher; + + /** + * Build and return a {@link UriMatcher} that catches all {@link Uri} variations supported by + * this {@link ContentProvider}. + */ + protected UriMatcher buildUriMatcher() { + final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); + + String authority = KeychainContract.CONTENT_AUTHORITY; + + /** + * list key_rings + * + *
+         * key_rings/unified
+         * key_rings/public
+         * 
+ */ + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + + "/" + KeychainContract.PATH_UNIFIED, + KEY_RINGS_UNIFIED); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + + "/" + KeychainContract.PATH_PUBLIC, + KEY_RINGS_PUBLIC); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + + "/" + KeychainContract.PATH_SECRET, + KEY_RINGS_SECRET); + + /** + * find by criteria other than master key id + * + * key_rings/find/email/_ + * key_rings/find/subkey/_ + * + */ + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + + KeychainContract.PATH_FIND + "/" + KeychainContract.PATH_BY_EMAIL + "/*", + KEY_RINGS_FIND_BY_EMAIL); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + + KeychainContract.PATH_FIND + "/" + KeychainContract.PATH_BY_SUBKEY + "/*", + KEY_RINGS_FIND_BY_SUBKEY); + + /** + * list key_ring specifics + * + *
+         * key_rings/_/unified
+         * key_rings/_/keys
+         * key_rings/_/user_ids
+         * key_rings/_/public
+         * key_rings/_/secret
+         * key_rings/_/certs
+         * key_rings/_/certs/_/_
+         * 
+ */ + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + + KeychainContract.PATH_UNIFIED, + KEY_RING_UNIFIED); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + + KeychainContract.PATH_KEYS, + KEY_RING_KEYS); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + + KeychainContract.PATH_USER_IDS, + KEY_RING_USER_IDS); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + + KeychainContract.PATH_PUBLIC, + KEY_RING_PUBLIC); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + + KeychainContract.PATH_SECRET, + KEY_RING_SECRET); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + + KeychainContract.PATH_CERTS, + KEY_RING_CERTS); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + + KeychainContract.PATH_CERTS + "/*/*", + KEY_RING_CERTS_SPECIFIC); + + /** + * API apps + * + *
+         * api_apps
+         * api_apps/_ (package name)
+         *
+         * api_apps/_/accounts
+         * api_apps/_/accounts/_ (account name)
+         * 
+ */ + matcher.addURI(authority, KeychainContract.BASE_API_APPS, API_APPS); + matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*", API_APPS_BY_PACKAGE_NAME); + + matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/" + + KeychainContract.PATH_ACCOUNTS, API_ACCOUNTS); + matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/" + + KeychainContract.PATH_ACCOUNTS + "/*", API_ACCOUNTS_BY_ACCOUNT_NAME); + + /** + * data stream + * + *
+         * data / _
+         * 
+ */ + // matcher.addURI(authority, KeychainContract.BASE_DATA + "/*", DATA_STREAM); + + return matcher; + } + + private KeychainDatabase mKeychainDatabase; + + /** + * {@inheritDoc} + */ + @Override + public boolean onCreate() { + mUriMatcher = buildUriMatcher(); + return true; + } + + public KeychainDatabase getDb() { + if(mKeychainDatabase == null) + mKeychainDatabase = new KeychainDatabase(getContext()); + return mKeychainDatabase; + } + + /** + * {@inheritDoc} + */ + @Override + public String getType(Uri uri) { + final int match = mUriMatcher.match(uri); + switch (match) { + case KEY_RING_PUBLIC: + return KeyRings.CONTENT_ITEM_TYPE; + + case KEY_RING_KEYS: + return Keys.CONTENT_TYPE; + + case KEY_RING_USER_IDS: + return UserIds.CONTENT_TYPE; + + case KEY_RING_SECRET: + return KeyRings.CONTENT_ITEM_TYPE; + + case API_APPS: + return ApiApps.CONTENT_TYPE; + + case API_APPS_BY_PACKAGE_NAME: + return ApiApps.CONTENT_ITEM_TYPE; + + case API_ACCOUNTS: + return ApiAccounts.CONTENT_TYPE; + + case API_ACCOUNTS_BY_ACCOUNT_NAME: + return ApiAccounts.CONTENT_ITEM_TYPE; + + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + + /** + * {@inheritDoc} + */ + @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(); + + int match = mUriMatcher.match(uri); + + // all query() parameters, for good measure + String groupBy = null, having = null; + + switch (match) { + case KEY_RING_UNIFIED: + case KEY_RINGS_UNIFIED: + case KEY_RINGS_FIND_BY_EMAIL: + case KEY_RINGS_FIND_BY_SUBKEY: { + HashMap projectionMap = new HashMap(); + projectionMap.put(KeyRings._ID, Tables.KEYS + ".oid AS _id"); + projectionMap.put(KeyRings.MASTER_KEY_ID, Tables.KEYS + "." + Keys.MASTER_KEY_ID); + projectionMap.put(KeyRings.KEY_ID, Keys.KEY_ID); + projectionMap.put(KeyRings.KEY_SIZE, Keys.KEY_SIZE); + projectionMap.put(KeyRings.IS_REVOKED, Tables.KEYS + "." + Keys.IS_REVOKED); + projectionMap.put(KeyRings.CAN_CERTIFY, Keys.CAN_CERTIFY); + projectionMap.put(KeyRings.CAN_ENCRYPT, Keys.CAN_ENCRYPT); + projectionMap.put(KeyRings.CAN_SIGN, Keys.CAN_SIGN); + projectionMap.put(KeyRings.CREATION, Tables.KEYS + "." + Keys.CREATION); + projectionMap.put(KeyRings.EXPIRY, Keys.EXPIRY); + projectionMap.put(KeyRings.ALGORITHM, Keys.ALGORITHM); + projectionMap.put(KeyRings.FINGERPRINT, Keys.FINGERPRINT); + projectionMap.put(KeyRings.USER_ID, UserIds.USER_ID); + projectionMap.put(KeyRings.VERIFIED, KeyRings.VERIFIED); + projectionMap.put(KeyRings.HAS_SECRET, "(" + Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + " IS NOT NULL) AS " + KeyRings.HAS_SECRET); + qb.setProjectionMap(projectionMap); + + qb.setTables( + Tables.KEYS + + " INNER JOIN " + Tables.USER_IDS + " ON (" + + Tables.KEYS + "." + Keys.MASTER_KEY_ID + + " = " + + Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID + + " AND " + Tables.USER_IDS + "." + UserIds.RANK + " = 0" + + ") LEFT JOIN " + Tables.KEY_RINGS_SECRET + " ON (" + + Tables.KEYS + "." + Keys.MASTER_KEY_ID + + " = " + + Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + + ") LEFT JOIN " + Tables.CERTS + " ON (" + + Tables.KEYS + "." + Keys.MASTER_KEY_ID + + " = " + + Tables.CERTS + "." + KeyRings.MASTER_KEY_ID + + " AND " + Tables.CERTS + "." + Certs.VERIFIED + + " = " + Certs.VERIFIED_SECRET + + ")" + ); + qb.appendWhere(Tables.KEYS + "." + Keys.RANK + " = 0"); + // in case there are multiple verifying certificates + groupBy = Tables.KEYS + "." + Keys.MASTER_KEY_ID; + + switch(match) { + case KEY_RING_UNIFIED: { + qb.appendWhere(" AND " + Tables.KEYS + "." + Keys.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + break; + } + case KEY_RINGS_FIND_BY_SUBKEY: { + try { + String subkey = Long.valueOf(uri.getLastPathSegment()).toString(); + qb.appendWhere(" AND EXISTS (" + + " SELECT 1 FROM " + Tables.KEYS + " AS tmp" + + " WHERE tmp." + UserIds.MASTER_KEY_ID + + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID + + " AND tmp." + Keys.KEY_ID + " = " + subkey + "" + + ")"); + } catch(NumberFormatException e) { + Log.e(Constants.TAG, "Malformed find by subkey query!", e); + qb.appendWhere(" AND 0"); + } + break; + } + case KEY_RINGS_FIND_BY_EMAIL: { + String chunks[] = uri.getLastPathSegment().split(" *, *"); + boolean gotCondition = false; + String emailWhere = ""; + // JAVA ♥ + for (int i = 0; i < chunks.length; ++i) { + if (chunks[i].length() == 0) { + continue; + } + if (i != 0) { + emailWhere += " OR "; + } + emailWhere += "tmp." + UserIds.USER_ID + " LIKE "; + // match '*', 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 1 FROM " + Tables.USER_IDS + " AS tmp" + + " WHERE tmp." + UserIds.MASTER_KEY_ID + + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID + + " AND (" + emailWhere + ")" + + ")"); + } else { + // TODO better way to do this? + Log.e(Constants.TAG, "Malformed find by email query!"); + qb.appendWhere(" AND 0"); + } + break; + } + } + + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = + Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + " IS NULL ASC, " + + Tables.USER_IDS + "." + UserIds.USER_ID + " ASC"; + } + + // uri to watch is all /key_rings/ + uri = KeyRings.CONTENT_URI; + + break; + } + + case KEY_RING_KEYS: { + HashMap projectionMap = new HashMap(); + projectionMap.put(Keys._ID, Tables.KEYS + ".oid AS _id"); + projectionMap.put(Keys.MASTER_KEY_ID, Tables.KEYS + "." + Keys.MASTER_KEY_ID); + projectionMap.put(Keys.RANK, Tables.KEYS + "." + Keys.RANK); + projectionMap.put(Keys.KEY_ID, Keys.KEY_ID); + projectionMap.put(Keys.KEY_SIZE, Keys.KEY_SIZE); + projectionMap.put(Keys.IS_REVOKED, Keys.IS_REVOKED); + projectionMap.put(Keys.CAN_CERTIFY, Keys.CAN_CERTIFY); + projectionMap.put(Keys.CAN_ENCRYPT, Keys.CAN_ENCRYPT); + projectionMap.put(Keys.CAN_SIGN, Keys.CAN_SIGN); + projectionMap.put(Keys.CREATION, Keys.CREATION); + projectionMap.put(Keys.EXPIRY, Keys.EXPIRY); + projectionMap.put(Keys.ALGORITHM, Keys.ALGORITHM); + projectionMap.put(Keys.FINGERPRINT, Keys.FINGERPRINT); + qb.setProjectionMap(projectionMap); + + qb.setTables(Tables.KEYS); + qb.appendWhere(Keys.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + + break; + } + + case KEY_RING_USER_IDS: { + HashMap projectionMap = new HashMap(); + projectionMap.put(UserIds._ID, Tables.USER_IDS + ".oid AS _id"); + projectionMap.put(UserIds.MASTER_KEY_ID, Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID); + projectionMap.put(UserIds.USER_ID, Tables.USER_IDS + "." + UserIds.USER_ID); + projectionMap.put(UserIds.RANK, Tables.USER_IDS + "." + UserIds.RANK); + projectionMap.put(UserIds.IS_PRIMARY, Tables.USER_IDS + "." + UserIds.IS_PRIMARY); + projectionMap.put(UserIds.IS_REVOKED, Tables.USER_IDS + "." + UserIds.IS_REVOKED); + // we take the minimum (>0) here, where "1" is "verified by known secret key" + projectionMap.put(UserIds.VERIFIED, "MIN(" + Certs.VERIFIED + ") AS " + UserIds.VERIFIED); + qb.setProjectionMap(projectionMap); + + qb.setTables(Tables.USER_IDS + + " LEFT JOIN " + Tables.CERTS + " ON (" + + Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID + " = " + + Tables.CERTS + "." + Certs.MASTER_KEY_ID + + " AND " + Tables.USER_IDS + "." + UserIds.RANK + " = " + + Tables.CERTS + "." + Certs.RANK + + " AND " + Tables.CERTS + "." + Certs.VERIFIED + " > 0" + + ")"); + groupBy = Tables.USER_IDS + "." + UserIds.RANK; + + qb.appendWhere(Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = Tables.USER_IDS + "." + UserIds.RANK + " ASC"; + } + + break; + + } + + case KEY_RINGS_PUBLIC: + case KEY_RING_PUBLIC: { + HashMap projectionMap = new HashMap(); + projectionMap.put(KeyRingData._ID, Tables.KEY_RINGS_PUBLIC + ".oid AS _id"); + projectionMap.put(KeyRingData.MASTER_KEY_ID, KeyRingData.MASTER_KEY_ID); + projectionMap.put(KeyRingData.KEY_RING_DATA, KeyRingData.KEY_RING_DATA); + qb.setProjectionMap(projectionMap); + + qb.setTables(Tables.KEY_RINGS_PUBLIC); + + if(match == KEY_RING_PUBLIC) { + qb.appendWhere(KeyRings.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + } + + break; + } + + case KEY_RINGS_SECRET: + case KEY_RING_SECRET: { + HashMap projectionMap = new HashMap(); + projectionMap.put(KeyRingData._ID, Tables.KEY_RINGS_SECRET + ".oid AS _id"); + projectionMap.put(KeyRingData.MASTER_KEY_ID, KeyRingData.MASTER_KEY_ID); + projectionMap.put(KeyRingData.KEY_RING_DATA, KeyRingData.KEY_RING_DATA); + qb.setProjectionMap(projectionMap); + + qb.setTables(Tables.KEY_RINGS_SECRET); + + if(match == KEY_RING_SECRET) { + qb.appendWhere(KeyRings.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + } + + break; + } + + case KEY_RING_CERTS: + case KEY_RING_CERTS_SPECIFIC: { + HashMap projectionMap = new HashMap(); + projectionMap.put(Certs._ID, Tables.CERTS + ".oid AS " + Certs._ID); + projectionMap.put(Certs.MASTER_KEY_ID, Tables.CERTS + "." + Certs.MASTER_KEY_ID); + projectionMap.put(Certs.RANK, Tables.CERTS + "." + Certs.RANK); + projectionMap.put(Certs.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED); + projectionMap.put(Certs.TYPE, Tables.CERTS + "." + Certs.TYPE); + projectionMap.put(Certs.CREATION, Tables.CERTS + "." + Certs.CREATION); + projectionMap.put(Certs.KEY_ID_CERTIFIER, Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER); + projectionMap.put(Certs.DATA, Tables.CERTS + "." + Certs.DATA); + projectionMap.put(Certs.USER_ID, Tables.USER_IDS + "." + UserIds.USER_ID); + projectionMap.put(Certs.SIGNER_UID, "signer." + UserIds.USER_ID + " AS " + Certs.SIGNER_UID); + qb.setProjectionMap(projectionMap); + + qb.setTables(Tables.CERTS + + " JOIN " + Tables.USER_IDS + " ON (" + + Tables.CERTS + "." + Certs.MASTER_KEY_ID + " = " + + Tables.USER_IDS + "." + UserIds.MASTER_KEY_ID + + " AND " + + Tables.CERTS + "." + Certs.RANK + " = " + + Tables.USER_IDS + "." + UserIds.RANK + + ") LEFT JOIN " + Tables.USER_IDS + " AS signer ON (" + + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = " + + "signer." + UserIds.MASTER_KEY_ID + + " AND " + + "signer." + Keys.RANK + " = 0" + + ")"); + + groupBy = Tables.CERTS + "." + Certs.RANK + ", " + + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER; + + qb.appendWhere(Tables.CERTS + "." + Certs.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + if(match == KEY_RING_CERTS_SPECIFIC) { + qb.appendWhere(" AND " + Tables.CERTS + "." + Certs.RANK + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(3)); + qb.appendWhere(" AND " + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER+ " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(4)); + } + + break; + } + + case API_APPS: + qb.setTables(Tables.API_APPS); + + break; + case API_APPS_BY_PACKAGE_NAME: + qb.setTables(Tables.API_APPS); + qb.appendWhere(ApiApps.PACKAGE_NAME + " = "); + qb.appendWhereEscapeString(uri.getLastPathSegment()); + + break; + case API_ACCOUNTS: + qb.setTables(Tables.API_ACCOUNTS); + qb.appendWhere(Tables.API_ACCOUNTS + "." + ApiAccounts.PACKAGE_NAME + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + + break; + case API_ACCOUNTS_BY_ACCOUNT_NAME: + qb.setTables(Tables.API_ACCOUNTS); + qb.appendWhere(Tables.API_ACCOUNTS + "." + ApiAccounts.PACKAGE_NAME + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + + qb.appendWhere(" AND " + Tables.API_ACCOUNTS + "." + ApiAccounts.ACCOUNT_NAME + " = "); + qb.appendWhereEscapeString(uri.getLastPathSegment()); + + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri + " (" + match + ")"); + + } + + // If no sort order is specified use the default + String orderBy; + if (TextUtils.isEmpty(sortOrder)) { + orderBy = null; + } else { + orderBy = sortOrder; + } + + SQLiteDatabase db = getDb().getReadableDatabase(); + Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, having, 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 = getDb().getWritableDatabase(); + + Uri rowUri = null; + Long keyId = null; + try { + final int match = mUriMatcher.match(uri); + + switch (match) { + case KEY_RING_PUBLIC: + db.insertOrThrow(Tables.KEY_RINGS_PUBLIC, null, values); + keyId = values.getAsLong(KeyRings.MASTER_KEY_ID); + break; + + case KEY_RING_SECRET: + db.insertOrThrow(Tables.KEY_RINGS_SECRET, null, values); + keyId = values.getAsLong(KeyRings.MASTER_KEY_ID); + break; + + case KEY_RING_KEYS: + Log.d(Constants.TAG, "keys"); + db.insertOrThrow(Tables.KEYS, null, values); + keyId = values.getAsLong(Keys.MASTER_KEY_ID); + break; + + case KEY_RING_USER_IDS: + db.insertOrThrow(Tables.USER_IDS, null, values); + keyId = values.getAsLong(UserIds.MASTER_KEY_ID); + break; + + case KEY_RING_CERTS: + // we replace here, keeping only the latest signature + // TODO this would be better handled in saveKeyRing directly! + db.replaceOrThrow(Tables.CERTS, null, values); + keyId = values.getAsLong(Certs.MASTER_KEY_ID); + break; + + case API_APPS: + db.insertOrThrow(Tables.API_APPS, null, values); + break; + + case API_ACCOUNTS: + // set foreign key automatically based on given uri + // e.g., api_apps/com.example.app/accounts/ + String packageName = uri.getPathSegments().get(1); + values.put(ApiAccounts.PACKAGE_NAME, packageName); + + Log.d(Constants.TAG, "provider packageName: " + packageName); + + db.insertOrThrow(Tables.API_ACCOUNTS, null, values); + // TODO: this is wrong: +// rowUri = ApiAccounts.buildIdUri(Long.toString(rowId)); + + break; + + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + + if(keyId != null) { + uri = KeyRings.buildGenericKeyRingUri(keyId.toString()); + rowUri = uri; + } + + // notify of changes in db + getContext().getContentResolver().notifyChange(uri, null); + + } catch (SQLiteConstraintException e) { + Log.e(Constants.TAG, "Constraint exception on insert! Entry already existing?", e); + } + + return rowUri; + } + + /** + * {@inheritDoc} + */ + @Override + public int delete(Uri uri, String additionalSelection, String[] selectionArgs) { + Log.v(Constants.TAG, "delete(uri=" + uri + ")"); + + final SQLiteDatabase db = getDb().getWritableDatabase(); + + int count; + final int match = mUriMatcher.match(uri); + + switch (match) { + case KEY_RING_PUBLIC: { + @SuppressWarnings("ConstantConditions") // ensured by uriMatcher above + String selection = KeyRings.MASTER_KEY_ID + " = " + uri.getPathSegments().get(1); + if (!TextUtils.isEmpty(additionalSelection)) { + selection += " AND (" + additionalSelection + ")"; + } + // corresponding keys and userIds are deleted by ON DELETE CASCADE + count = db.delete(Tables.KEY_RINGS_PUBLIC, selection, selectionArgs); + uri = KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)); + break; + } + case KEY_RING_SECRET: { + @SuppressWarnings("ConstantConditions") // ensured by uriMatcher above + String selection = KeyRings.MASTER_KEY_ID + " = " + uri.getPathSegments().get(1); + if (!TextUtils.isEmpty(additionalSelection)) { + selection += " AND (" + additionalSelection + ")"; + } + count = db.delete(Tables.KEY_RINGS_SECRET, selection, selectionArgs); + uri = KeyRings.buildGenericKeyRingUri(uri.getPathSegments().get(1)); + break; + } + + case API_APPS_BY_PACKAGE_NAME: + count = db.delete(Tables.API_APPS, buildDefaultApiAppsSelection(uri, additionalSelection), + selectionArgs); + break; + case API_ACCOUNTS_BY_ACCOUNT_NAME: + count = db.delete(Tables.API_ACCOUNTS, buildDefaultApiAccountsSelection(uri, additionalSelection), + 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 = mKeychainDatabase.getWritableDatabase(); + + String defaultSelection = null; + int count = 0; + try { + final int match = mUriMatcher.match(uri); + switch (match) { + case API_APPS_BY_PACKAGE_NAME: + count = db.update(Tables.API_APPS, values, + buildDefaultApiAppsSelection(uri, selection), selectionArgs); + break; + case API_ACCOUNTS_BY_ACCOUNT_NAME: + count = db.update(Tables.API_ACCOUNTS, values, + buildDefaultApiAccountsSelection(uri, selection), selectionArgs); + break; + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + + // notify of changes in db + getContext().getContentResolver().notifyChange(uri, null); + + } catch (SQLiteConstraintException e) { + Log.e(Constants.TAG, "Constraint exception on update! Entry already existing?"); + } + + return count; + } + + /** + * Build default selection statement for API apps. If no extra selection is specified only build + * where clause with rowId + * + * @param uri + * @param selection + * @return + */ + private String buildDefaultApiAppsSelection(Uri uri, String selection) { + String packageName = DatabaseUtils.sqlEscapeString(uri.getLastPathSegment()); + + String andSelection = ""; + if (!TextUtils.isEmpty(selection)) { + andSelection = " AND (" + selection + ")"; + } + + return ApiApps.PACKAGE_NAME + "=" + packageName + andSelection; + } + + private String buildDefaultApiAccountsSelection(Uri uri, String selection) { + String packageName = DatabaseUtils.sqlEscapeString(uri.getPathSegments().get(1)); + String accountName = DatabaseUtils.sqlEscapeString(uri.getLastPathSegment()); + + String andSelection = ""; + if (!TextUtils.isEmpty(selection)) { + andSelection = " AND (" + selection + ")"; + } + + return ApiAccounts.PACKAGE_NAME + "=" + packageName + " AND " + + ApiAccounts.ACCOUNT_NAME + "=" + accountName + + andSelection; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java new file mode 100644 index 000000000..701ffc6af --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.provider; + +import android.net.Uri; +import android.provider.BaseColumns; +import org.sufficientlysecure.keychain.Constants; + +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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java new file mode 100644 index 000000000..bc7de0b37 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2011 Markus Doits + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.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 = "openkeychain_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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java new file mode 100644 index 000000000..aa30e845d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2011 Markus Doits + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.provider; + +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 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 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 + "/KeychainBlobs"; + + 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 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java new file mode 100644 index 000000000..5e6d37237 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -0,0 +1,673 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.provider; + +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; + +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.pgp.PgpConversionHelper; +import org.sufficientlysecure.keychain.pgp.PgpHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.remote.AccountSettings; +import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SignatureException; +import java.util.ArrayList; +import java.util.Collections; +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 ProviderHelper { + + // If we ever switch to api level 11, we can ditch this whole mess! + public static final int FIELD_TYPE_NULL = 1; + // this is called integer to stay coherent with the constants in Cursor (api level 11) + public static final int FIELD_TYPE_INTEGER = 2; + public static final int FIELD_TYPE_FLOAT = 3; + public static final int FIELD_TYPE_STRING = 4; + public static final int FIELD_TYPE_BLOB = 5; + + public static Object getGenericData(Context context, Uri uri, String column, int type) { + return getGenericData(context, uri, new String[] { column }, new int[] { type }).get(column); + } + + public static HashMap getGenericData(Context context, Uri uri, String[] proj, int[] types) { + Cursor cursor = context.getContentResolver().query(uri, proj, null, null, null); + + HashMap result = new HashMap(proj.length); + if (cursor != null && cursor.moveToFirst()) { + int pos = 0; + for(String p : proj) { + switch(types[pos]) { + case FIELD_TYPE_NULL: result.put(p, cursor.isNull(pos)); break; + case FIELD_TYPE_INTEGER: result.put(p, cursor.getLong(pos)); break; + case FIELD_TYPE_FLOAT: result.put(p, cursor.getFloat(pos)); break; + case FIELD_TYPE_STRING: result.put(p, cursor.getString(pos)); break; + case FIELD_TYPE_BLOB: result.put(p, cursor.getBlob(pos)); break; + } + pos += 1; + } + } + + if (cursor != null) { + cursor.close(); + } + + return result; + } + + public static Object getUnifiedData(Context context, long masterKeyId, String column, int type) { + return getUnifiedData(context, masterKeyId, new String[] { column }, new int[] { type }).get(column); + } + + public static HashMap getUnifiedData(Context context, long masterKeyId, String[] proj, int[] types) { + return getGenericData(context, KeyRings.buildUnifiedKeyRingUri(Long.toString(masterKeyId)), proj, types); + } + + /** + * Find the master key id related to a given query. The id will either be extracted from the + * query, which should work for all specific /key_rings/ queries, or will be queried if it can't. + */ + public static long getMasterKeyId(Context context, Uri queryUri) { + // try extracting from the uri first + String firstSegment = queryUri.getPathSegments().get(1); + if(!firstSegment.equals("find")) try { + return Long.parseLong(firstSegment); + } catch(NumberFormatException e) { + // didn't work? oh well. + Log.d(Constants.TAG, "Couldn't get masterKeyId from URI, querying..."); + } + Object data = getGenericData(context, queryUri, KeyRings.MASTER_KEY_ID, FIELD_TYPE_INTEGER); + if(data != null) + return (Long) data; + // TODO better error handling? + return 0L; + } + + public static Map getPGPKeyRings(Context context, Uri queryUri) { + Cursor cursor = context.getContentResolver().query(queryUri, + new String[]{KeyRingData.MASTER_KEY_ID, KeyRingData.KEY_RING_DATA }, + null, null, null); + + Map result = new HashMap(cursor.getCount()); + if (cursor != null && cursor.moveToFirst()) do { + long masterKeyId = cursor.getLong(0); + byte[] data = cursor.getBlob(1); + if (data != null) { + result.put(masterKeyId, PgpConversionHelper.BytesToPGPKeyRing(data)); + } + } while(cursor.moveToNext()); + + if (cursor != null) { + cursor.close(); + } + + return result; + } + public static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) { + Map result = getPGPKeyRings(context, queryUri); + if(result.isEmpty()) + return null; + return result.values().iterator().next(); + } + + public static PGPPublicKeyRing getPGPPublicKeyRingWithKeyId(Context context, long keyId) { + Uri uri = KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(keyId)); + long masterKeyId = getMasterKeyId(context, uri); + if(masterKeyId != 0) + return getPGPPublicKeyRing(context, masterKeyId); + return null; + } + public static PGPSecretKeyRing getPGPSecretKeyRingWithKeyId(Context context, long keyId) { + Uri uri = KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(keyId)); + long masterKeyId = getMasterKeyId(context, uri); + if(masterKeyId != 0) + return getPGPSecretKeyRing(context, masterKeyId); + return null; + } + + /** + * Retrieves the actual PGPPublicKeyRing object from the database blob based on the masterKeyId + */ + public static PGPPublicKeyRing getPGPPublicKeyRing(Context context, + long masterKeyId) { + Uri queryUri = KeyRingData.buildPublicKeyRingUri(Long.toString(masterKeyId)); + return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri); + } + + /** + * Retrieves the actual PGPSecretKeyRing object from the database blob based on the maserKeyId + */ + public static PGPSecretKeyRing getPGPSecretKeyRing(Context context, + long masterKeyId) { + Uri queryUri = KeyRingData.buildSecretKeyRingUri(Long.toString(masterKeyId)); + return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri); + } + + /** + * Saves PGPPublicKeyRing with its keys and userIds in DB + */ + @SuppressWarnings("unchecked") + public static void saveKeyRing(Context context, PGPPublicKeyRing keyRing) throws IOException { + PGPPublicKey masterKey = keyRing.getPublicKey(); + long masterKeyId = masterKey.getKeyID(); + + // IF there is a secret key, preserve it! + PGPSecretKeyRing secretRing = ProviderHelper.getPGPSecretKeyRing(context, masterKeyId); + + // delete old version of this keyRing, which also deletes all keys and userIds on cascade + try { + context.getContentResolver().delete(KeyRingData.buildPublicKeyRingUri(Long.toString(masterKeyId)), 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(); + // use exactly the same _ID again to replace key in-place. + // NOTE: If we would not use the same _ID again, + // getting back to the ViewKeyActivity would result in Nullpointer, + // because the currently loaded key would be gone from the database + values.put(KeyRingData.MASTER_KEY_ID, masterKeyId); + values.put(KeyRingData.KEY_RING_DATA, keyRing.getEncoded()); + + // insert new version of this keyRing + Uri uri = KeyRingData.buildPublicKeyRingUri(Long.toString(masterKeyId)); + Uri insertedUri = context.getContentResolver().insert(uri, values); + + // save all keys and userIds included in keyRing object in database + ArrayList operations = new ArrayList(); + + int rank = 0; + for (PGPPublicKey key : new IterableIterator(keyRing.getPublicKeys())) { + operations.add(buildPublicKeyOperations(context, masterKeyId, key, rank)); + ++rank; + } + + // get a list of owned secret keys, for verification filtering + Map allKeyRings = getPGPKeyRings(context, KeyRingData.buildSecretKeyRingUri()); + // special case: available secret keys verify themselves! + if(secretRing != null) + allKeyRings.put(secretRing.getSecretKey().getKeyID(), secretRing); + + // classify and order user ids. primary are moved to the front, revoked to the back, + // otherwise the order in the keyfile is preserved. + List uids = new ArrayList(); + + for (String userId : new IterableIterator(masterKey.getUserIDs())) { + UserIdItem item = new UserIdItem(); + uids.add(item); + item.userId = userId; + + // look through signatures for this specific key + for (PGPSignature cert : new IterableIterator( + masterKey.getSignaturesForID(userId))) { + long certId = cert.getKeyID(); + try { + // self signature + if(certId == masterKeyId) { + cert.init(new JcaPGPContentVerifierBuilderProvider().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME), masterKey); + if(!cert.verifyCertification(userId, masterKey)) { + // not verified?! dang! TODO notify user? this is kinda serious... + Log.e(Constants.TAG, "Could not verify self signature for " + userId + "!"); + continue; + } + // is this the first, or a more recent certificate? + if(item.selfCert == null || + item.selfCert.getCreationTime().before(cert.getCreationTime())) { + item.selfCert = cert; + item.isPrimary = cert.getHashedSubPackets().isPrimaryUserID(); + item.isRevoked = + cert.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION; + } + } + // verify signatures from known private keys + if(allKeyRings.containsKey(certId)) { + // mark them as verified + cert.init(new JcaPGPContentVerifierBuilderProvider().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME), + allKeyRings.get(certId).getPublicKey()); + if(cert.verifyCertification(userId, masterKey)) { + item.trustedCerts.add(cert); + } + } + } catch(SignatureException e) { + Log.e(Constants.TAG, "Signature verification failed! " + + PgpKeyHelper.convertKeyIdToHex(masterKey.getKeyID()) + + " from " + + PgpKeyHelper.convertKeyIdToHex(cert.getKeyID()), e); + } catch(PGPException e) { + Log.e(Constants.TAG, "Signature verification failed! " + + PgpKeyHelper.convertKeyIdToHex(masterKey.getKeyID()) + + " from " + + PgpKeyHelper.convertKeyIdToHex(cert.getKeyID()), e); + } + } + } + + // primary before regular before revoked (see UserIdItem.compareTo) + // this is a stable sort, so the order of keys is otherwise preserved. + Collections.sort(uids); + // iterate and put into db + for(int userIdRank = 0; userIdRank < uids.size(); userIdRank++) { + UserIdItem item = uids.get(userIdRank); + operations.add(buildUserIdOperations(masterKeyId, item, userIdRank)); + // no self cert is bad, but allowed by the rfc... + if(item.selfCert != null) { + operations.add(buildCertOperations( + masterKeyId, userIdRank, item.selfCert, Certs.VERIFIED_SELF)); + } + // don't bother with trusted certs if the uid is revoked, anyways + if(item.isRevoked) { + continue; + } + for(int i = 0; i < item.trustedCerts.size(); i++) { + operations.add(buildCertOperations( + masterKeyId, userIdRank, item.trustedCerts.get(i), Certs.VERIFIED_SECRET)); + } + } + + try { + context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY, operations); + } catch (RemoteException e) { + Log.e(Constants.TAG, "applyBatch failed!", e); + } catch (OperationApplicationException e) { + Log.e(Constants.TAG, "applyBatch failed!", e); + } + + // Save the saved keyring (if any) + if(secretRing != null) { + saveKeyRing(context, secretRing); + } + + } + + private static class UserIdItem implements Comparable { + String userId; + boolean isPrimary = false; + boolean isRevoked = false; + PGPSignature selfCert; + List trustedCerts = new ArrayList(); + + @Override + public int compareTo(UserIdItem o) { + // if one key is primary but the other isn't, the primary one always comes first + if(isPrimary != o.isPrimary) + return isPrimary ? -1 : 1; + // revoked keys always come last! + if(isRevoked != o.isRevoked) + return isRevoked ? 1 : -1; + return 0; + } + } + + /** + * Saves a PGPSecretKeyRing in the DB. This will only work if a corresponding public keyring + * is already in the database! + */ + @SuppressWarnings("unchecked") + public static void saveKeyRing(Context context, PGPSecretKeyRing keyRing) throws IOException { + long masterKeyId = keyRing.getPublicKey().getKeyID(); + + // save secret keyring + ContentValues values = new ContentValues(); + values.put(KeyRingData.MASTER_KEY_ID, masterKeyId); + values.put(KeyRingData.KEY_RING_DATA, keyRing.getEncoded()); + // insert new version of this keyRing + Uri uri = KeyRingData.buildSecretKeyRingUri(Long.toString(masterKeyId)); + context.getContentResolver().insert(uri, values); + + } + + /** + * Saves (or updates) a pair of public and secret KeyRings in the database + */ + @SuppressWarnings("unchecked") + public static void saveKeyRing(Context context, PGPPublicKeyRing pubRing, PGPSecretKeyRing privRing) throws IOException { + long masterKeyId = pubRing.getPublicKey().getKeyID(); + + // delete secret keyring (so it isn't unnecessarily saved by public-saveKeyRing below) + context.getContentResolver().delete(KeyRingData.buildSecretKeyRingUri(Long.toString(masterKeyId)), null, null); + + // save public keyring + saveKeyRing(context, pubRing); + saveKeyRing(context, privRing); + } + + /** + * Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing + */ + private static ContentProviderOperation buildPublicKeyOperations(Context context, + long masterKeyId, PGPPublicKey key, int rank) throws IOException { + + ContentValues values = new ContentValues(); + values.put(Keys.MASTER_KEY_ID, masterKeyId); + values.put(Keys.RANK, rank); + + values.put(Keys.KEY_ID, key.getKeyID()); + values.put(Keys.KEY_SIZE, key.getBitStrength()); + values.put(Keys.ALGORITHM, key.getAlgorithm()); + values.put(Keys.FINGERPRINT, key.getFingerprint()); + + values.put(Keys.CAN_CERTIFY, (PgpKeyHelper.isCertificationKey(key))); + values.put(Keys.CAN_SIGN, (PgpKeyHelper.isSigningKey(key))); + values.put(Keys.CAN_ENCRYPT, PgpKeyHelper.isEncryptionKey(key)); + values.put(Keys.IS_REVOKED, key.isRevoked()); + + values.put(Keys.CREATION, PgpKeyHelper.getCreationDate(key).getTime() / 1000); + Date expiryDate = PgpKeyHelper.getExpiryDate(key); + if (expiryDate != null) { + values.put(Keys.EXPIRY, expiryDate.getTime() / 1000); + } + + Uri uri = Keys.buildKeysUri(Long.toString(masterKeyId)); + + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + /** + * Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing + */ + private static ContentProviderOperation buildCertOperations(long masterKeyId, + int rank, + PGPSignature cert, + int verified) + throws IOException { + ContentValues values = new ContentValues(); + values.put(Certs.MASTER_KEY_ID, masterKeyId); + values.put(Certs.RANK, rank); + values.put(Certs.KEY_ID_CERTIFIER, cert.getKeyID()); + values.put(Certs.TYPE, cert.getSignatureType()); + values.put(Certs.CREATION, cert.getCreationTime().getTime() / 1000); + values.put(Certs.VERIFIED, verified); + values.put(Certs.DATA, cert.getEncoded()); + + Uri uri = Certs.buildCertsUri(Long.toString(masterKeyId)); + + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + /** + * Build ContentProviderOperation to add PublicUserIds to database corresponding to a keyRing + */ + private static ContentProviderOperation buildUserIdOperations(long masterKeyId, UserIdItem item, + int rank) { + ContentValues values = new ContentValues(); + values.put(UserIds.MASTER_KEY_ID, masterKeyId); + values.put(UserIds.USER_ID, item.userId); + values.put(UserIds.IS_PRIMARY, item.isPrimary); + values.put(UserIds.IS_REVOKED, item.isRevoked); + values.put(UserIds.RANK, rank); + + Uri uri = UserIds.buildUserIdsUri(Long.toString(masterKeyId)); + + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + public static ArrayList getKeyRingsAsArmoredString(Context context, long[] masterKeyIds) { + ArrayList output = new ArrayList(); + + if (masterKeyIds != null && masterKeyIds.length > 0) { + + Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, masterKeyIds); + + if (cursor != null) { + int masterIdCol = cursor.getColumnIndex(KeyRingData.MASTER_KEY_ID); + int dataCol = cursor.getColumnIndex(KeyRingData.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", PgpHelper.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, "armoredKey:" + 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; + } + } + + private static Cursor getCursorWithSelectedKeyringMasterKeyIds(Context context, long[] masterKeyIds) { + Cursor cursor = null; + if (masterKeyIds != null && masterKeyIds.length > 0) { + + String inMasterKeyList = KeyRingData.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(KeyRingData.buildPublicKeyRingUri(), new String[] { + KeyRingData._ID, KeyRingData.MASTER_KEY_ID, KeyRingData.KEY_RING_DATA + }, inMasterKeyList, null, null); + } + + return cursor; + } + + public static ArrayList getRegisteredApiApps(Context context) { + Cursor cursor = context.getContentResolver().query(ApiApps.CONTENT_URI, null, null, null, + null); + + ArrayList packageNames = new ArrayList(); + if (cursor != null) { + int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME); + if (cursor.moveToFirst()) { + do { + packageNames.add(cursor.getString(packageNameCol)); + } while (cursor.moveToNext()); + } + } + + if (cursor != null) { + cursor.close(); + } + + return packageNames; + } + + private static ContentValues contentValueForApiApps(AppSettings appSettings) { + ContentValues values = new ContentValues(); + values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName()); + values.put(ApiApps.PACKAGE_SIGNATURE, appSettings.getPackageSignature()); + return values; + } + + private static ContentValues contentValueForApiAccounts(AccountSettings accSettings) { + ContentValues values = new ContentValues(); + values.put(KeychainContract.ApiAccounts.ACCOUNT_NAME, accSettings.getAccountName()); + values.put(KeychainContract.ApiAccounts.KEY_ID, accSettings.getKeyId()); + values.put(KeychainContract.ApiAccounts.COMPRESSION, accSettings.getCompression()); + values.put(KeychainContract.ApiAccounts.ENCRYPTION_ALGORITHM, accSettings.getEncryptionAlgorithm()); + values.put(KeychainContract.ApiAccounts.HASH_ALORITHM, accSettings.getHashAlgorithm()); + return values; + } + + public static void insertApiApp(Context context, AppSettings appSettings) { + context.getContentResolver().insert(KeychainContract.ApiApps.CONTENT_URI, + contentValueForApiApps(appSettings)); + } + + public static void insertApiAccount(Context context, Uri uri, AccountSettings accSettings) { + context.getContentResolver().insert(uri, contentValueForApiAccounts(accSettings)); + } + + public static void updateApiApp(Context context, AppSettings appSettings, Uri uri) { + if (context.getContentResolver().update(uri, contentValueForApiApps(appSettings), null, + null) <= 0) { + throw new RuntimeException(); + } + } + + public static void updateApiAccount(Context context, AccountSettings accSettings, Uri uri) { + if (context.getContentResolver().update(uri, contentValueForApiAccounts(accSettings), null, + null) <= 0) { + throw new RuntimeException(); + } + } + + /** + * Must be an uri pointing to an account + * + * @param context + * @param uri + * @return + */ + public static AppSettings getApiAppSettings(Context context, Uri uri) { + AppSettings settings = null; + + Cursor cur = context.getContentResolver().query(uri, null, null, null, null); + if (cur != null && cur.moveToFirst()) { + settings = new AppSettings(); + settings.setPackageName(cur.getString( + cur.getColumnIndex(KeychainContract.ApiApps.PACKAGE_NAME))); + settings.setPackageSignature(cur.getBlob( + cur.getColumnIndex(KeychainContract.ApiApps.PACKAGE_SIGNATURE))); + } + + return settings; + } + + public static AccountSettings getApiAccountSettings(Context context, Uri accountUri) { + AccountSettings settings = null; + + Cursor cur = context.getContentResolver().query(accountUri, null, null, null, null); + if (cur != null && cur.moveToFirst()) { + settings = new AccountSettings(); + + settings.setAccountName(cur.getString( + cur.getColumnIndex(KeychainContract.ApiAccounts.ACCOUNT_NAME))); + settings.setKeyId(cur.getLong( + cur.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID))); + settings.setCompression(cur.getInt( + cur.getColumnIndexOrThrow(KeychainContract.ApiAccounts.COMPRESSION))); + settings.setHashAlgorithm(cur.getInt( + cur.getColumnIndexOrThrow(KeychainContract.ApiAccounts.HASH_ALORITHM))); + settings.setEncryptionAlgorithm(cur.getInt( + cur.getColumnIndexOrThrow(KeychainContract.ApiAccounts.ENCRYPTION_ALGORITHM))); + } + + return settings; + } + + public static Set getAllKeyIdsForApp(Context context, Uri uri) { + Set keyIds = new HashSet(); + + Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + if (cursor != null) { + int keyIdColumn = cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID); + while (cursor.moveToNext()) { + keyIds.add(cursor.getLong(keyIdColumn)); + } + } + + return keyIds; + } + + public static byte[] getApiAppSignature(Context context, String packageName) { + Uri queryUri = ApiApps.buildByPackageNameUri(packageName); + + String[] projection = new String[]{ApiApps.PACKAGE_SIGNATURE}; + + ContentResolver cr = context.getContentResolver(); + Cursor cursor = cr.query(queryUri, projection, null, null, null); + + byte[] signature = null; + if (cursor != null && cursor.moveToFirst()) { + int signatureCol = 0; + + signature = cursor.getBlob(signatureCol); + } + + if (cursor != null) { + cursor.close(); + } + + return signature; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AccountSettings.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AccountSettings.java new file mode 100644 index 000000000..832cbc752 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AccountSettings.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote; + +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.sufficientlysecure.keychain.Id; + +public class AccountSettings { + private String mAccountName; + private long mKeyId = Id.key.none; + private int mEncryptionAlgorithm; + private int mHashAlgorithm; + private int mCompression; + + public AccountSettings() { + + } + + public AccountSettings(String accountName) { + super(); + this.mAccountName = accountName; + + // defaults: + this.mEncryptionAlgorithm = PGPEncryptedData.AES_256; + this.mHashAlgorithm = HashAlgorithmTags.SHA512; + this.mCompression = Id.choice.compression.zlib; + } + + public String getAccountName() { + return mAccountName; + } + + public void setAccountName(String mAccountName) { + this.mAccountName = mAccountName; + } + + public long getKeyId() { + return mKeyId; + } + + public void setKeyId(long scretKeyId) { + this.mKeyId = scretKeyId; + } + + public int getEncryptionAlgorithm() { + return mEncryptionAlgorithm; + } + + public void setEncryptionAlgorithm(int encryptionAlgorithm) { + this.mEncryptionAlgorithm = encryptionAlgorithm; + } + + public int getHashAlgorithm() { + return mHashAlgorithm; + } + + public void setHashAlgorithm(int hashAlgorithm) { + this.mHashAlgorithm = hashAlgorithm; + } + + public int getCompression() { + return mCompression; + } + + public void setCompression(int compression) { + this.mCompression = compression; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java new file mode 100644 index 000000000..a3f9f84c9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/AppSettings.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote; + +public class AppSettings { + private String mPackageName; + private byte[] mPackageSignature; + + public AppSettings() { + + } + + public AppSettings(String packageName, byte[] packageSignature) { + super(); + this.mPackageName = packageName; + this.mPackageSignature = packageSignature; + } + + public String getPackageName() { + return mPackageName; + } + + public void setPackageName(String packageName) { + this.mPackageName = packageName; + } + + public byte[] getPackageSignature() { + return mPackageSignature; + } + + public void setPackageSignature(byte[] packageSignature) { + this.mPackageSignature = packageSignature; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java new file mode 100644 index 000000000..b38fea5a9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote; + +import android.app.PendingIntent; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; + +import org.openintents.openpgp.IOpenPgpService; +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.spongycastle.util.Arrays; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.PgpSignEncrypt; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.remote.ui.RemoteServiceActivity; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.ImportKeysActivity; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Set; + +public class OpenPgpService extends RemoteService { + + /** + * Search database for key ids based on emails. + * + * @param encryptionUserIds + * @return + */ + private Intent getKeyIdsFromEmails(Intent data, String[] encryptionUserIds) { + // find key ids to given emails in database + ArrayList keyIds = new ArrayList(); + + boolean missingUserIdsCheck = false; + boolean duplicateUserIdsCheck = false; + ArrayList missingUserIds = new ArrayList(); + ArrayList duplicateUserIds = new ArrayList(); + + for (String email : encryptionUserIds) { + Uri uri = KeyRings.buildUnifiedKeyRingsFindByEmailUri(email); + Cursor cur = getContentResolver().query(uri, null, null, null, null); + if (cur.moveToFirst()) { + long id = cur.getLong(cur.getColumnIndex(KeyRings.MASTER_KEY_ID)); + keyIds.add(id); + } else { + missingUserIdsCheck = true; + missingUserIds.add(email); + Log.d(Constants.TAG, "user id missing"); + } + if (cur.moveToNext()) { + duplicateUserIdsCheck = true; + duplicateUserIds.add(email); + Log.d(Constants.TAG, "more than one user id with the same email"); + } + } + + // convert to long[] + long[] keyIdsArray = new long[keyIds.size()]; + for (int i = 0; i < keyIdsArray.length; i++) { + keyIdsArray[i] = keyIds.get(i); + } + + // allow the user to verify pub key selection + if (missingUserIdsCheck || duplicateUserIdsCheck) { + // build PendingIntent + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_SELECT_PUB_KEYS); + intent.putExtra(RemoteServiceActivity.EXTRA_SELECTED_MASTER_KEY_IDS, keyIdsArray); + intent.putExtra(RemoteServiceActivity.EXTRA_MISSING_USER_IDS, missingUserIds); + intent.putExtra(RemoteServiceActivity.EXTRA_DUBLICATE_USER_IDS, duplicateUserIds); + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + return result; + } + + if (keyIdsArray.length == 0) { + return null; + } + + Intent result = new Intent(); + result.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIdsArray); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } + + private Intent getPassphraseBundleIntent(Intent data, long keyId) { + // build PendingIntent for passphrase input + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_CACHE_PASSPHRASE); + intent.putExtra(RemoteServiceActivity.EXTRA_SECRET_KEY_ID, keyId); + // pass params through to activity that it can be returned again later to repeat pgp operation + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + return result; + } + + private Intent signImpl(Intent data, ParcelFileDescriptor input, + ParcelFileDescriptor output, AccountSettings accSettings) { + try { + boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + + // get passphrase from cache, if key has "no" passphrase, this returns an empty String + String passphrase; + if (data.hasExtra(OpenPgpApi.EXTRA_PASSPHRASE)) { + passphrase = data.getStringExtra(OpenPgpApi.EXTRA_PASSPHRASE); + } else { + passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), accSettings.getKeyId()); + } + if (passphrase == null) { + // get PendingIntent for passphrase input, add it to given params and return to client + Intent passphraseBundle = getPassphraseBundleIntent(data, accSettings.getKeyId()); + return passphraseBundle; + } + + // Get Input- and OutputStream from ParcelFileDescriptor + InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input); + OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + try { + long inputLength = is.available(); + InputData inputData = new InputData(is, inputLength); + + // sign-only + PgpSignEncrypt.Builder builder = new PgpSignEncrypt.Builder(getContext(), inputData, os); + builder.enableAsciiArmorOutput(asciiArmor) + .signatureHashAlgorithm(accSettings.getHashAlgorithm()) + .signatureForceV3(false) + .signatureKeyId(accSettings.getKeyId()) + .signaturePassphrase(passphrase); + builder.build().execute(); + } finally { + is.close(); + os.close(); + } + + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } catch (Exception e) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + } + + private Intent encryptAndSignImpl(Intent data, ParcelFileDescriptor input, + ParcelFileDescriptor output, AccountSettings accSettings, + boolean sign) { + try { + boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + + long[] keyIds; + if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) { + keyIds = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS); + } else if (data.hasExtra(OpenPgpApi.EXTRA_USER_IDS)) { + // get key ids based on given user ids + String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS); + // give params through to activity... + Intent result = getKeyIdsFromEmails(data, userIds); + + if (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0) == OpenPgpApi.RESULT_CODE_SUCCESS) { + keyIds = result.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS); + } else { + // if not success -> result contains a PendingIntent for user interaction + return result; + } + } else { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, + "Missing parameter user_ids or key_ids!")); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + + // add own key for encryption + keyIds = Arrays.copyOf(keyIds, keyIds.length + 1); + keyIds[keyIds.length - 1] = accSettings.getKeyId(); + + // build InputData and write into OutputStream + // Get Input- and OutputStream from ParcelFileDescriptor + InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input); + OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + try { + long inputLength = is.available(); + InputData inputData = new InputData(is, inputLength); + + PgpSignEncrypt.Builder builder = new PgpSignEncrypt.Builder(getContext(), inputData, os); + builder.enableAsciiArmorOutput(asciiArmor) + .compressionId(accSettings.getCompression()) + .symmetricEncryptionAlgorithm(accSettings.getEncryptionAlgorithm()) + .encryptionKeyIds(keyIds); + + if (sign) { + String passphrase; + if (data.hasExtra(OpenPgpApi.EXTRA_PASSPHRASE)) { + passphrase = data.getStringExtra(OpenPgpApi.EXTRA_PASSPHRASE); + } else { + passphrase = PassphraseCacheService.getCachedPassphrase(getContext(), + accSettings.getKeyId()); + } + if (passphrase == null) { + // get PendingIntent for passphrase input, add it to given params and return to client + Intent passphraseBundle = getPassphraseBundleIntent(data, accSettings.getKeyId()); + return passphraseBundle; + } + + // sign and encrypt + builder.signatureHashAlgorithm(accSettings.getHashAlgorithm()) + .signatureForceV3(false) + .signatureKeyId(accSettings.getKeyId()) + .signaturePassphrase(passphrase); + } else { + // encrypt only + builder.signatureKeyId(Id.key.none); + } + // execute PGP operation! + builder.build().execute(); + } finally { + is.close(); + os.close(); + } + + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } catch (Exception e) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + } + + private Intent decryptAndVerifyImpl(Intent data, ParcelFileDescriptor input, + ParcelFileDescriptor output, Set allowedKeyIds) { + try { + // Get Input- and OutputStream from ParcelFileDescriptor + InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input); + OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + + Intent result = new Intent(); + try { + + String passphrase = data.getStringExtra(OpenPgpApi.EXTRA_PASSPHRASE); + long inputLength = is.available(); + InputData inputData = new InputData(is, inputLength); + + PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder(this, inputData, os); + builder.allowSymmetricDecryption(false) // no support for symmetric encryption + .allowedKeyIds(allowedKeyIds) // allow only private keys associated with + // accounts of this app + .passphrase(passphrase); + + // TODO: currently does not support binary signed-only content + PgpDecryptVerifyResult decryptVerifyResult = builder.build().execute(); + + if (PgpDecryptVerifyResult.KEY_PASSHRASE_NEEDED == decryptVerifyResult.getStatus()) { + // get PendingIntent for passphrase input, add it to given params and return to client + Intent passphraseBundle = + getPassphraseBundleIntent(data, decryptVerifyResult.getKeyIdPassphraseNeeded()); + return passphraseBundle; + } else if (PgpDecryptVerifyResult.SYMMETRIC_PASSHRASE_NEEDED == + decryptVerifyResult.getStatus()) { + throw new PgpGeneralException("Decryption of symmetric content not supported by API!"); + } + + OpenPgpSignatureResult signatureResult = decryptVerifyResult.getSignatureResult(); + if (signatureResult != null) { + if (signatureResult.getStatus() == OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY) { + // If signature is unknown we return an _additional_ PendingIntent + // to retrieve the missing key + Intent intent = new Intent(getBaseContext(), ImportKeysActivity.class); + intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN); + intent.putExtra(ImportKeysActivity.EXTRA_KEY_ID, signatureResult.getKeyId()); + intent.putExtra(ImportKeysActivity.EXTRA_PENDING_INTENT_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + } + + result.putExtra(OpenPgpApi.RESULT_SIGNATURE, signatureResult); + } + + } finally { + is.close(); + os.close(); + } + + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } catch (Exception e) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + } + + private Intent getKeyImpl(Intent data) { + try { + long keyId = data.getLongExtra(OpenPgpApi.EXTRA_KEY_ID, 0); + + if (ProviderHelper.getPGPPublicKeyRing(this, keyId) == null) { + Intent result = new Intent(); + + // If keys are not in db we return an additional PendingIntent + // to retrieve the missing key + Intent intent = new Intent(getBaseContext(), ImportKeysActivity.class); + intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN); + intent.putExtra(ImportKeysActivity.EXTRA_KEY_ID, keyId); + intent.putExtra(ImportKeysActivity.EXTRA_PENDING_INTENT_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + return result; + } else { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + + // TODO: also return PendingIntent that opens the key view activity + + return result; + } + } catch (Exception e) { + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + } + + private Intent getKeyIdsImpl(Intent data) { + // get key ids based on given user ids + String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS); + Intent result = getKeyIdsFromEmails(data, userIds); + return result; + } + + /** + * Check requirements: + * - params != null + * - has supported API version + * - is allowed to call the service (access has been granted) + * + * @param data + * @return null if everything is okay, or a Bundle with an error/PendingIntent + */ + private Intent checkRequirements(Intent data) { + // params Bundle is required! + if (data == null) { + Intent result = new Intent(); + OpenPgpError error = new OpenPgpError(OpenPgpError.GENERIC_ERROR, "params Bundle required!"); + result.putExtra(OpenPgpApi.RESULT_ERROR, error); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + + // version code is required and needs to correspond to version code of service! + if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != OpenPgpApi.API_VERSION) { + Intent result = new Intent(); + OpenPgpError error = new OpenPgpError + (OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!"); + result.putExtra(OpenPgpApi.RESULT_ERROR, error); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + return result; + } + + // check if caller is allowed to access openpgp keychain + Intent result = isAllowed(data); + if (result != null) { + return result; + } + + return null; + } + + // TODO: multi-threading + private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() { + + @Override + public Intent execute(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) { + Intent errorResult = checkRequirements(data); + if (errorResult != null) { + return errorResult; + } + + String accName; + if (data.getStringExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME) != null) { + accName = data.getStringExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME); + } else { + accName = "default"; + } + final AccountSettings accSettings = getAccSettings(accName); + if (accSettings == null) { + return getCreateAccountIntent(data, accName); + } + + String action = data.getAction(); + if (OpenPgpApi.ACTION_SIGN.equals(action)) { + return signImpl(data, input, output, accSettings); + } else if (OpenPgpApi.ACTION_ENCRYPT.equals(action)) { + return encryptAndSignImpl(data, input, output, accSettings, false); + } else if (OpenPgpApi.ACTION_SIGN_AND_ENCRYPT.equals(action)) { + return encryptAndSignImpl(data, input, output, accSettings, true); + } else if (OpenPgpApi.ACTION_DECRYPT_VERIFY.equals(action)) { + String currentPkg = getCurrentCallingPackage(); + Set allowedKeyIds = + ProviderHelper.getAllKeyIdsForApp(mContext, + ApiAccounts.buildBaseUri(currentPkg)); + return decryptAndVerifyImpl(data, input, output, allowedKeyIds); + } else if (OpenPgpApi.ACTION_GET_KEY.equals(action)) { + return getKeyImpl(data); + } else if (OpenPgpApi.ACTION_GET_KEY_IDS.equals(action)) { + return getKeyIdsImpl(data); + } else { + return null; + } + } + + }; + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java new file mode 100644 index 000000000..16a800022 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/RemoteService.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.Signature; +import android.net.Uri; +import android.os.Binder; + +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.util.OpenPgpApi; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.remote.ui.RemoteServiceActivity; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Abstract service class for remote APIs that handle app registration and user input. + */ +public abstract class RemoteService extends Service { + Context mContext; + + public Context getContext() { + return mContext; + } + + protected Intent isAllowed(Intent data) { + try { + if (isCallerAllowed(false)) { + return null; + } else { + String packageName = getCurrentCallingPackage(); + Log.d(Constants.TAG, "isAllowed packageName: " + packageName); + + byte[] packageSignature; + try { + packageSignature = getPackageSignature(packageName); + } catch (NameNotFoundException e) { + Log.e(Constants.TAG, "Should not happen, returning!", e); + // return error + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); + result.putExtra(OpenPgpApi.RESULT_ERROR, + new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); + return result; + } + Log.e(Constants.TAG, "Not allowed to use service! return PendingIntent for registration!"); + + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_REGISTER); + intent.putExtra(RemoteServiceActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteServiceActivity.EXTRA_PACKAGE_SIGNATURE, packageSignature); + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + + return result; + } + } catch (WrongPackageSignatureException e) { + Log.e(Constants.TAG, "wrong signature!", e); + + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_ERROR_MESSAGE); + intent.putExtra(RemoteServiceActivity.EXTRA_ERROR_MESSAGE, + getString(R.string.api_error_wrong_signature)); + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + + return result; + } + } + + private byte[] getPackageSignature(String packageName) throws NameNotFoundException { + PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName, + PackageManager.GET_SIGNATURES); + Signature[] signatures = pkgInfo.signatures; + // TODO: Only first signature?! + byte[] packageSignature = signatures[0].toByteArray(); + + return packageSignature; + } + + /** + * Returns package name associated with the UID, which is assigned to the process that sent you the + * current transaction that is being processed :) + * + * @return package name + */ + protected String getCurrentCallingPackage() { + // TODO: + // callingPackages contains more than one entry when sharedUserId has been used... + String[] callingPackages = getPackageManager().getPackagesForUid(Binder.getCallingUid()); + String currentPkg = callingPackages[0]; + Log.d(Constants.TAG, "currentPkg: " + currentPkg); + + return currentPkg; + } + + /** + * Retrieves AccountSettings from database for the application calling this remote service + * + * @return + */ + protected AccountSettings getAccSettings(String accountName) { + String currentPkg = getCurrentCallingPackage(); + Log.d(Constants.TAG, "accountName: " + accountName); + + Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(currentPkg, accountName); + + AccountSettings settings = ProviderHelper.getApiAccountSettings(this, uri); + + return settings; // can be null! + } + + protected Intent getCreateAccountIntent(Intent data, String accountName) { + String packageName = getCurrentCallingPackage(); + Log.d(Constants.TAG, "accountName: " + accountName); + + Intent intent = new Intent(getBaseContext(), RemoteServiceActivity.class); + intent.setAction(RemoteServiceActivity.ACTION_CREATE_ACCOUNT); + intent.putExtra(RemoteServiceActivity.EXTRA_PACKAGE_NAME, packageName); + intent.putExtra(RemoteServiceActivity.EXTRA_ACC_NAME, accountName); + intent.putExtra(RemoteServiceActivity.EXTRA_DATA, data); + + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + + return result; + } + + /** + * Checks if process that binds to this service (i.e. the package name corresponding to the + * process) is in the list of allowed package names. + * + * @param allowOnlySelf allow only Keychain app itself + * @return true if process is allowed to use this service + * @throws WrongPackageSignatureException + */ + private boolean isCallerAllowed(boolean allowOnlySelf) throws WrongPackageSignatureException { + return isUidAllowed(Binder.getCallingUid(), allowOnlySelf); + } + + private boolean isUidAllowed(int uid, boolean allowOnlySelf) + throws WrongPackageSignatureException { + if (android.os.Process.myUid() == uid) { + return true; + } + if (allowOnlySelf) { // barrier + return false; + } + + String[] callingPackages = getPackageManager().getPackagesForUid(uid); + + // is calling package allowed to use this service? + for (int i = 0; i < callingPackages.length; i++) { + String currentPkg = callingPackages[i]; + + if (isPackageAllowed(currentPkg)) { + return true; + } + } + + Log.d(Constants.TAG, "Uid is NOT allowed!"); + return false; + } + + /** + * Checks if packageName is a registered app for the API. Does not return true for own package! + * + * @param packageName + * @return + * @throws WrongPackageSignatureException + */ + private boolean isPackageAllowed(String packageName) throws WrongPackageSignatureException { + Log.d(Constants.TAG, "isPackageAllowed packageName: " + packageName); + + ArrayList allowedPkgs = ProviderHelper.getRegisteredApiApps(this); + Log.d(Constants.TAG, "allowed: " + allowedPkgs); + + // check if package is allowed to use our service + if (allowedPkgs.contains(packageName)) { + Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName); + + // check package signature + byte[] currentSig; + try { + currentSig = getPackageSignature(packageName); + } catch (NameNotFoundException e) { + throw new WrongPackageSignatureException(e.getMessage()); + } + + byte[] storedSig = ProviderHelper.getApiAppSignature(this, packageName); + if (Arrays.equals(currentSig, storedSig)) { + Log.d(Constants.TAG, + "Package signature is correct! (equals signature from database)"); + return true; + } else { + throw new WrongPackageSignatureException( + "PACKAGE NOT ALLOWED! Signature wrong! (Signature not " + + "equals signature from database)"); + } + } + + Log.d(Constants.TAG, "Package is NOT allowed! packageName: " + packageName); + return false; + } + + @Override + public void onCreate() { + super.onCreate(); + mContext = this; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/WrongPackageSignatureException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/WrongPackageSignatureException.java new file mode 100644 index 000000000..6f44a65e9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/WrongPackageSignatureException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote; + +public class WrongPackageSignatureException extends Exception { + + private static final long serialVersionUID = -8294642703122196028L; + + public WrongPackageSignatureException(String message) { + super(message); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java new file mode 100644 index 000000000..123ed526f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ActionBarHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.remote.AccountSettings; +import org.sufficientlysecure.keychain.util.Log; + +public class AccountSettingsActivity extends ActionBarActivity { + private Uri mAccountUri; + + private AccountSettingsFragment mAccountSettingsFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Inflate a "Done" custom action bar + ActionBarHelper.setOneButtonView(getSupportActionBar(), + R.string.api_settings_save, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // "Done" + save(); + } + }); + + setContentView(R.layout.api_account_settings_activity); + + mAccountSettingsFragment = (AccountSettingsFragment) getSupportFragmentManager().findFragmentById( + R.id.api_account_settings_fragment); + + Intent intent = getIntent(); + mAccountUri = intent.getData(); + if (mAccountUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!"); + finish(); + return; + } else { + Log.d(Constants.TAG, "uri: " + mAccountUri); + loadData(mAccountUri); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.api_account_settings, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_account_settings_delete: + deleteAccount(); + return true; + case R.id.menu_account_settings_cancel: + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void loadData(Uri accountUri) { + AccountSettings settings = ProviderHelper.getApiAccountSettings(this, accountUri); + mAccountSettingsFragment.setAccSettings(settings); + } + + private void deleteAccount() { + if (getContentResolver().delete(mAccountUri, null, null) <= 0) { + throw new RuntimeException(); + } + finish(); + } + + private void save() { + ProviderHelper.updateApiAccount(this, mAccountSettingsFragment.getAccSettings(), mAccountUri); + finish(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java new file mode 100644 index 000000000..0a3ec3c3b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsFragment.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.Spinner; +import android.widget.TextView; + +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.remote.AccountSettings; +import org.sufficientlysecure.keychain.ui.EditKeyActivity; +import org.sufficientlysecure.keychain.ui.SelectSecretKeyLayoutFragment; +import org.sufficientlysecure.keychain.ui.adapter.KeyValueSpinnerAdapter; +import org.sufficientlysecure.keychain.util.AlgorithmNames; + +public class AccountSettingsFragment extends Fragment implements + SelectSecretKeyLayoutFragment.SelectSecretKeyCallback { + + private static final int REQUEST_CODE_CREATE_KEY = 0x00008884; + + // model + private AccountSettings mAccSettings; + + // view + private TextView mAccNameView; + private Spinner mEncryptionAlgorithm; + private Spinner mHashAlgorithm; + private Spinner mCompression; + + private SelectSecretKeyLayoutFragment mSelectKeyFragment; + private BootstrapButton mCreateKeyButton; + + KeyValueSpinnerAdapter mEncryptionAdapter; + KeyValueSpinnerAdapter mHashAdapter; + KeyValueSpinnerAdapter mCompressionAdapter; + + public AccountSettings getAccSettings() { + return mAccSettings; + } + + public void setAccSettings(AccountSettings accountSettings) { + this.mAccSettings = accountSettings; + + mAccNameView.setText(accountSettings.getAccountName()); + mSelectKeyFragment.selectKey(accountSettings.getKeyId()); + mEncryptionAlgorithm.setSelection(mEncryptionAdapter.getPosition(accountSettings + .getEncryptionAlgorithm())); + mHashAlgorithm.setSelection(mHashAdapter.getPosition(accountSettings.getHashAlgorithm())); + mCompression.setSelection(mCompressionAdapter.getPosition(accountSettings.getCompression())); + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.api_account_settings_fragment, container, false); + initView(view); + return view; + } + + /** + * Set error String on key selection + * + * @param error + */ + public void setErrorOnSelectKeyFragment(String error) { + mSelectKeyFragment.setError(error); + } + + private void initView(View view) { + mSelectKeyFragment = (SelectSecretKeyLayoutFragment) getFragmentManager().findFragmentById( + R.id.api_account_settings_select_key_fragment); + mSelectKeyFragment.setCallback(this); + + mAccNameView = (TextView) view.findViewById(R.id.api_account_settings_acc_name); + mEncryptionAlgorithm = (Spinner) view + .findViewById(R.id.api_account_settings_encryption_algorithm); + mHashAlgorithm = (Spinner) view.findViewById(R.id.api_account_settings_hash_algorithm); + mCompression = (Spinner) view.findViewById(R.id.api_account_settings_compression); + mCreateKeyButton = (BootstrapButton) view.findViewById(R.id.api_account_settings_create_key); + + mCreateKeyButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + createKey(); + } + }); + + AlgorithmNames algorithmNames = new AlgorithmNames(getActivity()); + + mEncryptionAdapter = new KeyValueSpinnerAdapter(getActivity(), + algorithmNames.getEncryptionNames()); + mEncryptionAlgorithm.setAdapter(mEncryptionAdapter); + mEncryptionAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mAccSettings.setEncryptionAlgorithm((int) id); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + + mHashAdapter = new KeyValueSpinnerAdapter(getActivity(), algorithmNames.getHashNames()); + mHashAlgorithm.setAdapter(mHashAdapter); + mHashAlgorithm.setOnItemSelectedListener(new OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mAccSettings.setHashAlgorithm((int) id); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + + mCompressionAdapter = new KeyValueSpinnerAdapter(getActivity(), + algorithmNames.getCompressionNames()); + mCompression.setAdapter(mCompressionAdapter); + mCompression.setOnItemSelectedListener(new OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mAccSettings.setCompression((int) id); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + } + + private void createKey() { + Intent intent = new Intent(getActivity(), EditKeyActivity.class); + intent.setAction(EditKeyActivity.ACTION_CREATE_KEY); + intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true); + // set default user id to account name + intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, mAccSettings.getAccountName()); + startActivityForResult(intent, REQUEST_CODE_CREATE_KEY); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_CREATE_KEY: { + if (resultCode == Activity.RESULT_OK) { + // select newly created key + long masterKeyId = ProviderHelper.getMasterKeyId(getActivity(), data.getData()); + mSelectKeyFragment.selectKey(masterKeyId); + } + break; + } + + default: + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + + /** + * callback from select secret key fragment + */ + @Override + public void onKeySelected(long secretKeyId) { + mAccSettings.setKeyId(secretKeyId); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountsListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountsListFragment.java new file mode 100644 index 000000000..4d99e1923 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountsListFragment.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.ui.widget.FixedListView; +import org.sufficientlysecure.keychain.util.Log; + +public class AccountsListFragment extends ListFragment implements + LoaderManager.LoaderCallbacks { + + private static final String ARG_DATA_URI = "uri"; + + // This is the Adapter being used to display the list's data. + AccountsAdapter mAdapter; + + private Uri mDataUri; + + /** + * Creates new instance of this fragment + */ + public static AccountsListFragment newInstance(Uri dataUri) { + AccountsListFragment frag = new AccountsListFragment(); + + Bundle args = new Bundle(); + args.putParcelable(ARG_DATA_URI, dataUri); + + frag.setArguments(args); + + return frag; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View layout = super.onCreateView(inflater, container, + savedInstanceState); + ListView lv = (ListView) layout.findViewById(android.R.id.list); + ViewGroup parent = (ViewGroup) lv.getParent(); + + /* + * http://stackoverflow.com/a/15880684 + * Remove ListView and add FixedListView in its place. + * This is done here programatically to be still able to use the progressBar of ListFragment. + * + * We want FixedListView to be able to put this ListFragment inside a ScrollView + */ + int lvIndex = parent.indexOfChild(lv); + parent.removeViewAt(lvIndex); + FixedListView newLv = new FixedListView(getActivity()); + newLv.setId(android.R.id.list); + parent.addView(newLv, lvIndex, lv.getLayoutParams()); + return layout; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mDataUri = getArguments().getParcelable(ARG_DATA_URI); + + getListView().setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + String selectedAccountName = mAdapter.getItemAccountName(position); + Uri accountUri = mDataUri.buildUpon().appendEncodedPath(selectedAccountName).build(); + Log.d(Constants.TAG, "accountUri: " + accountUri); + + // edit account settings + Intent intent = new Intent(getActivity(), AccountSettingsActivity.class); + intent.setData(accountUri); + startActivity(intent); + } + }); + + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText(getString(R.string.api_settings_accounts_empty)); + + // We have a menu item to show in action bar. + setHasOptionsMenu(true); + + // Create an empty adapter we will use to display the loaded data. + mAdapter = new AccountsAdapter(getActivity(), null, 0); + setListAdapter(mAdapter); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); + } + + // These are the Contacts rows that we will retrieve. + static final String[] PROJECTION = new String[]{ + KeychainContract.ApiAccounts._ID, // 0 + KeychainContract.ApiAccounts.ACCOUNT_NAME // 1 + }; + + public Loader 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. + + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(getActivity(), mDataUri, PROJECTION, null, null, + KeychainContract.ApiAccounts.ACCOUNT_NAME + " COLLATE LOCALIZED ASC"); + } + + public void onLoadFinished(Loader loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.swapCursor(data); + } + + public void onLoaderReset(Loader 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); + } + + private class AccountsAdapter extends CursorAdapter { + private LayoutInflater mInflater; + + public AccountsAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + } + + /** + * Similar to CursorAdapter.getItemId(). + * Required to build Uris for api accounts, which are not based on row ids + * + * @param position + * @return + */ + public String getItemAccountName(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getString(1); + } else { + return null; + } + } else { + return null; + } + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView text = (TextView) view.findViewById(R.id.api_accounts_adapter_item_name); + + String accountName = cursor.getString(1); + text.setText(accountName); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.api_accounts_adapter_list_item, null); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java new file mode 100644 index 000000000..818c296c1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui; + +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.util.Log; + +public class AppSettingsActivity extends ActionBarActivity { + private Uri mAppUri; + + private AppSettingsFragment mSettingsFragment; + private AccountsListFragment mAccountsListFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // let the actionbar look like Android's contact app + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setIcon(android.R.color.transparent); + actionBar.setHomeButtonEnabled(true); + + setContentView(R.layout.api_app_settings_activity); + + mSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById( + R.id.api_app_settings_fragment); + + Intent intent = getIntent(); + mAppUri = intent.getData(); + if (mAppUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!"); + finish(); + return; + } else { + Log.d(Constants.TAG, "uri: " + mAppUri); + loadData(savedInstanceState, mAppUri); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.api_app_settings, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_api_settings_revoke: + revokeAccess(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void loadData(Bundle savedInstanceState, Uri appUri) { + AppSettings settings = ProviderHelper.getApiAppSettings(this, appUri); + mSettingsFragment.setAppSettings(settings); + + String appName; + PackageManager pm = getPackageManager(); + try { + ApplicationInfo ai = pm.getApplicationInfo(settings.getPackageName(), 0); + appName = (String) pm.getApplicationLabel(ai); + } catch (PackageManager.NameNotFoundException e) { + // fallback + appName = settings.getPackageName(); + } + setTitle(appName); + + Uri accountsUri = appUri.buildUpon().appendPath(KeychainContract.PATH_ACCOUNTS).build(); + Log.d(Constants.TAG, "accountsUri: " + accountsUri); + startListFragment(savedInstanceState, accountsUri); + } + + private void startListFragment(Bundle savedInstanceState, Uri dataUri) { + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; + } + + // Create an instance of the fragment + mAccountsListFragment = AccountsListFragment.newInstance(dataUri); + + // Add the fragment to the 'fragment_container' FrameLayout + // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + getSupportFragmentManager().beginTransaction() + .replace(R.id.api_accounts_list_fragment, mAccountsListFragment) + .commitAllowingStateLoss(); + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); + } + + private void revokeAccess() { + if (getContentResolver().delete(mAppUri, null, null) <= 0) { + throw new RuntimeException(); + } + finish(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsFragment.java new file mode 100644 index 000000000..a6db02708 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsFragment.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.spongycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.util.Log; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class AppSettingsFragment extends Fragment { + + // model + private AppSettings mAppSettings; + + // view + private TextView mAppNameView; + private ImageView mAppIconView; + private TextView mPackageName; + private TextView mPackageSignature; + + public AppSettings getAppSettings() { + return mAppSettings; + } + + public void setAppSettings(AppSettings appSettings) { + this.mAppSettings = appSettings; + updateView(appSettings); + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.api_app_settings_fragment, container, false); + mAppNameView = (TextView) view.findViewById(R.id.api_app_settings_app_name); + mAppIconView = (ImageView) view.findViewById(R.id.api_app_settings_app_icon); + mPackageName = (TextView) view.findViewById(R.id.api_app_settings_package_name); + mPackageSignature = (TextView) view.findViewById(R.id.api_app_settings_package_signature); + return view; + } + + private void updateView(AppSettings appSettings) { + // get application name and icon from package manager + String appName; + Drawable appIcon = null; + PackageManager pm = getActivity().getApplicationContext().getPackageManager(); + try { + ApplicationInfo ai = pm.getApplicationInfo(appSettings.getPackageName(), 0); + + appName = (String) pm.getApplicationLabel(ai); + appIcon = pm.getApplicationIcon(ai); + } catch (NameNotFoundException e) { + // fallback + appName = appSettings.getPackageName(); + } + mAppNameView.setText(appName); + mAppIconView.setImageDrawable(appIcon); + + // advanced info: package name + mPackageName.setText(appSettings.getPackageName()); + + // advanced info: package signature SHA-256 + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(appSettings.getPackageSignature()); + byte[] digest = md.digest(); + String signature = new String(Hex.encode(digest)); + + mPackageSignature.setText(signature); + } catch (NoSuchAlgorithmException e) { + Log.e(Constants.TAG, "Should not happen!", e); + } + } + + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListActivity.java new file mode 100644 index 000000000..f86d279f0 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListActivity.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui; + +import android.os.Bundle; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.DrawerActivity; + +public class AppsListActivity extends DrawerActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.api_apps_list_activity); + + setupDrawerNavigation(savedInstanceState); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java new file mode 100644 index 000000000..9d0e6d3ef --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; + +public class AppsListFragment extends ListFragment implements + LoaderManager.LoaderCallbacks { + + // This is the Adapter being used to display the list's data. + RegisteredAppsAdapter mAdapter; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + getListView().setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + String selectedPackageName = mAdapter.getItemPackageName(position); + // edit app settings + Intent intent = new Intent(getActivity(), AppSettingsActivity.class); + intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(selectedPackageName)); + startActivity(intent); + } + }); + + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText(getString(R.string.api_no_apps)); + + // We have a menu item to show in action bar. + setHasOptionsMenu(true); + + // Create an empty adapter we will use to display the loaded data. + mAdapter = new RegisteredAppsAdapter(getActivity(), null, 0); + setListAdapter(mAdapter); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); + } + + // These are the Contacts rows that we will retrieve. + static final String[] PROJECTION = new String[]{ + ApiApps._ID, // 0 + ApiApps.PACKAGE_NAME // 1 + }; + + public Loader 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. + // First, pick the base URI to use depending on whether we are + // currently filtering. + Uri baseUri = ApiApps.CONTENT_URI; + + // 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, + ApiApps.PACKAGE_NAME + " COLLATE LOCALIZED ASC"); + } + + public void onLoadFinished(Loader loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.swapCursor(data); + } + + public void onLoaderReset(Loader 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); + } + + private class RegisteredAppsAdapter extends CursorAdapter { + + private LayoutInflater mInflater; + private PackageManager mPM; + + public RegisteredAppsAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + mPM = context.getApplicationContext().getPackageManager(); + } + + /** + * Similar to CursorAdapter.getItemId(). + * Required to build Uris for api apps, which are not based on row ids + * + * @param position + * @return + */ + public String getItemPackageName(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getString(1); + } else { + return null; + } + } else { + return null; + } + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView text = (TextView) view.findViewById(R.id.api_apps_adapter_item_name); + ImageView icon = (ImageView) view.findViewById(R.id.api_apps_adapter_item_icon); + + String packageName = cursor.getString(cursor.getColumnIndex(ApiApps.PACKAGE_NAME)); + if (packageName != null) { + // get application name + try { + ApplicationInfo ai = mPM.getApplicationInfo(packageName, 0); + + text.setText(mPM.getApplicationLabel(ai)); + icon.setImageDrawable(mPM.getApplicationIcon(ai)); + } catch (final PackageManager.NameNotFoundException e) { + // fallback + text.setText(packageName); + } + } else { + // fallback + text.setText(packageName); + } + + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.api_apps_adapter_list_item, null); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java new file mode 100644 index 000000000..ab95f2691 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.remote.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v7.app.ActionBarActivity; +import android.view.View; + +import org.openintents.openpgp.util.OpenPgpApi; +import org.sufficientlysecure.htmltextview.HtmlTextView; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ActionBarHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.remote.AccountSettings; +import org.sufficientlysecure.keychain.remote.AppSettings; +import org.sufficientlysecure.keychain.ui.SelectPublicKeyFragment; +import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; + +public class RemoteServiceActivity extends ActionBarActivity { + + public static final String ACTION_REGISTER = Constants.INTENT_PREFIX + "API_ACTIVITY_REGISTER"; + public static final String ACTION_CREATE_ACCOUNT = Constants.INTENT_PREFIX + + "API_ACTIVITY_CREATE_ACCOUNT"; + public static final String ACTION_CACHE_PASSPHRASE = Constants.INTENT_PREFIX + + "API_ACTIVITY_CACHE_PASSPHRASE"; + public static final String ACTION_SELECT_PUB_KEYS = Constants.INTENT_PREFIX + + "API_ACTIVITY_SELECT_PUB_KEYS"; + public static final String ACTION_ERROR_MESSAGE = Constants.INTENT_PREFIX + + "API_ACTIVITY_ERROR_MESSAGE"; + + public static final String EXTRA_MESSENGER = "messenger"; + + public static final String EXTRA_DATA = "data"; + + // passphrase action + public static final String EXTRA_SECRET_KEY_ID = "secret_key_id"; + // register action + public static final String EXTRA_PACKAGE_NAME = "package_name"; + public static final String EXTRA_PACKAGE_SIGNATURE = "package_signature"; + // create acc action + public static final String EXTRA_ACC_NAME = "acc_name"; + // select pub keys action + public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids"; + public static final String EXTRA_MISSING_USER_IDS = "missing_user_ids"; + public static final String EXTRA_DUBLICATE_USER_IDS = "dublicate_user_ids"; + // error message + public static final String EXTRA_ERROR_MESSAGE = "error_message"; + + // register view + private AppSettingsFragment mAppSettingsFragment; + // create acc view + private AccountSettingsFragment mAccSettingsFragment; + // select pub keys view + private SelectPublicKeyFragment mSelectFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + handleActions(getIntent(), savedInstanceState); + } + + protected void handleActions(Intent intent, Bundle savedInstanceState) { + + String action = intent.getAction(); + final Bundle extras = intent.getExtras(); + + + if (ACTION_REGISTER.equals(action)) { + final String packageName = extras.getString(EXTRA_PACKAGE_NAME); + final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE); + Log.d(Constants.TAG, "ACTION_REGISTER packageName: " + packageName); + + // Inflate a "Done"/"Cancel" custom action bar view + ActionBarHelper.setTwoButtonView(getSupportActionBar(), + R.string.api_register_allow, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Allow + + ProviderHelper.insertApiApp(RemoteServiceActivity.this, + mAppSettingsFragment.getAppSettings()); + + // give data through for new service call + Intent resultData = extras.getParcelable(EXTRA_DATA); + RemoteServiceActivity.this.setResult(RESULT_OK, resultData); + RemoteServiceActivity.this.finish(); + } + }, R.string.api_register_disallow, R.drawable.ic_action_cancel, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Disallow + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + RemoteServiceActivity.this.finish(); + } + } + ); + + setContentView(R.layout.api_remote_register_app); + + mAppSettingsFragment = (AppSettingsFragment) getSupportFragmentManager().findFragmentById( + R.id.api_app_settings_fragment); + + AppSettings settings = new AppSettings(packageName, packageSignature); + mAppSettingsFragment.setAppSettings(settings); + } else if (ACTION_CREATE_ACCOUNT.equals(action)) { + final String packageName = extras.getString(EXTRA_PACKAGE_NAME); + final String accName = extras.getString(EXTRA_ACC_NAME); + + // Inflate a "Done"/"Cancel" custom action bar view + ActionBarHelper.setTwoButtonView(getSupportActionBar(), + R.string.api_settings_save, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Save + + // user needs to select a key! + if (mAccSettingsFragment.getAccSettings().getKeyId() == Id.key.none) { + mAccSettingsFragment.setErrorOnSelectKeyFragment( + getString(R.string.api_register_error_select_key)); + } else { + ProviderHelper.insertApiAccount(RemoteServiceActivity.this, + KeychainContract.ApiAccounts.buildBaseUri(packageName), + mAccSettingsFragment.getAccSettings()); + + // give data through for new service call + Intent resultData = extras.getParcelable(EXTRA_DATA); + RemoteServiceActivity.this.setResult(RESULT_OK, resultData); + RemoteServiceActivity.this.finish(); + } + } + }, R.string.api_settings_cancel, R.drawable.ic_action_cancel, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Cancel + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + RemoteServiceActivity.this.finish(); + } + } + ); + + setContentView(R.layout.api_remote_create_account); + + mAccSettingsFragment = (AccountSettingsFragment) getSupportFragmentManager().findFragmentById( + R.id.api_account_settings_fragment); + + AccountSettings settings = new AccountSettings(accName); + mAccSettingsFragment.setAccSettings(settings); + } else if (ACTION_CACHE_PASSPHRASE.equals(action)) { + long secretKeyId = extras.getLong(EXTRA_SECRET_KEY_ID); + final Intent resultData = extras.getParcelable(EXTRA_DATA); + + PassphraseDialogFragment.show(this, secretKeyId, + new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + // return given params again, for calling the service method again + RemoteServiceActivity.this.setResult(RESULT_OK, resultData); + } else { + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + } + + RemoteServiceActivity.this.finish(); + } + }); + + } else if (ACTION_SELECT_PUB_KEYS.equals(action)) { + long[] selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS); + ArrayList missingUserIds = intent + .getStringArrayListExtra(EXTRA_MISSING_USER_IDS); + ArrayList dublicateUserIds = intent + .getStringArrayListExtra(EXTRA_DUBLICATE_USER_IDS); + + // TODO: do this with spannable instead of HTML to prevent parsing failures with weird user ids + String text = "" + getString(R.string.api_select_pub_keys_text) + ""; + text += "

"; + if (missingUserIds != null && missingUserIds.size() > 0) { + text += getString(R.string.api_select_pub_keys_missing_text); + text += "
"; + text += "
    "; + for (String userId : missingUserIds) { + text += "
  • " + userId + "
  • "; + } + text += "
"; + text += "
"; + } + if (dublicateUserIds != null && dublicateUserIds.size() > 0) { + text += getString(R.string.api_select_pub_keys_dublicates_text); + text += "
"; + text += "
    "; + for (String userId : dublicateUserIds) { + text += "
  • " + userId + "
  • "; + } + text += "
"; + } + + // Inflate a "Done"/"Cancel" custom action bar view + ActionBarHelper.setTwoButtonView(getSupportActionBar(), + R.string.btn_okay, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // add key ids to params Bundle for new request + Intent resultData = extras.getParcelable(EXTRA_DATA); + resultData.putExtra(OpenPgpApi.EXTRA_KEY_IDS, + mSelectFragment.getSelectedMasterKeyIds()); + + RemoteServiceActivity.this.setResult(RESULT_OK, resultData); + RemoteServiceActivity.this.finish(); + } + }, R.string.btn_do_not_save, R.drawable.ic_action_cancel, new View.OnClickListener() { + @Override + public void onClick(View v) { + // cancel + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + RemoteServiceActivity.this.finish(); + } + } + ); + + setContentView(R.layout.api_remote_select_pub_keys); + + // set text on view + HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_select_pub_keys_text); + textView.setHtmlFromString(text); + + /* Load select pub keys fragment */ + // Check that the activity is using the layout version with + // the fragment_container FrameLayout + if (findViewById(R.id.api_select_pub_keys_fragment_container) != null) { + + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; + } + + // Create an instance of the fragment + mSelectFragment = SelectPublicKeyFragment.newInstance(selectedMasterKeyIds); + + // Add the fragment to the 'fragment_container' FrameLayout + getSupportFragmentManager().beginTransaction() + .add(R.id.api_select_pub_keys_fragment_container, mSelectFragment).commit(); + } + } else if (ACTION_ERROR_MESSAGE.equals(action)) { + String errorMessage = intent.getStringExtra(EXTRA_ERROR_MESSAGE); + + String text = "" + errorMessage + ""; + + // Inflate a "Done" custom action bar view + ActionBarHelper.setOneButtonView(getSupportActionBar(), + R.string.btn_okay, R.drawable.ic_action_done, + new View.OnClickListener() { + + @Override + public void onClick(View v) { + RemoteServiceActivity.this.setResult(RESULT_CANCELED); + RemoteServiceActivity.this.finish(); + } + }); + + setContentView(R.layout.api_remote_error_message); + + // set text on view + HtmlTextView textView = (HtmlTextView) findViewById(R.id.api_app_error_message_text); + textView.setHtmlFromString(text); + } else { + Log.e(Constants.TAG, "Action does not exist!"); + setResult(RESULT_CANCELED); + finish(); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java new file mode 100644 index 000000000..1c6aa7971 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -0,0 +1,905 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.service; + +import android.app.IntentService; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; + +import org.spongycastle.bcpg.sig.KeyFlags; +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPObjectFactory; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.spongycastle.openpgp.PGPUtil; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.pgp.PgpConversionHelper; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.PgpHelper; +import org.sufficientlysecure.keychain.pgp.PgpImportExport; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; +import org.sufficientlysecure.keychain.pgp.PgpSignEncrypt; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralMsgIdException; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry; +import org.sufficientlysecure.keychain.util.HkpKeyServer; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.KeychainServiceListener; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ProgressDialogUpdater; +import org.sufficientlysecure.keychain.util.ProgressScaler; + +import java.io.BufferedInputStream; +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 java.util.List; + +/** + * 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, KeychainServiceListener { + + /* extras that can be given by intent */ + public static final String EXTRA_MESSENGER = "messenger"; + public static final String EXTRA_DATA = "data"; + + /* possible actions */ + public static final String ACTION_ENCRYPT_SIGN = Constants.INTENT_PREFIX + "ENCRYPT_SIGN"; + + public static final String ACTION_DECRYPT_VERIFY = Constants.INTENT_PREFIX + "DECRYPT_VERIFY"; + + public static final String ACTION_SAVE_KEYRING = Constants.INTENT_PREFIX + "SAVE_KEYRING"; + public static final String ACTION_GENERATE_KEY = Constants.INTENT_PREFIX + "GENERATE_KEY"; + public static final String ACTION_GENERATE_DEFAULT_RSA_KEYS = Constants.INTENT_PREFIX + + "GENERATE_DEFAULT_RSA_KEYS"; + + public static final String ACTION_DELETE_FILE_SECURELY = Constants.INTENT_PREFIX + + "DELETE_FILE_SECURELY"; + + public static final String ACTION_IMPORT_KEYRING = Constants.INTENT_PREFIX + "IMPORT_KEYRING"; + public static final String ACTION_EXPORT_KEYRING = Constants.INTENT_PREFIX + "EXPORT_KEYRING"; + + public static final String ACTION_UPLOAD_KEYRING = Constants.INTENT_PREFIX + "UPLOAD_KEYRING"; + public static final String ACTION_DOWNLOAD_AND_IMPORT_KEYS = Constants.INTENT_PREFIX + "QUERY_KEYRING"; + + public static final String ACTION_CERTIFY_KEYRING = Constants.INTENT_PREFIX + "SIGN_KEYRING"; + + /* 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_URI = 2; + + // encrypt + public static final String ENCRYPT_SIGNATURE_KEY_ID = "secret_key_id"; + public static final String ENCRYPT_USE_ASCII_ARMOR = "use_ascii_armor"; + public static final String ENCRYPT_ENCRYPTION_KEYS_IDS = "encryption_keys_ids"; + public static final String ENCRYPT_COMPRESSION_ID = "compression_id"; + public static final String ENCRYPT_MESSAGE_BYTES = "message_bytes"; + public static final String ENCRYPT_INPUT_FILE = "input_file"; + public static final String ENCRYPT_OUTPUT_FILE = "output_file"; + public static final String ENCRYPT_SYMMETRIC_PASSPHRASE = "passphrase"; + + // decrypt/verify + public static final String DECRYPT_CIPHERTEXT_BYTES = "ciphertext_bytes"; + public static final String DECRYPT_PASSPHRASE = "passphrase"; + + // save keyring + public static final String SAVE_KEYRING_PARCEL = "save_parcel"; + public static final String SAVE_KEYRING_CAN_SIGN = "can_sign"; + + + // generate key + public static final String GENERATE_KEY_ALGORITHM = "algorithm"; + public static final String GENERATE_KEY_KEY_SIZE = "key_size"; + public static final String GENERATE_KEY_SYMMETRIC_PASSPHRASE = "passphrase"; + public static final String GENERATE_KEY_MASTER_KEY = "master_key"; + + // delete file securely + public static final String DELETE_FILE = "deleteFile"; + + // import key + public static final String IMPORT_KEY_LIST = "import_key_list"; + + // export key + public static final String EXPORT_OUTPUT_STREAM = "export_output_stream"; + public static final String EXPORT_FILENAME = "export_filename"; + public static final String EXPORT_SECRET = "export_secret"; + public static final String EXPORT_ALL = "export_all"; + public static final String EXPORT_KEY_RING_MASTER_KEY_ID = "export_key_ring_id"; + + // upload key + public static final String UPLOAD_KEY_SERVER = "upload_key_server"; + + // query key + public static final String DOWNLOAD_KEY_SERVER = "query_key_server"; + public static final String DOWNLOAD_KEY_LIST = "query_key_id"; + + // sign key + public static final String CERTIFY_KEY_MASTER_KEY_ID = "sign_key_master_key_id"; + public static final String CERTIFY_KEY_PUB_KEY_ID = "sign_key_pub_key_id"; + public static final String CERTIFY_KEY_UIDS = "sign_key_uids"; + + /* + * possible data keys as result send over messenger + */ + // keys + public static final String RESULT_NEW_KEY = "new_key"; + public static final String RESULT_KEY_USAGES = "new_key_usages"; + + // encrypt + public static final String RESULT_BYTES = "encrypted_data"; + + // decrypt/verify + public static final String RESULT_DECRYPTED_BYTES = "decrypted_data"; + public static final String RESULT_DECRYPT_VERIFY_RESULT = "signature"; + + // 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"; + + Messenger mMessenger; + + private boolean mIsCanceled; + + public KeychainIntentService() { + super("KeychainIntentService"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + this.mIsCanceled = true; + } + + /** + * 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) || (intent + .getAction() == null))) { + Log.e(Constants.TAG, + "Extra bundle must contain a messenger, a data bundle, and an action!"); + return; + } + + Uri dataUri = intent.getData(); + + mMessenger = (Messenger) extras.get(EXTRA_MESSENGER); + Bundle data = extras.getBundle(EXTRA_DATA); + + OtherHelper.logDebugBundle(data, "EXTRA_DATA"); + + String action = intent.getAction(); + + // executeServiceMethod action from extra bundle + if (ACTION_ENCRYPT_SIGN.equals(action)) { + try { + /* Input */ + int target = data.getInt(TARGET); + + long signatureKeyId = data.getLong(ENCRYPT_SIGNATURE_KEY_ID); + String symmetricPassphrase = data.getString(ENCRYPT_SYMMETRIC_PASSPHRASE); + + boolean useAsciiArmor = data.getBoolean(ENCRYPT_USE_ASCII_ARMOR); + long encryptionKeyIds[] = data.getLongArray(ENCRYPT_ENCRYPTION_KEYS_IDS); + int compressionId = data.getInt(ENCRYPT_COMPRESSION_ID); + InputStream inStream; + long inLength; + InputData inputData; + OutputStream outStream; +// 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_URI: /* 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_external_storage_not_ready)); + } + + inStream = new FileInputStream(inputFile); + File file = new File(inputFile); + inLength = file.length(); + inputData = new InputData(inStream, inLength); + + outStream = new FileOutputStream(outputFile); + + break; + + // TODO: not used currently +// case TARGET_STREAM: /* Encrypting stream from content uri */ +// Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI); +// +// // InputStream +// InputStream in = getContentResolver().openInputStream(providerUri); +// inLength = PgpHelper.getLengthOfStream(in); +// inputData = new InputData(in, inLength); +// +// // OutputStream +// try { +// while (true) { +// streamFilename = PgpHelper.generateRandomFilename(32); +// if (streamFilename == null) { +// throw new 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 PgpGeneralException("No target choosen!"); + + } + + /* Operation */ + PgpSignEncrypt.Builder builder = + new PgpSignEncrypt.Builder(this, inputData, outStream); + builder.progress(this); + + builder.enableAsciiArmorOutput(useAsciiArmor) + .compressionId(compressionId) + .symmetricEncryptionAlgorithm( + Preferences.getPreferences(this).getDefaultEncryptionAlgorithm()) + .signatureForceV3(Preferences.getPreferences(this).getForceV3Signatures()) + .encryptionKeyIds(encryptionKeyIds) + .symmetricPassphrase(symmetricPassphrase) + .signatureKeyId(signatureKeyId) + .signatureHashAlgorithm( + Preferences.getPreferences(this).getDefaultHashAlgorithm()) + .signaturePassphrase( + PassphraseCacheService.getCachedPassphrase(this, signatureKeyId)); + + builder.build().execute(); + + outStream.close(); + + /* Output */ + + Bundle resultData = new Bundle(); + + switch (target) { + case TARGET_BYTES: + byte output[] = ((ByteArrayOutputStream) outStream).toByteArray(); + + resultData.putByteArray(RESULT_BYTES, output); + + break; + case TARGET_URI: + // 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); + } + } else if (ACTION_DECRYPT_VERIFY.equals(action)) { + try { + /* Input */ + int target = data.getInt(TARGET); + + byte[] bytes = data.getByteArray(DECRYPT_CIPHERTEXT_BYTES); + String passphrase = data.getString(DECRYPT_PASSPHRASE); + + InputStream inStream; + long inLength; + InputData inputData; + OutputStream outStream; + 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_URI: /* 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_external_storage_not_ready)); + } + + // 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; + + // TODO: not used, maybe contains code useful for new decrypt method for files? +// case TARGET_STREAM: /* decrypting stream from content uri */ +// Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI); +// +// // InputStream +// InputStream in = getContentResolver().openInputStream(providerUri); +// inLength = PgpHelper.getLengthOfStream(in); +// inputData = new InputData(in, inLength); +// +// // OutputStream +// try { +// while (true) { +// streamFilename = PgpHelper.generateRandomFilename(32); +// if (streamFilename == null) { +// throw new 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 PgpGeneralException("No target choosen!"); + + } + + /* Operation */ + + Bundle resultData = new Bundle(); + + // verifyText and decrypt returning additional resultData values for the + // verification of signatures + PgpDecryptVerify.Builder builder = new PgpDecryptVerify.Builder(this, inputData, outStream); + builder.progressDialogUpdater(this); + + builder.allowSymmetricDecryption(true) + .passphrase(passphrase); + + PgpDecryptVerifyResult decryptVerifyResult = builder.build().execute(); + + outStream.close(); + + resultData.putParcelable(RESULT_DECRYPT_VERIFY_RESULT, decryptVerifyResult); + + /* Output */ + + switch (target) { + case TARGET_BYTES: + byte output[] = ((ByteArrayOutputStream) outStream).toByteArray(); + resultData.putByteArray(RESULT_DECRYPTED_BYTES, output); + break; + case TARGET_URI: + // 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); + } + } else if (ACTION_SAVE_KEYRING.equals(action)) { + try { + /* Input */ + SaveKeyringParcel saveParams = data.getParcelable(SAVE_KEYRING_PARCEL); + String oldPassphrase = saveParams.oldPassphrase; + String newPassphrase = saveParams.newPassphrase; + boolean canSign = true; + + if (data.containsKey(SAVE_KEYRING_CAN_SIGN)) { + canSign = data.getBoolean(SAVE_KEYRING_CAN_SIGN); + } + + if (newPassphrase == null) { + newPassphrase = oldPassphrase; + } + + long masterKeyId = saveParams.keys.get(0).getKeyID(); + + /* Operation */ + if (!canSign) { + PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler(this, 0, 50, 100)); + PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRing(this, masterKeyId); + keyRing = keyOperations.changeSecretKeyPassphrase(keyRing, + oldPassphrase, newPassphrase); + setProgress(R.string.progress_saving_key_ring, 50, 100); + ProviderHelper.saveKeyRing(this, keyRing); + setProgress(R.string.progress_done, 100, 100); + } else { + PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler(this, 0, 90, 100)); + PGPSecretKeyRing privkey = ProviderHelper.getPGPSecretKeyRing(this, masterKeyId); + PGPPublicKeyRing pubkey = ProviderHelper.getPGPPublicKeyRing(this, masterKeyId); + PgpKeyOperation.Pair pair = + keyOperations.buildSecretKey(privkey, pubkey, saveParams); + setProgress(R.string.progress_saving_key_ring, 90, 100); + // save the pair + ProviderHelper.saveKeyRing(this, pair.second, pair.first); + setProgress(R.string.progress_done, 100, 100); + } + PassphraseCacheService.addCachedPassphrase(this, masterKeyId, newPassphrase); + + /* Output */ + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY); + } catch (Exception e) { + sendErrorToHandler(e); + } + } else if (ACTION_GENERATE_KEY.equals(action)) { + 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); + boolean masterKey = data.getBoolean(GENERATE_KEY_MASTER_KEY); + + /* Operation */ + PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler(this, 0, 100, 100)); + PGPSecretKey newKey = keyOperations.createKey(algorithm, keysize, + passphrase, masterKey); + + /* Output */ + Bundle resultData = new Bundle(); + resultData.putByteArray(RESULT_NEW_KEY, + PgpConversionHelper.PGPSecretKeyToBytes(newKey)); + + OtherHelper.logDebugBundle(resultData, "resultData"); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + } else if (ACTION_GENERATE_DEFAULT_RSA_KEYS.equals(action)) { + // generate one RSA 4096 key for signing and one subkey for encrypting! + try { + /* Input */ + String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE); + ArrayList newKeys = new ArrayList(); + ArrayList keyUsageList = new ArrayList(); + + /* Operation */ + int keysTotal = 3; + int keysCreated = 0; + setProgress( + getApplicationContext().getResources(). + getQuantityString(R.plurals.progress_generating, keysTotal), + keysCreated, + keysTotal); + PgpKeyOperation keyOperations = new PgpKeyOperation(new ProgressScaler(this, 0, 100, 100)); + + PGPSecretKey masterKey = keyOperations.createKey(Id.choice.algorithm.rsa, + 4096, passphrase, true); + newKeys.add(masterKey); + keyUsageList.add(KeyFlags.CERTIFY_OTHER); + keysCreated++; + setProgress(keysCreated, keysTotal); + + PGPSecretKey subKey = keyOperations.createKey(Id.choice.algorithm.rsa, + 4096, passphrase, false); + newKeys.add(subKey); + keyUsageList.add(KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE); + keysCreated++; + setProgress(keysCreated, keysTotal); + + subKey = keyOperations.createKey(Id.choice.algorithm.rsa, + 4096, passphrase, false); + newKeys.add(subKey); + keyUsageList.add(KeyFlags.SIGN_DATA); + keysCreated++; + setProgress(keysCreated, keysTotal); + + // TODO: default to one master for cert, one sub for encrypt and one sub + // for sign + + /* Output */ + + Bundle resultData = new Bundle(); + resultData.putByteArray(RESULT_NEW_KEY, + PgpConversionHelper.PGPSecretKeyArrayListToBytes(newKeys)); + resultData.putIntegerArrayList(RESULT_KEY_USAGES, keyUsageList); + + OtherHelper.logDebugBundle(resultData, "resultData"); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + } else if (ACTION_DELETE_FILE_SECURELY.equals(action)) { + try { + /* Input */ + String deleteFile = data.getString(DELETE_FILE); + + /* Operation */ + try { + PgpHelper.deleteFileSecurely(this, this, new File(deleteFile)); + } catch (FileNotFoundException e) { + throw new PgpGeneralException( + getString(R.string.error_file_not_found, deleteFile)); + } catch (IOException e) { + throw new PgpGeneralException(getString(R.string.error_file_delete_failed, + deleteFile)); + } + + /* Output */ + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY); + } catch (Exception e) { + sendErrorToHandler(e); + } + } else if (ACTION_IMPORT_KEYRING.equals(action)) { + try { + List entries = data.getParcelableArrayList(IMPORT_KEY_LIST); + + Bundle resultData = new Bundle(); + + PgpImportExport pgpImportExport = new PgpImportExport(this, this); + resultData = pgpImportExport.importKeyRings(entries); + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + } else if (ACTION_EXPORT_KEYRING.equals(action)) { + try { + + boolean exportSecret = data.getBoolean(EXPORT_SECRET, false); + long[] masterKeyIds = data.getLongArray(EXPORT_KEY_RING_MASTER_KEY_ID); + String outputFile = data.getString(EXPORT_FILENAME); + + // If not exporting all keys get the masterKeyIds of the keys to export from the intent + boolean exportAll = data.getBoolean(EXPORT_ALL); + + // check if storage is ready + if (!FileHelper.isStorageMounted(outputFile)) { + throw new PgpGeneralException(getString(R.string.error_external_storage_not_ready)); + } + + ArrayList publicMasterKeyIds = new ArrayList(); + ArrayList secretMasterKeyIds = new ArrayList(); + + String selection = null; + if(!exportAll) { + selection = KeychainDatabase.Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + " IN( "; + for(long l : masterKeyIds) { + selection += Long.toString(l) + ","; + } + selection = selection.substring(0, selection.length()-1) + " )"; + } + + Cursor cursor = getContentResolver().query(KeyRings.buildUnifiedKeyRingsUri(), + new String[]{ KeyRings.MASTER_KEY_ID, KeyRings.HAS_SECRET }, + selection, null, null); + try { + cursor.moveToFirst(); + do { + // export public either way + publicMasterKeyIds.add(cursor.getLong(0)); + // add secret if available (and requested) + if(exportSecret && cursor.getInt(1) != 0) + secretMasterKeyIds.add(cursor.getLong(0)); + } while(cursor.moveToNext()); + } finally { + cursor.close(); + } + + PgpImportExport pgpImportExport = new PgpImportExport(this, this, this); + Bundle resultData = pgpImportExport + .exportKeyRings(publicMasterKeyIds, secretMasterKeyIds, + new FileOutputStream(outputFile)); + + if (mIsCanceled) { + boolean isDeleted = new File(outputFile).delete(); + } + + sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData); + } catch (Exception e) { + sendErrorToHandler(e); + } + } else if (ACTION_UPLOAD_KEYRING.equals(action)) { + try { + + /* Input */ + String keyServer = data.getString(UPLOAD_KEY_SERVER); + // and dataUri! + + /* Operation */ + HkpKeyServer server = new HkpKeyServer(keyServer); + + PGPPublicKeyRing keyring = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri); + if (keyring != null) { + PgpImportExport pgpImportExport = new PgpImportExport(this, null); + + boolean uploaded = pgpImportExport.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); + } + } else if (ACTION_DOWNLOAD_AND_IMPORT_KEYS.equals(action)) { + try { + ArrayList entries = data.getParcelableArrayList(DOWNLOAD_KEY_LIST); + String keyServer = data.getString(DOWNLOAD_KEY_SERVER); + + // this downloads the keys and places them into the ImportKeysListEntry entries + HkpKeyServer server = new HkpKeyServer(keyServer); + + for (ImportKeysListEntry entry : entries) { + // if available use complete fingerprint for get request + byte[] downloadedKeyBytes; + if (entry.getFingerPrintHex() != null) { + downloadedKeyBytes = server.get("0x" + entry.getFingerPrintHex()).getBytes(); + } else { + downloadedKeyBytes = server.get(entry.getKeyIdHex()).getBytes(); + } + + // create PGPKeyRing object based on downloaded armored key + PGPKeyRing downloadedKey = null; + BufferedInputStream bufferedInput = + new BufferedInputStream(new ByteArrayInputStream(downloadedKeyBytes)); + if (bufferedInput.available() > 0) { + InputStream in = PGPUtil.getDecoderStream(bufferedInput); + PGPObjectFactory objectFactory = new PGPObjectFactory(in); + + // get first object in block + Object obj; + if ((obj = objectFactory.nextObject()) != null) { + Log.d(Constants.TAG, "Found class: " + obj.getClass()); + + if (obj instanceof PGPKeyRing) { + downloadedKey = (PGPKeyRing) obj; + } else { + throw new PgpGeneralException("Object not recognized as PGPKeyRing!"); + } + } + } + + // verify downloaded key by comparing fingerprints + if (entry.getFingerPrintHex() != null) { + String downloadedKeyFp = PgpKeyHelper.convertFingerprintToHex( + downloadedKey.getPublicKey().getFingerprint()); + if (downloadedKeyFp.equals(entry.getFingerPrintHex())) { + Log.d(Constants.TAG, "fingerprint of downloaded key is the same as " + + "the requested fingerprint!"); + } else { + throw new PgpGeneralException("fingerprint of downloaded key is " + + "NOT the same as the requested fingerprint!"); + } + } + + // save key bytes in entry object for doing the + // actual import afterwards + entry.setBytes(downloadedKey.getEncoded()); + } + + + Intent importIntent = new Intent(this, KeychainIntentService.class); + importIntent.setAction(ACTION_IMPORT_KEYRING); + Bundle importData = new Bundle(); + importData.putParcelableArrayList(IMPORT_KEY_LIST, entries); + importIntent.putExtra(EXTRA_DATA, importData); + importIntent.putExtra(EXTRA_MESSENGER, mMessenger); + + // now import it with this service + onHandleIntent(importIntent); + + // result is handled in ACTION_IMPORT_KEYRING + } catch (Exception e) { + sendErrorToHandler(e); + } + } else if (ACTION_CERTIFY_KEYRING.equals(action)) { + try { + + /* Input */ + long masterKeyId = data.getLong(CERTIFY_KEY_MASTER_KEY_ID); + long pubKeyId = data.getLong(CERTIFY_KEY_PUB_KEY_ID); + ArrayList userIds = data.getStringArrayList(CERTIFY_KEY_UIDS); + + /* Operation */ + String signaturePassphrase = PassphraseCacheService.getCachedPassphrase(this, + masterKeyId); + if (signaturePassphrase == null) { + throw new PgpGeneralException("Unable to obtain passphrase"); + } + + PgpKeyOperation keyOperation = new PgpKeyOperation(new ProgressScaler(this, 0, 100, 100)); + PGPPublicKeyRing publicRing = ProviderHelper.getPGPPublicKeyRing(this, pubKeyId); + PGPPublicKey publicKey = publicRing.getPublicKey(pubKeyId); + PGPSecretKey certificationKey = PgpKeyHelper.getCertificationKey(this, + masterKeyId); + publicKey = keyOperation.certifyKey(certificationKey, publicKey, + userIds, signaturePassphrase); + publicRing = PGPPublicKeyRing.insertPublicKey(publicRing, publicKey); + + // store the signed key in our local cache + PgpImportExport pgpImportExport = new PgpImportExport(this, null); + int retval = pgpImportExport.storeKeyRingInCache(publicRing); + 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); + } + } + } + + private void sendErrorToHandler(Exception e) { + // Service was canceled. Do not send error to handler. + if (this.mIsCanceled) { + return; + } + // contextualize the exception, if necessary + if (e instanceof PgpGeneralMsgIdException) { + e = ((PgpGeneralMsgIdException) e).getContextualized(this); + } + 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) { + // Service was canceled. Do not send message to handler. + if (this.mIsCanceled) { + return; + } + 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 progressDialogUpdater 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 progressDialogUpdater=" + 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); + } + + @Override + public boolean hasServiceStopped() { + return mIsCanceled; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java new file mode 100644 index 000000000..92d012c80 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.service; + +import android.app.Activity; +import android.content.DialogInterface.OnCancelListener; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.widget.Toast; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; + +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, String progressDialogMessage, + int progressDialogStyle) { + this(activity, progressDialogMessage, progressDialogStyle, false, null); + } + + public KeychainIntentServiceHandler(Activity activity, String progressDialogMessage, + int progressDialogStyle, boolean cancelable, + OnCancelListener onCancelListener) { + this.mActivity = activity; + this.mProgressDialogFragment = ProgressDialogFragment.newInstance( + progressDialogMessage, + progressDialogStyle, + cancelable, + onCancelListener); + } + + public void showProgressDialog(FragmentActivity activity) { + // TODO: This is a hack!, see + // http://stackoverflow.com/questions/10114324/show-dialogfragment-from-onactivityresult + final FragmentManager manager = activity.getSupportFragmentManager(); + Handler handler = new Handler(); + handler.post(new Runnable() { + public void run() { + mProgressDialogFragment.show(manager, "progressDialog"); + } + }); + } + + @Override + public void handleMessage(Message message) { + Bundle data = message.getData(); + + switch (message.arg1) { + case MESSAGE_OKAY: + mProgressDialogFragment.dismissAllowingStateLoss(); + + break; + + case MESSAGE_EXCEPTION: + mProgressDialogFragment.dismissAllowingStateLoss(); + + // show error from service + if (data.containsKey(DATA_ERROR)) { + Toast.makeText(mActivity, + mActivity.getString(R.string.error_message, 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java new file mode 100644 index 000000000..962b304c7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.service; + +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.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.util.LongSparseArray; +import android.util.Log; + +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPPrivateKey; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +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.Preferences; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; + +import java.util.Date; +import java.util.Iterator; + +/** + * This service runs in its own process, but is available to all other processes as the main + * passphrase cache. Use the static methods addCachedPassphrase and getCachedPassphrase for + * convenience. + */ +public class PassphraseCacheService extends Service { + public static final String TAG = Constants.TAG + ": PassphraseCacheService"; + + public static final String ACTION_PASSPHRASE_CACHE_ADD = Constants.INTENT_PREFIX + + "PASSPHRASE_CACHE_ADD"; + public static final String ACTION_PASSPHRASE_CACHE_GET = Constants.INTENT_PREFIX + + "PASSPHRASE_CACHE_GET"; + + public static final String BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE = Constants.INTENT_PREFIX + + "PASSPHRASE_CACHE_BROADCAST"; + + public static final String EXTRA_TTL = "ttl"; + public static final String EXTRA_KEY_ID = "key_id"; + public static final String EXTRA_PASSPHRASE = "passphrase"; + public static final String EXTRA_MESSENGER = "messenger"; + + private static final int REQUEST_ID = 0; + private static final long DEFAULT_TTL = 15; + + private BroadcastReceiver mIntentReceiver; + + private LongSparseArray mPassphraseCache = new LongSparseArray(); + + Context mContext; + + /** + * This caches a new passphrase in memory 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.setAction(ACTION_PASSPHRASE_CACHE_ADD); + 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 by sending an intent to the service. This method is + * designed to wait until the service returns the passphrase. + * + * @param context + * @param keyId + * @return passphrase or null (if no passphrase is cached for this keyId) + */ + public static String getCachedPassphrase(Context context, long keyId) { + Log.d(TAG, "getCachedPassphrase() get masterKeyId for " + keyId); + + Intent intent = new Intent(context, PassphraseCacheService.class); + intent.setAction(ACTION_PASSPHRASE_CACHE_GET); + + final Object mutex = new Object(); + final Bundle returnBundle = new Bundle(); + + HandlerThread handlerThread = new HandlerThread("getPassphraseThread"); + handlerThread.start(); + Handler returnHandler = new Handler(handlerThread.getLooper()) { + @Override + public void handleMessage(Message message) { + if (message.obj != null) { + String passphrase = ((Bundle) message.obj).getString(EXTRA_PASSPHRASE); + returnBundle.putString(EXTRA_PASSPHRASE, passphrase); + } + synchronized (mutex) { + mutex.notify(); + } + // quit handlerThread + getLooper().quit(); + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + intent.putExtra(EXTRA_KEY_ID, keyId); + intent.putExtra(EXTRA_MESSENGER, messenger); + // send intent to this service + context.startService(intent); + + // Wait on mutex until passphrase is returned to handlerThread + synchronized (mutex) { + try { + mutex.wait(3000); + } catch (InterruptedException e) { + } + } + + if (returnBundle.containsKey(EXTRA_PASSPHRASE)) { + return returnBundle.getString(EXTRA_PASSPHRASE); + } else { + return null; + } + } + + /** + * Internal implementation to get cached passphrase. + * + * @param keyId + * @return + */ + private String getCachedPassphraseImpl(long keyId) { + Log.d(TAG, "getCachedPassphraseImpl() get masterKeyId for " + keyId); + + // try to get master key id which is used as an identifier for cached passphrases + long masterKeyId = keyId; + if (masterKeyId != Id.key.symmetric) { + masterKeyId = ProviderHelper.getMasterKeyId(this, + KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(keyId))); + // Failure + if(masterKeyId == 0) + return null; + } + Log.d(TAG, "getCachedPassphraseImpl() for masterKeyId " + masterKeyId); + + // get cached passphrase + String cachedPassphrase = mPassphraseCache.get(masterKeyId); + if (cachedPassphrase == null) { + // if key has no passphrase -> cache and return empty passphrase + if (!hasPassphrase(this, masterKeyId)) { + Log.d(Constants.TAG, "Key has no passphrase! Caches and returns empty passphrase!"); + + addCachedPassphrase(this, masterKeyId, ""); + return ""; + } else { + return null; + } + } + // set it again to reset the cache life cycle + Log.d(TAG, "Cache passphrase again when getting it!"); + addCachedPassphrase(this, masterKeyId, cachedPassphrase); + + return cachedPassphrase; + } + + /** + * Checks if key has a passphrase. + * + * @param secretKeyId + * @return true if it has a passphrase + */ + public static boolean hasPassphrase(Context context, long secretKeyId) { + // check if the key has no passphrase + try { + PGPSecretKeyRing secRing = ProviderHelper.getPGPSecretKeyRing(context, secretKeyId); + PGPSecretKey secretKey = null; + boolean foundValidKey = false; + for (Iterator keys = secRing.getSecretKeys(); keys.hasNext(); ) { + secretKey = (PGPSecretKey) keys.next(); + if (!secretKey.isPrivateKeyEmpty()) { + foundValidKey = true; + break; + } + } + + if (!foundValidKey) { + return false; + } + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider( + "SC").build("".toCharArray()); + PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor); + if (testKey != null) { + return false; + } + } catch (PGPException e) { + // silently catch + } + + return true; + } + + /** + * 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; + } + + /** + * 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 && intent.getAction() != null) { + if (ACTION_PASSPHRASE_CACHE_ADD.equals(intent.getAction())) { + 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 ACTION_PASSPHRASE_CACHE_ADD intent in onStartCommand() with keyId: " + + keyId + ", ttl: " + ttl); + + // add keyId and passphrase to memory + mPassphraseCache.put(keyId, passphrase); + + if (ttl > 0) { + // 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)); + } + } else if (ACTION_PASSPHRASE_CACHE_GET.equals(intent.getAction())) { + long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1); + Messenger messenger = intent.getParcelableExtra(EXTRA_MESSENGER); + + String passphrase = getCachedPassphraseImpl(keyId); + + Message msg = Message.obtain(); + Bundle bundle = new Bundle(); + bundle.putString(EXTRA_PASSPHRASE, passphrase); + msg.obj = bundle; + try { + messenger.send(msg); + } catch (RemoteException e) { + Log.e(Constants.TAG, "Sending message failed", e); + } + } else { + Log.e(Constants.TAG, "Intent or Intent Action not supported!"); + } + } + + 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 " + keyId + ", removed from memory!"); + + // stop whole service if no cached passphrases remaining + if (mPassphraseCache.size() == 0) { + Log.d(TAG, "No passphrases remaining in memory, stopping service!"); + stopSelf(); + } + } + + @Override + public void onCreate() { + super.onCreate(); + mContext = this; + Log.d(Constants.TAG, "PassphraseCacheService, onCreate()"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(Constants.TAG, "PassphraseCacheService, onDestroy()"); + + unregisterReceiver(mIntentReceiver); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + public class PassphraseCacheBinder extends Binder { + public PassphraseCacheService getService() { + return PassphraseCacheService.this; + } + } + + private final IBinder mBinder = new PassphraseCacheBinder(); + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java new file mode 100644 index 000000000..7c2dcf2c1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2014 Ash Hughes + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +package org.sufficientlysecure.keychain.service; + +import android.os.Parcel; +import android.os.Parcelable; + +import org.spongycastle.openpgp.PGPSecretKey; +import org.sufficientlysecure.keychain.pgp.PgpConversionHelper; + +import java.util.ArrayList; +import java.util.GregorianCalendar; + +public class SaveKeyringParcel implements Parcelable { + + public ArrayList userIDs; + public ArrayList originalIDs; + public ArrayList deletedIDs; + public boolean[] newIDs; + public boolean primaryIDChanged; + public boolean[] moddedKeys; + public ArrayList deletedKeys; + public ArrayList keysExpiryDates; + public ArrayList keysUsages; + public String newPassphrase; + public String oldPassphrase; + public boolean[] newKeys; + public ArrayList keys; + public String originalPrimaryID; + + public SaveKeyringParcel() {} + + private SaveKeyringParcel(Parcel source) { + userIDs = (ArrayList) source.readSerializable(); + originalIDs = (ArrayList) source.readSerializable(); + deletedIDs = (ArrayList) source.readSerializable(); + newIDs = source.createBooleanArray(); + primaryIDChanged = source.readByte() != 0; + moddedKeys = source.createBooleanArray(); + byte[] tmp = source.createByteArray(); + if (tmp == null) { + deletedKeys = null; + } else { + deletedKeys = PgpConversionHelper.BytesToPGPSecretKeyList(tmp); + } + keysExpiryDates = (ArrayList) source.readSerializable(); + keysUsages = source.readArrayList(Integer.class.getClassLoader()); + newPassphrase = source.readString(); + oldPassphrase = source.readString(); + newKeys = source.createBooleanArray(); + keys = PgpConversionHelper.BytesToPGPSecretKeyList(source.createByteArray()); + originalPrimaryID = source.readString(); + } + + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeSerializable(userIDs); //might not be the best method to store. + destination.writeSerializable(originalIDs); + destination.writeSerializable(deletedIDs); + destination.writeBooleanArray(newIDs); + destination.writeByte((byte) (primaryIDChanged ? 1 : 0)); + destination.writeBooleanArray(moddedKeys); + byte[] tmp = null; + if (deletedKeys.size() != 0) { + tmp = PgpConversionHelper.PGPSecretKeyArrayListToBytes(deletedKeys); + } + destination.writeByteArray(tmp); + destination.writeSerializable(keysExpiryDates); + destination.writeList(keysUsages); + destination.writeString(newPassphrase); + destination.writeString(oldPassphrase); + destination.writeBooleanArray(newKeys); + destination.writeByteArray(PgpConversionHelper.PGPSecretKeyArrayListToBytes(keys)); + destination.writeString(originalPrimaryID); + } + + public static final Creator CREATOR = new Creator() { + public SaveKeyringParcel createFromParcel(final Parcel source) { + return new SaveKeyringParcel(source); + } + + public SaveKeyringParcel[] newArray(final int size) { + return new SaveKeyringParcel[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java new file mode 100644 index 000000000..7027c114e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * 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 android.app.ProgressDialog; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; +import com.beardedhen.androidbootstrap.BootstrapButton; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +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.adapter.ViewKeyUserIdsAdapter; +import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; + +/** + * Signs the specified public key with the specified secret master key + */ +public class CertifyKeyActivity extends ActionBarActivity implements + SelectSecretKeyLayoutFragment.SelectSecretKeyCallback, LoaderManager.LoaderCallbacks { + private BootstrapButton mSignButton; + private CheckBox mUploadKeyCheckbox; + private Spinner mSelectKeyserverSpinner; + + private SelectSecretKeyLayoutFragment mSelectKeyFragment; + + private Uri mDataUri; + private long mPubKeyId = 0; + private long mMasterKeyId = 0; + + private ListView mUserIds; + private ViewKeyUserIdsAdapter mUserIdsAdapter; + + private static final int LOADER_ID_KEYRING = 0; + private static final int LOADER_ID_USER_IDS = 1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.certify_key_activity); + + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setHomeButtonEnabled(false); + + mSelectKeyFragment = (SelectSecretKeyLayoutFragment) getSupportFragmentManager() + .findFragmentById(R.id.sign_key_select_key_fragment); + mSelectKeyFragment.setCallback(this); + mSelectKeyFragment.setFilterCertify(true); + + mSelectKeyserverSpinner = (Spinner) findViewById(R.id.sign_key_keyserver); + ArrayAdapter adapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, Preferences.getPreferences(this) + .getKeyServers()); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mSelectKeyserverSpinner.setAdapter(adapter); + + mUploadKeyCheckbox = (CheckBox) findViewById(R.id.sign_key_upload_checkbox); + if (!mUploadKeyCheckbox.isChecked()) { + mSelectKeyserverSpinner.setEnabled(false); + } else { + mSelectKeyserverSpinner.setEnabled(true); + } + + mUploadKeyCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (!isChecked) { + mSelectKeyserverSpinner.setEnabled(false); + } else { + mSelectKeyserverSpinner.setEnabled(true); + } + } + }); + + mSignButton = (BootstrapButton) findViewById(R.id.sign_key_sign_button); + mSignButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mPubKeyId != 0) { + if (mMasterKeyId == 0) { + mSelectKeyFragment.setError(getString(R.string.select_key_to_sign)); + } else { + initiateSigning(); + } + } + } + }); + + mDataUri = getIntent().getData(); + if (mDataUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!"); + finish(); + return; + } + Log.e(Constants.TAG, "uri: " + mDataUri); + + mUserIds = (ListView) findViewById(R.id.user_ids); + + mUserIdsAdapter = new ViewKeyUserIdsAdapter(this, null, 0, true); + mUserIds.setAdapter(mUserIdsAdapter); + mUserIds.setOnItemClickListener(mUserIdsAdapter); + + getSupportLoaderManager().initLoader(LOADER_ID_KEYRING, null, this); + getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); + + } + + static final String USER_IDS_SELECTION = UserIds.IS_REVOKED + " = 0"; + + static final String[] KEYRING_PROJECTION = + new String[] { + KeyRings._ID, + KeyRings.MASTER_KEY_ID, + KeyRings.FINGERPRINT, + KeyRings.USER_ID, + }; + static final int INDEX_MASTER_KEY_ID = 1; + static final int INDEX_FINGERPRINT = 2; + static final int INDEX_USER_ID = 3; + + @Override + public Loader onCreateLoader(int id, Bundle args) { + switch(id) { + case LOADER_ID_KEYRING: { + Uri uri = KeyRings.buildUnifiedKeyRingUri(mDataUri); + return new CursorLoader(this, uri, KEYRING_PROJECTION, null, null, null); + } + case LOADER_ID_USER_IDS: { + Uri uri = UserIds.buildUserIdsUri(mDataUri); + return new CursorLoader(this, uri, + ViewKeyUserIdsAdapter.USER_IDS_PROJECTION, USER_IDS_SELECTION, null, null); + } + } + return null; + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + switch(loader.getId()) { + case LOADER_ID_KEYRING: + // the first key here is our master key + if (data.moveToFirst()) { + // TODO: put findViewById in onCreate! + mPubKeyId = data.getLong(INDEX_MASTER_KEY_ID); + String keyIdStr = PgpKeyHelper.convertKeyIdToHexShort(mPubKeyId); + ((TextView) findViewById(R.id.key_id)).setText(keyIdStr); + + String mainUserId = data.getString(INDEX_USER_ID); + ((TextView) findViewById(R.id.main_user_id)).setText(mainUserId); + + byte[] fingerprintBlob = data.getBlob(INDEX_FINGERPRINT); + String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob); + ((TextView) findViewById(R.id.fingerprint)) + .setText(PgpKeyHelper.colorizeFingerprint(fingerprint)); + } + break; + case LOADER_ID_USER_IDS: + mUserIdsAdapter.swapCursor(data); + break; + } + } + + @Override + public void onLoaderReset(Loader loader) { + switch(loader.getId()) { + case LOADER_ID_USER_IDS: + mUserIdsAdapter.swapCursor(null); + break; + } + } + + /** + * handles the UI bits of the signing process on the UI thread + */ + private void initiateSigning() { + PGPPublicKeyRing pubring = ProviderHelper.getPGPPublicKeyRing(this, mPubKeyId); + if (pubring != null) { + // if we have already signed this key, dont bother doing it again + boolean alreadySigned = false; + + /* todo: reconsider this at a later point when certs are in the db + @SuppressWarnings("unchecked") + Iterator 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) { + PassphraseDialogFragment.show(this, mMasterKeyId, + new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + startSigning(); + } + } + }); + // bail out; need to wait until the user has entered the passphrase before trying again + return; + } else { + startSigning(); + } + } else { + Toast.makeText(this, R.string.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() { + + // Bail out if there is not at least one user id selected + ArrayList userIds = mUserIdsAdapter.getSelectedUserIds(); + if (userIds.isEmpty()) { + Toast.makeText(CertifyKeyActivity.this, "No User IDs to sign selected!", + Toast.LENGTH_SHORT).show(); + return; + } + + // Send all information needed to service to sign key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.setAction(KeychainIntentService.ACTION_CERTIFY_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + + data.putLong(KeychainIntentService.CERTIFY_KEY_MASTER_KEY_ID, mMasterKeyId); + data.putLong(KeychainIntentService.CERTIFY_KEY_PUB_KEY_ID, mPubKeyId); + data.putStringArrayList(KeychainIntentService.CERTIFY_KEY_UIDS, userIds); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after signing is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + getString(R.string.progress_signing), ProgressDialog.STYLE_SPINNER) { + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentServiceHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + + Toast.makeText(CertifyKeyActivity.this, R.string.key_sign_success, + Toast.LENGTH_SHORT).show(); + + // check if we need to send the key to the server or not + if (mUploadKeyCheckbox.isChecked()) { + // upload the newly signed key to the keyserver + uploadKey(); + } else { + setResult(RESULT_OK); + 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.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING); + + // set data uri as path to keyring + intent.setData(mDataUri); + + // fill values for this action + Bundle data = new Bundle(); + + Spinner keyServer = (Spinner) findViewById(R.id.sign_key_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 KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + 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) { + Toast.makeText(CertifyKeyActivity.this, R.string.key_send_success, + Toast.LENGTH_SHORT).show(); + + setResult(RESULT_OK); + 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); + } + + /** + * callback from select key fragment + */ + @Override + public void onKeySelected(long secretKeyId) { + mMasterKeyId = secretKeyId; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java new file mode 100644 index 000000000..8533e9072 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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 android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.view.PagerTabStrip; +import android.support.v4.view.ViewPager; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ActionBarHelper; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.pgp.PgpHelper; +import org.sufficientlysecure.keychain.ui.adapter.PagerTabStripAdapter; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.regex.Matcher; + +public class DecryptActivity extends DrawerActivity { + + /* Intents */ + public static final String ACTION_DECRYPT = Constants.INTENT_PREFIX + "DECRYPT"; + + /* EXTRA keys for input */ + public static final String EXTRA_TEXT = "text"; + + ViewPager mViewPager; + PagerTabStrip mPagerTabStrip; + PagerTabStripAdapter mTabsAdapter; + + Bundle mMessageFragmentBundle = new Bundle(); + Bundle mFileFragmentBundle = new Bundle(); + int mSwitchToTab = PAGER_TAB_MESSAGE; + + private static final int PAGER_TAB_MESSAGE = 0; + private static final int PAGER_TAB_FILE = 1; + + private void initView() { + mViewPager = (ViewPager) findViewById(R.id.decrypt_pager); + mPagerTabStrip = (PagerTabStrip) findViewById(R.id.decrypt_pager_tab_strip); + + mTabsAdapter = new PagerTabStripAdapter(this); + mViewPager.setAdapter(mTabsAdapter); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.decrypt_activity); + + // set actionbar without home button if called from another app + ActionBarHelper.setBackButton(this); + + initView(); + + setupDrawerNavigation(savedInstanceState); + + // Handle intent actions, maybe changes the bundles + handleActions(getIntent()); + + mTabsAdapter.addTab(DecryptMessageFragment.class, + mMessageFragmentBundle, getString(R.string.label_message)); + mTabsAdapter.addTab(DecryptFileFragment.class, + mFileFragmentBundle, getString(R.string.label_file)); + mViewPager.setCurrentItem(mSwitchToTab); + } + + + /** + * 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 Keychain Decrypt 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 + // executeServiceMethod 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; + } + } else if (Intent.ACTION_VIEW.equals(action)) { + // Android's Action when opening file associated to Keychain (see AndroidManifest.xml) + + // override action + action = ACTION_DECRYPT; + } + + String textData = extras.getString(EXTRA_TEXT); + + /** + * Main Actions + */ + if (ACTION_DECRYPT.equals(action) && textData != null) { + Log.d(Constants.TAG, "textData not null, matching text ..."); + Matcher matcher = PgpHelper.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", " "); + + mMessageFragmentBundle.putString(DecryptMessageFragment.ARG_CIPHERTEXT, textData); + mSwitchToTab = PAGER_TAB_MESSAGE; + } else { + matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(textData); + if (matcher.matches()) { + Log.d(Constants.TAG, "PGP_CLEARTEXT_SIGNATURE matched"); + textData = matcher.group(1); + // replace non breakable spaces + textData = textData.replaceAll("\\xa0", " "); + + mMessageFragmentBundle.putString(DecryptMessageFragment.ARG_CIPHERTEXT, textData); + mSwitchToTab = PAGER_TAB_MESSAGE; + } else { + Log.d(Constants.TAG, "Nothing matched!"); + } + } + } else if (ACTION_DECRYPT.equals(action) && uri != null) { + // get file path from uri + String path = FileHelper.getPath(this, uri); + + if (path != null) { + mFileFragmentBundle.putString(DecryptFileFragment.ARG_FILENAME, path); + mSwitchToTab = PAGER_TAB_FILE; + } else { + Log.e(Constants.TAG, + "Direct binary data without actual file in filesystem is not supported. " + + "Please use the Remote Service API!"); + Toast.makeText(this, R.string.error_only_files_are_supported, Toast.LENGTH_LONG) + .show(); + // end activity + finish(); + } + } else { + Log.e(Constants.TAG, + "Include the extra 'text' or an Uri with setData() in your Intent!"); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFileFragment.java new file mode 100644 index 000000000..492c0cf29 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFileFragment.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +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.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.EditText; + +import com.beardedhen.androidbootstrap.BootstrapButton; +import com.devspark.appmsg.AppMsg; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyResult; +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 java.io.File; + +public class DecryptFileFragment extends DecryptFragment { + public static final String ARG_FILENAME = "filename"; + + private static final int RESULT_CODE_FILE = 0x00007003; + + // view + private EditText mFilename; + private CheckBox mDeleteAfter; + private BootstrapButton mBrowse; + private BootstrapButton mDecryptButton; + + private String mInputFilename = null; + private String mOutputFilename = null; + + private FileDialogFragment mFileDialog; + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.decrypt_file_fragment, container, false); + + mFilename = (EditText) view.findViewById(R.id.decrypt_file_filename); + mBrowse = (BootstrapButton) view.findViewById(R.id.decrypt_file_browse); + mDeleteAfter = (CheckBox) view.findViewById(R.id.decrypt_file_delete_after_decryption); + mDecryptButton = (BootstrapButton) view.findViewById(R.id.decrypt_file_action_decrypt); + mBrowse.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + FileHelper.openFile(DecryptFileFragment.this, mFilename.getText().toString(), "*/*", + RESULT_CODE_FILE); + } + }); + mDecryptButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + decryptAction(); + } + }); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + String filename = getArguments().getString(ARG_FILENAME); + if (filename != null) { + mFilename.setText(filename); + } + } + + 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 decryptAction() { + String currentFilename = mFilename.getText().toString(); + if (mInputFilename == null || !mInputFilename.equals(currentFilename)) { + guessOutputFilename(); + } + + if (mInputFilename.equals("")) { + AppMsg.makeText(getActivity(), R.string.no_file_selected, AppMsg.STYLE_ALERT).show(); + return; + } + + if (mInputFilename.startsWith("file")) { + File file = new File(mInputFilename); + if (!file.exists() || !file.isFile()) { + AppMsg.makeText( + getActivity(), + getString(R.string.error_message, + getString(R.string.error_file_not_found)), AppMsg.STYLE_ALERT) + .show(); + return; + } + } + + askForOutputFilename(); + } + + 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(null); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + mFileDialog = FileDialogFragment.newInstance(messenger, + getString(R.string.title_decrypt_to_file), + getString(R.string.specify_file_to_decrypt_to), mOutputFilename, null); + + mFileDialog.show(getActivity().getSupportFragmentManager(), "fileDialog"); + } + + @Override + protected void decryptStart(String passphrase) { + Log.d(Constants.TAG, "decryptStart"); + + // Send all information needed to service to decrypt in other thread + Intent intent = new Intent(getActivity(), KeychainIntentService.class); + + // fill values for this action + Bundle data = new Bundle(); + + intent.setAction(KeychainIntentService.ACTION_DECRYPT_VERIFY); + + // data + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_URI); + + Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename=" + + mOutputFilename); + + data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename); + data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename); + + data.putString(KeychainIntentService.DECRYPT_PASSPHRASE, passphrase); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after encrypting is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(getActivity(), + getString(R.string.progress_decrypting), 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(); + + PgpDecryptVerifyResult decryptVerifyResult = + returnData.getParcelable(KeychainIntentService.RESULT_DECRYPT_VERIFY_RESULT); + + if (PgpDecryptVerifyResult.KEY_PASSHRASE_NEEDED == decryptVerifyResult.getStatus()) { + showPassphraseDialog(decryptVerifyResult.getKeyIdPassphraseNeeded()); + } else if (PgpDecryptVerifyResult.SYMMETRIC_PASSHRASE_NEEDED == + decryptVerifyResult.getStatus()) { + showPassphraseDialog(Id.key.symmetric); + } else { + AppMsg.makeText(getActivity(), R.string.decryption_successful, + AppMsg.STYLE_INFO).show(); + + if (mDeleteAfter.isChecked()) { + // Create and show dialog to delete original file + DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment + .newInstance(mInputFilename); + deleteFileDialog.show(getActivity().getSupportFragmentManager(), "deleteDialog"); + } + + OpenPgpSignatureResult signatureResult = decryptVerifyResult.getSignatureResult(); + + // display signature result in activity + onSignatureResult(signatureResult); + } + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + saveHandler.showProgressDialog(getActivity()); + + // start service with intent + getActivity().startService(intent); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case RESULT_CODE_FILE: { + if (resultCode == Activity.RESULT_OK && data != null) { + try { + String path = FileHelper.getPath(getActivity(), data.getData()); + Log.d(Constants.TAG, "path=" + path); + + mFilename.setText(path); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Nullpointer while retrieving path!"); + } + } + return; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java new file mode 100644 index 000000000..1c465f55c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptFragment.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.beardedhen.androidbootstrap.BootstrapButton; +import com.devspark.appmsg.AppMsg; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; + +public class DecryptFragment extends Fragment { + private static final int RESULT_CODE_LOOKUP_KEY = 0x00007006; + + protected long mSignatureKeyId = 0; + + protected RelativeLayout mSignatureLayout = null; + protected ImageView mSignatureStatusImage = null; + protected TextView mUserId = null; + protected TextView mUserIdRest = null; + + protected BootstrapButton mLookupKey = null; + + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mSignatureLayout = (RelativeLayout) getView().findViewById(R.id.signature); + mSignatureStatusImage = (ImageView) getView().findViewById(R.id.ic_signature_status); + mUserId = (TextView) getView().findViewById(R.id.mainUserId); + mUserIdRest = (TextView) getView().findViewById(R.id.mainUserIdRest); + mLookupKey = (BootstrapButton) getView().findViewById(R.id.lookup_key); + mLookupKey.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + lookupUnknownKey(mSignatureKeyId); + } + }); + mSignatureLayout.setVisibility(View.GONE); + mSignatureLayout.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + lookupUnknownKey(mSignatureKeyId); + } + }); + } + + private void lookupUnknownKey(long unknownKeyId) { + Intent intent = new Intent(getActivity(), ImportKeysActivity.class); + intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER); + intent.putExtra(ImportKeysActivity.EXTRA_KEY_ID, unknownKeyId); + startActivityForResult(intent, RESULT_CODE_LOOKUP_KEY); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + + case RESULT_CODE_LOOKUP_KEY: { + if (resultCode == Activity.RESULT_OK) { + // TODO: generate new OpenPgpSignatureResult and display it + } + return; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + } + + protected void onSignatureResult(OpenPgpSignatureResult signatureResult) { + mSignatureKeyId = 0; + mSignatureLayout.setVisibility(View.GONE); + if (signatureResult != null) { + + mSignatureKeyId = signatureResult.getKeyId(); + + String userId = signatureResult.getUserId(); + String[] userIdSplit = PgpKeyHelper.splitUserId(userId); + if (userIdSplit[0] != null) { + mUserId.setText(userId); + } else { + mUserId.setText(R.string.user_id_no_name); + } + if (userIdSplit[1] != null) { + mUserIdRest.setText(userIdSplit[1]); + } else { + mUserIdRest.setText(getString(R.string.label_key_id) + ": " + + PgpKeyHelper.convertKeyIdToHex(mSignatureKeyId)); + } + + switch (signatureResult.getStatus()) { + case OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED: { + mSignatureStatusImage.setImageResource(R.drawable.overlay_ok); + mLookupKey.setVisibility(View.GONE); + break; + } + + // TODO! +// case OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED: { +// break; +// } + + case OpenPgpSignatureResult.SIGNATURE_UNKNOWN_PUB_KEY: { + mSignatureStatusImage.setImageResource(R.drawable.overlay_error); + mLookupKey.setVisibility(View.VISIBLE); + AppMsg.makeText(getActivity(), + R.string.unknown_signature, + AppMsg.STYLE_ALERT).show(); + break; + } + + default: { + mSignatureStatusImage.setImageResource(R.drawable.overlay_error); + mLookupKey.setVisibility(View.GONE); + break; + } + } + mSignatureLayout.setVisibility(View.VISIBLE); + } + } + + protected void showPassphraseDialog(long keyId) { + PassphraseDialogFragment.show(getActivity(), keyId, + new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + String passphrase = + message.getData().getString(PassphraseDialogFragment.MESSAGE_DATA_PASSPHRASE); + decryptStart(passphrase); + } + } + }); + } + + /** + * Should be overridden by MessageFragment and FileFragment to start actual decryption + * + * @param passphrase + */ + protected void decryptStart(String passphrase) { + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptMessageFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptMessageFragment.java new file mode 100644 index 000000000..2169bbd77 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptMessageFragment.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.EditText; + +import com.beardedhen.androidbootstrap.BootstrapButton; +import com.devspark.appmsg.AppMsg; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyResult; +import org.sufficientlysecure.keychain.pgp.PgpHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.regex.Matcher; + +public class DecryptMessageFragment extends DecryptFragment { + public static final String ARG_CIPHERTEXT = "ciphertext"; + + // view + private EditText mMessage; + private BootstrapButton mDecryptButton; + private BootstrapButton mDecryptFromCLipboardButton; + + // model + private String mCiphertext; + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.decrypt_message_fragment, container, false); + + mMessage = (EditText) view.findViewById(R.id.message); + mDecryptButton = (BootstrapButton) view.findViewById(R.id.action_decrypt); + mDecryptFromCLipboardButton = (BootstrapButton) view.findViewById(R.id.action_decrypt_from_clipboard); + mDecryptButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + decryptClicked(); + } + }); + mDecryptFromCLipboardButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + decryptFromClipboardClicked(); + } + }); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + String ciphertext = getArguments().getString(ARG_CIPHERTEXT); + if (ciphertext != null) { + mMessage.setText(ciphertext); + decryptStart(null); + } + } + + private void decryptClicked() { + mCiphertext = mMessage.getText().toString(); + decryptStart(null); + } + + private void decryptFromClipboardClicked() { + CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity()); + + // only decrypt if clipboard content is available and a pgp message or cleartext signature + if (clipboardText != null) { + Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(clipboardText); + if (!matcher.matches()) { + matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(clipboardText); + } + if (matcher.matches()) { + mCiphertext = matcher.group(1); + decryptStart(null); + } else { + AppMsg.makeText(getActivity(), R.string.error_invalid_data, AppMsg.STYLE_INFO) + .show(); + } + } else { + AppMsg.makeText(getActivity(), R.string.error_invalid_data, AppMsg.STYLE_INFO) + .show(); + } + } + + @Override + protected void decryptStart(String passphrase) { + Log.d(Constants.TAG, "decryptStart"); + + // Send all information needed to service to decrypt in other thread + Intent intent = new Intent(getActivity(), KeychainIntentService.class); + + // fill values for this action + Bundle data = new Bundle(); + + intent.setAction(KeychainIntentService.ACTION_DECRYPT_VERIFY); + + // data + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES); + data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, mCiphertext.getBytes()); + data.putString(KeychainIntentService.DECRYPT_PASSPHRASE, passphrase); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after encrypting is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(getActivity(), + getString(R.string.progress_decrypting), 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(); + + PgpDecryptVerifyResult decryptVerifyResult = + returnData.getParcelable(KeychainIntentService.RESULT_DECRYPT_VERIFY_RESULT); + + if (PgpDecryptVerifyResult.KEY_PASSHRASE_NEEDED == decryptVerifyResult.getStatus()) { + showPassphraseDialog(decryptVerifyResult.getKeyIdPassphraseNeeded()); + } else if (PgpDecryptVerifyResult.SYMMETRIC_PASSHRASE_NEEDED == + decryptVerifyResult.getStatus()) { + showPassphraseDialog(Id.key.symmetric); + } else { + AppMsg.makeText(getActivity(), R.string.decryption_successful, + AppMsg.STYLE_INFO).show(); + + byte[] decryptedMessage = returnData + .getByteArray(KeychainIntentService.RESULT_DECRYPTED_BYTES); + mMessage.setText(new String(decryptedMessage)); + mMessage.setHorizontallyScrolling(false); + + OpenPgpSignatureResult signatureResult = decryptVerifyResult.getSignatureResult(); + + // display signature result in activity + onSignatureResult(signatureResult); + } + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + saveHandler.showProgressDialog(getActivity()); + + // start service with intent + getActivity().startService(intent); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java new file mode 100644 index 000000000..c875818e3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Color; +import android.os.Bundle; +import android.support.v4.app.ActionBarDrawerToggle; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.beardedhen.androidbootstrap.FontAwesomeText; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; + +public class DrawerActivity extends ActionBarActivity { + private DrawerLayout mDrawerLayout; + private ListView mDrawerList; + private ActionBarDrawerToggle mDrawerToggle; + + private CharSequence mDrawerTitle; + private CharSequence mTitle; + private boolean mIsDrawerLocked = false; + + private Class mSelectedItem; + + private static final int MENU_ID_PREFERENCE = 222; + private static final int MENU_ID_HELP = 223; + + protected void setupDrawerNavigation(Bundle savedInstanceState) { + mDrawerTitle = getString(R.string.app_name); + mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + mDrawerList = (ListView) findViewById(R.id.left_drawer); + ViewGroup viewGroup = (ViewGroup) findViewById(R.id.content_frame); + int leftMarginLoaded = ((ViewGroup.MarginLayoutParams) viewGroup.getLayoutParams()).leftMargin; + int leftMarginInTablets = (int) getResources().getDimension(R.dimen.drawer_size); + int errorInMarginAllowed = 5; + + // if the left margin of the loaded layout is close to the + // one used in tablets then set drawer as open and locked + if (Math.abs(leftMarginLoaded - leftMarginInTablets) < errorInMarginAllowed) { + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN, mDrawerList); + mDrawerLayout.setScrimColor(Color.TRANSPARENT); + mIsDrawerLocked = true; + } else { + // set a custom shadow that overlays the main content when the drawer opens + mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); + mIsDrawerLocked = false; + } + + NavItem mItemIconTexts[] = new NavItem[]{ + new NavItem("fa-user", getString(R.string.nav_contacts)), + new NavItem("fa-lock", getString(R.string.nav_encrypt)), + new NavItem("fa-unlock", getString(R.string.nav_decrypt)), + new NavItem("fa-android", getString(R.string.nav_apps))}; + + mDrawerList.setAdapter(new NavigationDrawerAdapter(this, R.layout.drawer_list_item, + mItemIconTexts)); + + mDrawerList.setOnItemClickListener(new DrawerItemClickListener()); + + // enable ActionBar app icon to behave as action to toggle nav drawer + // if the drawer is not locked + if (!mIsDrawerLocked) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + } + + // ActionBarDrawerToggle ties together the the proper interactions + // between the sliding drawer and the action bar app icon + mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */ + mDrawerLayout, /* DrawerLayout object */ + R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */ + R.string.drawer_open, /* "open drawer" description for accessibility */ + R.string.drawer_close /* "close drawer" description for accessibility */ + ) { + public void onDrawerClosed(View view) { + getSupportActionBar().setTitle(mTitle); + + callIntentForDrawerItem(mSelectedItem); + } + + public void onDrawerOpened(View drawerView) { + mTitle = getSupportActionBar().getTitle(); + getSupportActionBar().setTitle(mDrawerTitle); + // creates call to onPrepareOptionsMenu() + supportInvalidateOptionsMenu(); + } + }; + + if (!mIsDrawerLocked) { + mDrawerLayout.setDrawerListener(mDrawerToggle); + } else { + // If the drawer is locked open make it un-focusable + // so that it doesn't consume all the Back button presses + mDrawerLayout.setFocusableInTouchMode(false); + } + } + + /** + * Uses startActivity to call the Intent of the given class + * + * @param drawerItem the class of the drawer item you want to load. Based on Constants.DrawerItems.* + */ + public void callIntentForDrawerItem(Class drawerItem) { + // creates call to onPrepareOptionsMenu() + supportInvalidateOptionsMenu(); + + // call intent activity if selected + if (drawerItem != null) { + finish(); + overridePendingTransition(0, 0); + + Intent intent = new Intent(this, drawerItem); + startActivity(intent); + + // disable animation of activity start + overridePendingTransition(0, 0); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (mDrawerToggle == null) { + return super.onCreateOptionsMenu(menu); + } + + menu.add(42, MENU_ID_PREFERENCE, 100, R.string.menu_preferences); + menu.add(42, MENU_ID_HELP, 101, R.string.menu_help); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (mDrawerToggle == null) { + return super.onOptionsItemSelected(item); + } + + // The action bar home/up action should open or close the drawer. + // ActionBarDrawerToggle will take care of this. + if (mDrawerToggle.onOptionsItemSelected(item)) { + return true; + } + + switch (item.getItemId()) { + case MENU_ID_PREFERENCE: { + Intent intent = new Intent(this, PreferencesActivity.class); + startActivity(intent); + return true; + } + case MENU_ID_HELP: { + Intent intent = new Intent(this, HelpActivity.class); + startActivity(intent); + return true; + } + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * The click listener for ListView in the navigation drawer + */ + private class DrawerItemClickListener implements ListView.OnItemClickListener { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + selectItem(position); + } + } + + private void selectItem(int position) { + // update selected item and title, then close the drawer + mDrawerList.setItemChecked(position, true); + // set selected class + mSelectedItem = Constants.DrawerItems.ARRAY[position]; + + // setTitle(mDrawerTitles[position]); + // If drawer isn't locked just close the drawer and + // it will move to the selected item by itself (via drawer toggle listener) + if (!mIsDrawerLocked) { + mDrawerLayout.closeDrawer(mDrawerList); + // else move to the selected item yourself + } else { + callIntentForDrawerItem(mSelectedItem); + } + } + + /** + * When using the ActionBarDrawerToggle, you must call it during onPostCreate() and + * onConfigurationChanged()... + */ + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + // Sync the toggle state after onRestoreInstanceState has occurred. + if (mDrawerToggle != null) { + mDrawerToggle.syncState(); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // Pass any configuration change to the drawer toggles + if (mDrawerToggle != null) { + mDrawerToggle.onConfigurationChanged(newConfig); + } + } + + private class NavItem { + public String icon; + public String title; + + public NavItem(String icon, String title) { + super(); + this.icon = icon; + this.title = title; + } + } + + private class NavigationDrawerAdapter extends ArrayAdapter { + Context mContext; + int mLayoutResourceId; + NavItem mData[] = null; + + public NavigationDrawerAdapter(Context context, int layoutResourceId, NavItem[] data) { + super(context, layoutResourceId, data); + this.mLayoutResourceId = layoutResourceId; + this.mContext = context; + this.mData = data; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row = convertView; + NavItemHolder holder; + + if (row == null) { + LayoutInflater inflater = ((Activity) mContext).getLayoutInflater(); + row = inflater.inflate(mLayoutResourceId, parent, false); + + holder = new NavItemHolder(); + holder.mImg = (FontAwesomeText) row.findViewById(R.id.drawer_item_icon); + holder.mTxtTitle = (TextView) row.findViewById(R.id.drawer_item_text); + + row.setTag(holder); + } else { + holder = (NavItemHolder) row.getTag(); + } + + NavItem item = mData[position]; + holder.mTxtTitle.setText(item.title); + holder.mImg.setIcon(item.icon); + + return row; + } + + } + + static class NavItemHolder { + FontAwesomeText mImg; + TextView mTxtTitle; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java new file mode 100644 index 000000000..93d5688b9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java @@ -0,0 +1,760 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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 android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +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.v4.app.ActivityCompat; +import android.support.v7.app.ActionBarActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.LinearLayout; + +import com.beardedhen.androidbootstrap.BootstrapButton; +import com.devspark.appmsg.AppMsg; + +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ActionBarHelper; +import org.sufficientlysecure.keychain.helper.ExportHelper; +import org.sufficientlysecure.keychain.pgp.PgpConversionHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +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 org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; +import org.sufficientlysecure.keychain.ui.widget.Editor; +import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener; +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 java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Vector; + +public class EditKeyActivity extends ActionBarActivity implements EditorListener { + + // Actions for internal use only: + 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 = "user_ids"; + public static final String EXTRA_NO_PASSPHRASE = "no_passphrase"; + public static final String EXTRA_GENERATE_DEFAULT_KEYS = "generate_default_keys"; + + // EDIT + private Uri mDataUri; + + private PGPSecretKeyRing mKeyRing = null; + + private SectionView mUserIdsView; + private SectionView mKeysView; + + private String mCurrentPassphrase = null; + private String mNewPassphrase = null; + private String mSavedNewPassphrase = null; + private boolean mIsPassphraseSet; + private boolean mNeedsSaving; + private boolean mIsBrandNewKeyring = false; + + private BootstrapButton mChangePassphrase; + + private CheckBox mNoPassphrase; + + Vector mUserIds; + Vector mKeys; + Vector mKeysUsages; + boolean mMasterCanSign = true; + + ExportHelper mExportHelper; + + public boolean needsSaving() { + mNeedsSaving = (mUserIdsView == null) ? false : mUserIdsView.needsSaving(); + mNeedsSaving |= (mKeysView == null) ? false : mKeysView.needsSaving(); + mNeedsSaving |= hasPassphraseChanged(); + mNeedsSaving |= mIsBrandNewKeyring; + return mNeedsSaving; + } + + + public void somethingChanged() { + ActivityCompat.invalidateOptionsMenu(this); + } + + public void onDeleted(Editor e, boolean wasNewItem) { + somethingChanged(); + } + + public void onEdited() { + somethingChanged(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mExportHelper = new ExportHelper(this); + + // Inflate a "Done"/"Cancel" custom action bar view + ActionBarHelper.setTwoButtonView(getSupportActionBar(), + R.string.btn_save, R.drawable.ic_action_save, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Save + saveClicked(); + } + }, R.string.menu_key_edit_cancel, R.drawable.ic_action_cancel, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Cancel + cancelClicked(); + } + } + ); + + mUserIds = new Vector(); + mKeys = new Vector(); + mKeysUsages = new Vector(); + + // 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); + } + } + + /** + * Handle intent action to create new key + * + * @param intent + */ + private void handleActionCreateKey(Intent intent) { + Bundle extras = intent.getExtras(); + + mCurrentPassphrase = ""; + mIsBrandNewKeyring = true; + + 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) { + + // Send all information needed to service generate keys in other thread + final Intent serviceIntent = new Intent(this, KeychainIntentService.class); + serviceIntent.setAction(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 KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler( + this, getResources().getQuantityString(R.plurals.progress_generating, 1), + ProgressDialog.STYLE_HORIZONTAL, true, + + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + // Stop key generation on cancel + stopService(serviceIntent); + EditKeyActivity.this.setResult(Activity.RESULT_CANCELED); + EditKeyActivity.this.finish(); + } + }) { + + @Override + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentServiceHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + // get new key from data bundle returned from service + Bundle data = message.getData(); + + ArrayList newKeys = + PgpConversionHelper.BytesToPGPSecretKeyList(data + .getByteArray(KeychainIntentService.RESULT_NEW_KEY)); + + ArrayList keyUsageFlags = data.getIntegerArrayList( + KeychainIntentService.RESULT_KEY_USAGES); + + if (newKeys.size() == keyUsageFlags.size()) { + for (int i = 0; i < newKeys.size(); ++i) { + mKeys.add(newKeys.get(i)); + mKeysUsages.add(keyUsageFlags.get(i)); + } + } + + buildLayout(true); + } + } + }; + + // 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); + } + } + } else { + buildLayout(false); + } + } + + /** + * Handle intent action to edit existing key + * + * @param intent + */ + private void handleActionEditKey(Intent intent) { + mDataUri = intent.getData(); + if (mDataUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!"); + finish(); + } else { + Log.d(Constants.TAG, "uri: " + mDataUri); + + // get master key id using row id + long masterKeyId = ProviderHelper.getMasterKeyId(this, mDataUri); + finallyEdit(masterKeyId); + } + } + + @SuppressWarnings("unchecked") + private void finallyEdit(final long masterKeyId) { + if (masterKeyId != 0) { + PGPSecretKey masterKey = null; + mKeyRing = ProviderHelper.getPGPSecretKeyRing(this, masterKeyId); + if (mKeyRing != null) { + masterKey = mKeyRing.getSecretKey(); + mMasterCanSign = PgpKeyHelper.isCertificationKey(mKeyRing.getSecretKey()); + for (PGPSecretKey key : new IterableIterator(mKeyRing.getSecretKeys())) { + mKeys.add(key); + mKeysUsages.add(-1); // get usage when view is created + } + } else { + Log.e(Constants.TAG, "Keyring not found with masterKeyId: " + masterKeyId); + AppMsg.makeText(this, R.string.error_no_secret_key_found, AppMsg.STYLE_ALERT).show(); + // TODO + } + if (masterKey != null) { + boolean isSet = false; + for (String userId : new IterableIterator(masterKey.getUserIDs())) { + Log.d(Constants.TAG, "Added userId " + userId); + if (!isSet) { + isSet = true; + String[] parts = PgpKeyHelper.splitUserId(userId); + if (parts[0] != null) { + setTitle(parts[0]); + } + } + mUserIds.add(userId); + } + } + } + + mCurrentPassphrase = ""; + buildLayout(false); + + mIsPassphraseSet = PassphraseCacheService.hasPassphrase(this, masterKeyId); + if (!mIsPassphraseSet) { + // check "no passphrase" checkbox and remove button + mNoPassphrase.setChecked(true); + mChangePassphrase.setVisibility(View.GONE); + } + } + + /** + * 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(); + somethingChanged(); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + // set title based on isPassphraseSet() + int title; + if (isPassphraseSet()) { + title = R.string.title_change_passphrase; + } else { + title = R.string.title_set_passphrase; + } + + 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. + * + * @param newKeys + */ + private void buildLayout(boolean newKeys) { + setContentView(R.layout.edit_key_activity); + + // find views + mChangePassphrase = (BootstrapButton) findViewById(R.id.edit_key_btn_change_passphrase); + mNoPassphrase = (CheckBox) findViewById(R.id.edit_key_no_passphrase); + // 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); + if (mIsPassphraseSet) { + mChangePassphrase.setText(getString(R.string.btn_change_passphrase)); + } + mUserIdsView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false); + mUserIdsView.setType(Id.type.user_id); + mUserIdsView.setCanBeEdited(mMasterCanSign); + mUserIdsView.setUserIds(mUserIds); + mUserIdsView.setEditorListener(this); + container.addView(mUserIdsView); + mKeysView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false); + mKeysView.setType(Id.type.key); + mKeysView.setCanBeEdited(mMasterCanSign); + mKeysView.setKeys(mKeys, mKeysUsages, newKeys); + mKeysView.setEditorListener(this); + container.addView(mKeysView); + + updatePassphraseButtonText(); + + mChangePassphrase.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + showSetPassphraseDialog(); + } + }); + + // disable passphrase when no passphrase checkbox is checked! + mNoPassphrase.setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + // remove passphrase + mSavedNewPassphrase = mNewPassphrase; + mNewPassphrase = ""; + mChangePassphrase.setVisibility(View.GONE); + } else { + mNewPassphrase = mSavedNewPassphrase; + mChangePassphrase.setVisibility(View.VISIBLE); + } + somethingChanged(); + } + }); + } + + 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 ((mIsPassphraseSet) + || (mNewPassphrase != null && !mNewPassphrase.equals(""))) { + return true; + } else { + return false; + } + } + + public boolean hasPassphraseChanged() { + if (mNoPassphrase != null) { + if (mNoPassphrase.isChecked()) { + return mIsPassphraseSet; + } else { + return (mNewPassphrase != null && !mNewPassphrase.equals("")); + } + } else { + return false; + } + } + + private void saveClicked() { + final long masterKeyId = getMasterKeyId(); + if (needsSaving()) { //make sure, as some versions don't support invalidateOptionsMenu + try { + if (!isPassphraseSet()) { + throw new PgpGeneralException(this.getString(R.string.set_a_passphrase)); + } + + String passphrase; + if (mIsPassphraseSet) { + passphrase = PassphraseCacheService.getCachedPassphrase(this, masterKeyId); + } else { + passphrase = ""; + } + if (passphrase == null) { + PassphraseDialogFragment.show(this, masterKeyId, + new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + mCurrentPassphrase = PassphraseCacheService.getCachedPassphrase( + EditKeyActivity.this, masterKeyId); + checkEmptyIDsWanted(); + } + } + }); + } else { + mCurrentPassphrase = passphrase; + checkEmptyIDsWanted(); + } + } catch (PgpGeneralException e) { + AppMsg.makeText(this, getString(R.string.error_message, e.getMessage()), + AppMsg.STYLE_ALERT).show(); + } + } else { + AppMsg.makeText(this, R.string.error_change_something_first, AppMsg.STYLE_ALERT).show(); + } + } + + private void checkEmptyIDsWanted() { + try { + ArrayList userIDs = getUserIds(mUserIdsView); + List newIDs = mUserIdsView.getNewIDFlags(); + ArrayList originalIDs = mUserIdsView.getOriginalIDs(); + int curID = 0; + for (String userID : userIDs) { + if (userID.equals("") && (!userID.equals(originalIDs.get(curID)) || newIDs.get(curID))) { + AlertDialog.Builder alert = new AlertDialog.Builder( + EditKeyActivity.this); + + alert.setIcon(R.drawable.ic_dialog_alert_holo_light); + alert.setTitle(R.string.warning); + alert.setMessage(EditKeyActivity.this.getString(R.string.ask_empty_id_ok)); + + alert.setPositiveButton(EditKeyActivity.this.getString(android.R.string.yes), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + finallySaveClicked(); + } + } + ); + alert.setNegativeButton(this.getString(android.R.string.no), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + } + ); + alert.setCancelable(false); + alert.create().show(); + return; + } + curID++; + } + } catch (PgpGeneralException e) { + Log.e(Constants.TAG, getString(R.string.error_message, e.getMessage())); + AppMsg.makeText(this, getString(R.string.error_message, e.getMessage()), AppMsg.STYLE_ALERT).show(); + } + finallySaveClicked(); + } + + private boolean[] toPrimitiveArray(final List booleanList) { + final boolean[] primitives = new boolean[booleanList.size()]; + int index = 0; + for (Boolean object : booleanList) { + primitives[index++] = object; + } + return primitives; + } + + private void finallySaveClicked() { + try { + // Send all information needed to service to edit key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.setAction(KeychainIntentService.ACTION_SAVE_KEYRING); + + SaveKeyringParcel saveParams = new SaveKeyringParcel(); + saveParams.userIDs = getUserIds(mUserIdsView); + saveParams.originalIDs = mUserIdsView.getOriginalIDs(); + saveParams.deletedIDs = mUserIdsView.getDeletedIDs(); + saveParams.newIDs = toPrimitiveArray(mUserIdsView.getNewIDFlags()); + saveParams.primaryIDChanged = mUserIdsView.primaryChanged(); + saveParams.moddedKeys = toPrimitiveArray(mKeysView.getNeedsSavingArray()); + saveParams.deletedKeys = mKeysView.getDeletedKeys(); + saveParams.keysExpiryDates = getKeysExpiryDates(mKeysView); + saveParams.keysUsages = getKeysUsages(mKeysView); + saveParams.newPassphrase = mNewPassphrase; + saveParams.oldPassphrase = mCurrentPassphrase; + saveParams.newKeys = toPrimitiveArray(mKeysView.getNewKeysArray()); + saveParams.keys = getKeys(mKeysView); + saveParams.originalPrimaryID = mUserIdsView.getOriginalPrimaryID(); + + + // fill values for this action + Bundle data = new Bundle(); + data.putBoolean(KeychainIntentService.SAVE_KEYRING_CAN_SIGN, mMasterCanSign); + data.putParcelable(KeychainIntentService.SAVE_KEYRING_PARCEL, saveParams); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after saving is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + getString(R.string.progress_saving), ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentServiceHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + Intent data = new Intent(); + + // return uri pointing to new created key + Uri uri = KeychainContract.KeyRings.buildGenericKeyRingUri( + String.valueOf(getMasterKeyId())); + data.setData(uri); + + 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 (PgpGeneralException e) { + Log.e(Constants.TAG, getString(R.string.error_message, e.getMessage())); + AppMsg.makeText(this, getString(R.string.error_message, e.getMessage()), + AppMsg.STYLE_ALERT).show(); + } + } + + private void cancelClicked() { + if (needsSaving()) { //ask if we want to save + AlertDialog.Builder alert = new AlertDialog.Builder( + EditKeyActivity.this); + + alert.setIcon(R.drawable.ic_dialog_alert_holo_light); + alert.setTitle(R.string.warning); + alert.setMessage(EditKeyActivity.this.getString(R.string.ask_save_changed_key)); + + alert.setPositiveButton(EditKeyActivity.this.getString(android.R.string.yes), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + saveClicked(); + } + }); + alert.setNegativeButton(this.getString(android.R.string.no), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + setResult(RESULT_CANCELED); + finish(); + } + }); + alert.setCancelable(false); + alert.create().show(); + } else { + setResult(RESULT_CANCELED); + finish(); + } + } + + /** + * Returns user ids from the SectionView + * + * @param userIdsView + * @return + */ + private ArrayList getUserIds(SectionView userIdsView) throws PgpGeneralException { + ArrayList userIds = new ArrayList(); + + ViewGroup userIdEditors = userIdsView.getEditors(); + + boolean gotMainUserId = false; + for (int i = 0; i < userIdEditors.getChildCount(); ++i) { + UserIdEditor editor = (UserIdEditor) userIdEditors.getChildAt(i); + String userId; + userId = editor.getValue(); + + if (editor.isMainUserId()) { + userIds.add(0, userId); + gotMainUserId = true; + } else { + userIds.add(userId); + } + } + + if (userIds.size() == 0) { + throw new PgpGeneralException(getString(R.string.error_key_needs_a_user_id)); + } + + if (!gotMainUserId) { + throw new PgpGeneralException(getString(R.string.error_main_user_id_must_not_be_empty)); + } + + return userIds; + } + + /** + * Returns keys from the SectionView + * + * @param keysView + * @return + */ + private ArrayList getKeys(SectionView keysView) throws PgpGeneralException { + ArrayList keys = new ArrayList(); + + ViewGroup keyEditors = keysView.getEditors(); + + if (keyEditors.getChildCount() == 0) { + throw new PgpGeneralException(getString(R.string.error_key_needs_master_key)); + } + + 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 getKeysUsages(SectionView keysView) throws PgpGeneralException { + ArrayList keysUsages = new ArrayList(); + + ViewGroup keyEditors = keysView.getEditors(); + + if (keyEditors.getChildCount() == 0) { + throw new PgpGeneralException(getString(R.string.error_key_needs_master_key)); + } + + for (int i = 0; i < keyEditors.getChildCount(); ++i) { + KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i); + keysUsages.add(editor.getUsage()); + } + + return keysUsages; + } + + private ArrayList getKeysExpiryDates(SectionView keysView) throws PgpGeneralException { + ArrayList keysExpiryDates = new ArrayList(); + + ViewGroup keyEditors = keysView.getEditors(); + + if (keyEditors.getChildCount() == 0) { + throw new PgpGeneralException(getString(R.string.error_key_needs_master_key)); + } + + for (int i = 0; i < keyEditors.getChildCount(); ++i) { + KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i); + keysExpiryDates.add(editor.getExpiryDate()); + } + + return keysExpiryDates; + } + + private void updatePassphraseButtonText() { + mChangePassphrase.setText(isPassphraseSet() ? getString(R.string.btn_change_passphrase) + : getString(R.string.btn_set_passphrase)); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java new file mode 100644 index 000000000..a03c7d797 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivity.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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 android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.view.PagerTabStrip; +import android.support.v4.view.ViewPager; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ActionBarHelper; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.ui.adapter.PagerTabStripAdapter; +import org.sufficientlysecure.keychain.util.Log; + +public class EncryptActivity extends DrawerActivity implements + EncryptSymmetricFragment.OnSymmetricKeySelection, + EncryptAsymmetricFragment.OnAsymmetricKeySelection, + EncryptActivityInterface { + + /* Intents */ + public static final String ACTION_ENCRYPT = Constants.INTENT_PREFIX + "ENCRYPT"; + + /* EXTRA keys for input */ + public static final String EXTRA_TEXT = "text"; + + // enables ASCII Armor for file encryption when uri is given + public static final String EXTRA_ASCII_ARMOR = "ascii_armor"; + + // preselect ids, for internal use + public static final String EXTRA_SIGNATURE_KEY_ID = "signature_key_id"; + public static final String EXTRA_ENCRYPTION_KEY_IDS = "encryption_key_ids"; + + // view + ViewPager mViewPagerMode; + PagerTabStrip mPagerTabStripMode; + PagerTabStripAdapter mTabsAdapterMode; + ViewPager mViewPagerContent; + PagerTabStrip mPagerTabStripContent; + PagerTabStripAdapter mTabsAdapterContent; + + // tabs + Bundle mAsymmetricFragmentBundle = new Bundle(); + Bundle mSymmetricFragmentBundle = new Bundle(); + Bundle mMessageFragmentBundle = new Bundle(); + Bundle mFileFragmentBundle = new Bundle(); + int mSwitchToMode = PAGER_MODE_ASYMMETRIC; + int mSwitchToContent = PAGER_CONTENT_MESSAGE; + + private static final int PAGER_MODE_ASYMMETRIC = 0; + private static final int PAGER_MODE_SYMMETRIC = 1; + private static final int PAGER_CONTENT_MESSAGE = 0; + private static final int PAGER_CONTENT_FILE = 1; + + // model useb by message and file fragment + private long mEncryptionKeyIds[] = null; + private long mSigningKeyId = Id.key.none; + private String mPassphrase; + private String mPassphraseAgain; + + @Override + public void onSigningKeySelected(long signingKeyId) { + mSigningKeyId = signingKeyId; + } + + @Override + public void onEncryptionKeysSelected(long[] encryptionKeyIds) { + mEncryptionKeyIds = encryptionKeyIds; + } + + @Override + public void onPassphraseUpdate(String passphrase) { + mPassphrase = passphrase; + } + + @Override + public void onPassphraseAgainUpdate(String passphrase) { + mPassphraseAgain = passphrase; + } + + @Override + public boolean isModeSymmetric() { + if (PAGER_MODE_SYMMETRIC == mViewPagerMode.getCurrentItem()) { + return true; + } else { + return false; + } + } + + @Override + public long getSignatureKey() { + return mSigningKeyId; + } + + @Override + public long[] getEncryptionKeys() { + return mEncryptionKeyIds; + } + + @Override + public String getPassphrase() { + return mPassphrase; + } + + @Override + public String getPassphraseAgain() { + return mPassphraseAgain; + } + + + private void initView() { + mViewPagerMode = (ViewPager) findViewById(R.id.encrypt_pager_mode); + mPagerTabStripMode = (PagerTabStrip) findViewById(R.id.encrypt_pager_tab_strip_mode); + mViewPagerContent = (ViewPager) findViewById(R.id.encrypt_pager_content); + mPagerTabStripContent = (PagerTabStrip) findViewById(R.id.encrypt_pager_tab_strip_content); + + mTabsAdapterMode = new PagerTabStripAdapter(this); + mViewPagerMode.setAdapter(mTabsAdapterMode); + mTabsAdapterContent = new PagerTabStripAdapter(this); + mViewPagerContent.setAdapter(mTabsAdapterContent); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.encrypt_activity); + + // set actionbar without home button if called from another app + ActionBarHelper.setBackButton(this); + + initView(); + + setupDrawerNavigation(savedInstanceState); + + // Handle intent actions + handleActions(getIntent()); + + mTabsAdapterMode.addTab(EncryptAsymmetricFragment.class, + mAsymmetricFragmentBundle, getString(R.string.label_asymmetric)); + mTabsAdapterMode.addTab(EncryptSymmetricFragment.class, + mSymmetricFragmentBundle, getString(R.string.label_symmetric)); + mViewPagerMode.setCurrentItem(mSwitchToMode); + + mTabsAdapterContent.addTab(EncryptMessageFragment.class, + mMessageFragmentBundle, getString(R.string.label_message)); + mTabsAdapterContent.addTab(EncryptFileFragment.class, + mFileFragmentBundle, getString(R.string.label_file)); + mViewPagerContent.setCurrentItem(mSwitchToContent); + } + + /** + * 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 + // executeServiceMethod ACTION_ENCRYPT in main actions + extras.putString(EXTRA_TEXT, sharedText); + extras.putBoolean(EXTRA_ASCII_ARMOR, true); + action = ACTION_ENCRYPT; + } + } else { + // Files via content provider, override uri and action + uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); + action = ACTION_ENCRYPT; + } + } + + if (extras.containsKey(EXTRA_ASCII_ARMOR)) { + boolean requestAsciiArmor = extras.getBoolean(EXTRA_ASCII_ARMOR, true); + mFileFragmentBundle.putBoolean(EncryptFileFragment.ARG_ASCII_ARMOR, requestAsciiArmor); + } + + String textData = extras.getString(EXTRA_TEXT); + + long signatureKeyId = extras.getLong(EXTRA_SIGNATURE_KEY_ID); + long[] encryptionKeyIds = extras.getLongArray(EXTRA_ENCRYPTION_KEY_IDS); + + // preselect keys given by intent + mAsymmetricFragmentBundle.putLongArray(EncryptAsymmetricFragment.ARG_ENCRYPTION_KEY_IDS, + encryptionKeyIds); + mAsymmetricFragmentBundle.putLong(EncryptAsymmetricFragment.ARG_SIGNATURE_KEY_ID, + signatureKeyId); + mSwitchToMode = PAGER_MODE_ASYMMETRIC; + + /** + * Main Actions + */ + if (ACTION_ENCRYPT.equals(action) && textData != null) { + // encrypt text based on given extra + mMessageFragmentBundle.putString(EncryptMessageFragment.ARG_TEXT, textData); + mSwitchToContent = PAGER_CONTENT_MESSAGE; + } else if (ACTION_ENCRYPT.equals(action) && uri != null) { + // encrypt file based on Uri + + // get file path from uri + String path = FileHelper.getPath(this, uri); + + if (path != null) { + mFileFragmentBundle.putString(EncryptFileFragment.ARG_FILENAME, path); + mSwitchToContent = PAGER_CONTENT_FILE; + } else { + Log.e(Constants.TAG, + "Direct binary data without actual file in filesystem is not supported " + + "by Intents. Please use the Remote Service API!"); + Toast.makeText(this, R.string.error_only_files_are_supported, Toast.LENGTH_LONG) + .show(); + // end activity + finish(); + } + } else { + Log.e(Constants.TAG, + "Include the extra 'text' or an Uri with setData() in your Intent!"); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivityInterface.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivityInterface.java new file mode 100644 index 000000000..0786b3a16 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptActivityInterface.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +public interface EncryptActivityInterface { + + public boolean isModeSymmetric(); + + public long getSignatureKey(); + public long[] getEncryptionKeys(); + + public String getPassphrase(); + public String getPassphraseAgain(); + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java new file mode 100644 index 000000000..8400cf397 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.spongycastle.openpgp.PGPSecretKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.ProviderHelper; + +import java.util.HashMap; +import java.util.Vector; + +public class EncryptAsymmetricFragment extends Fragment { + public static final String ARG_SIGNATURE_KEY_ID = "signature_key_id"; + public static final String ARG_ENCRYPTION_KEY_IDS = "encryption_key_ids"; + + public static final int RESULT_CODE_PUBLIC_KEYS = 0x00007001; + public static final int RESULT_CODE_SECRET_KEYS = 0x00007002; + + OnAsymmetricKeySelection mKeySelectionListener; + + // view + private BootstrapButton mSelectKeysButton; + private CheckBox mSign; + private TextView mMainUserId; + private TextView mMainUserIdRest; + + // model + private long mSecretKeyId = Id.key.none; + private long mEncryptionKeyIds[] = null; + + // Container Activity must implement this interface + public interface OnAsymmetricKeySelection { + public void onSigningKeySelected(long signingKeyId); + + public void onEncryptionKeysSelected(long[] encryptionKeyIds); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + mKeySelectionListener = (OnAsymmetricKeySelection) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must implement OnAsymmetricKeySelection"); + } + } + + private void setSignatureKeyId(long signatureKeyId) { + mSecretKeyId = signatureKeyId; + // update key selection in EncryptActivity + mKeySelectionListener.onSigningKeySelected(signatureKeyId); + updateView(); + } + + private void setEncryptionKeyIds(long[] encryptionKeyIds) { + mEncryptionKeyIds = encryptionKeyIds; + // update key selection in EncryptActivity + mKeySelectionListener.onEncryptionKeysSelected(encryptionKeyIds); + updateView(); + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.encrypt_asymmetric_fragment, container, false); + + mSelectKeysButton = (BootstrapButton) view.findViewById(R.id.btn_selectEncryptKeys); + mSign = (CheckBox) view.findViewById(R.id.sign); + mMainUserId = (TextView) view.findViewById(R.id.mainUserId); + mMainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + mSelectKeysButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + selectPublicKeys(); + } + }); + mSign.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + CheckBox checkBox = (CheckBox) v; + if (checkBox.isChecked()) { + selectSecretKey(); + } else { + setSignatureKeyId(Id.key.none); + } + } + }); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + long signatureKeyId = getArguments().getLong(ARG_SIGNATURE_KEY_ID); + long[] encryptionKeyIds = getArguments().getLongArray(ARG_ENCRYPTION_KEY_IDS); + + // preselect keys given by arguments (given by Intent to EncryptActivity) + preselectKeys(signatureKeyId, encryptionKeyIds); + } + + /** + * 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) { + // TODO: don't use bouncy castle objects! + PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingWithKeyId(getActivity(), + preselectedSignatureKeyId); + PGPSecretKey masterKey; + if (keyRing != null) { + masterKey = keyRing.getSecretKey(); + if (masterKey != null) { + Vector signKeys = PgpKeyHelper.getUsableSigningKeys(keyRing); + if (signKeys.size() > 0) { + setSignatureKeyId(masterKey.getKeyID()); + } + } + } + } + + if (preselectedEncryptionKeyIds != null) { + Vector goodIds = new Vector(); + for (int i = 0; i < preselectedEncryptionKeyIds.length; ++i) { + long id = ProviderHelper.getMasterKeyId(getActivity(), + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(preselectedEncryptionKeyIds[i])) + ); + // TODO check for available encrypt keys... is this even relevant? + goodIds.add(id); + } + if (goodIds.size() > 0) { + long[] keyIds = new long[goodIds.size()]; + for (int i = 0; i < goodIds.size(); ++i) { + keyIds[i] = goodIds.get(i); + } + setEncryptionKeyIds(keyIds); + } + } + } + + private void updateView() { + if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) { + mSelectKeysButton.setText(getString(R.string.select_keys_button_default)); + } else { + mSelectKeysButton.setText(getResources().getQuantityString( + R.plurals.select_keys_button, mEncryptionKeyIds.length, + mEncryptionKeyIds.length)); + } + + if (mSecretKeyId == Id.key.none) { + mSign.setChecked(false); + mMainUserId.setText(""); + mMainUserIdRest.setText(""); + } else { + // See if we can get a user_id from a unified query + String userIdResult = (String) ProviderHelper.getUnifiedData( + getActivity(), mSecretKeyId, KeyRings.USER_ID, ProviderHelper.FIELD_TYPE_STRING); + String[] userId = PgpKeyHelper.splitUserId(userIdResult); + if (userId[0] != null) { + mMainUserId.setText(userId[0]); + } else { + mMainUserId.setText(getResources().getString(R.string.user_id_no_name)); + } + if (userId[1] != null) { + mMainUserIdRest.setText(userId[1]); + } else { + mMainUserIdRest.setText(""); + } + mSign.setChecked(true); + } + } + + private void selectPublicKeys() { + Intent intent = new Intent(getActivity(), SelectPublicKeyActivity.class); + Vector keyIds = new Vector(); + 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(getActivity(), SelectSecretKeyActivity.class); + startActivityForResult(intent, Id.request.secret_keys); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case RESULT_CODE_PUBLIC_KEYS: { + if (resultCode == Activity.RESULT_OK) { + Bundle bundle = data.getExtras(); + setEncryptionKeyIds(bundle + .getLongArray(SelectPublicKeyActivity.RESULT_EXTRA_MASTER_KEY_IDS)); + } + break; + } + + case RESULT_CODE_SECRET_KEYS: { + if (resultCode == Activity.RESULT_OK) { + Uri uriMasterKey = data.getData(); + setSignatureKeyId(Long.valueOf(uriMasterKey.getLastPathSegment())); + } else { + setSignatureKeyId(Id.key.none); + } + break; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileFragment.java new file mode 100644 index 000000000..470c85715 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileFragment.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +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.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.Spinner; + +import com.beardedhen.androidbootstrap.BootstrapButton; +import com.devspark.appmsg.AppMsg; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.helper.Preferences; +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 java.io.File; + +public class EncryptFileFragment extends Fragment { + public static final String ARG_FILENAME = "filename"; + public static final String ARG_ASCII_ARMOR = "ascii_armor"; + + private static final int RESULT_CODE_FILE = 0x00007003; + + private EncryptActivityInterface mEncryptInterface; + + // view + private CheckBox mAsciiArmor = null; + private Spinner mFileCompression = null; + private EditText mFilename = null; + private CheckBox mDeleteAfter = null; + private CheckBox mShareAfter = null; + private BootstrapButton mBrowse = null; + private BootstrapButton mEncryptFile; + + private FileDialogFragment mFileDialog; + + // model + private String mInputFilename = null; + private String mOutputFilename = null; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + mEncryptInterface = (EncryptActivityInterface) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must implement EncryptActivityInterface"); + } + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.encrypt_file_fragment, container, false); + + mEncryptFile = (BootstrapButton) view.findViewById(R.id.action_encrypt_file); + mEncryptFile.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + encryptClicked(); + } + }); + + mFilename = (EditText) view.findViewById(R.id.filename); + mBrowse = (BootstrapButton) view.findViewById(R.id.btn_browse); + mBrowse.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + FileHelper.openFile(EncryptFileFragment.this, mFilename.getText().toString(), "*/*", + Id.request.filename); + } + }); + + mFileCompression = (Spinner) view.findViewById(R.id.fileCompression); + Choice[] choices = new Choice[] { + new Choice(Id.choice.compression.none, getString(R.string.choice_none) + " (" + + getString(R.string.compression_fast) + ")"), + new Choice(Id.choice.compression.zip, "ZIP (" + + getString(R.string.compression_fast) + ")"), + new Choice(Id.choice.compression.zlib, "ZLIB (" + + getString(R.string.compression_fast) + ")"), + new Choice(Id.choice.compression.bzip2, "BZIP2 (" + + getString(R.string.compression_very_slow) + ")"), + }; + ArrayAdapter adapter = new ArrayAdapter(getActivity(), + android.R.layout.simple_spinner_item, choices); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mFileCompression.setAdapter(adapter); + + int defaultFileCompression = Preferences.getPreferences(getActivity()).getDefaultFileCompression(); + for (int i = 0; i < choices.length; ++i) { + if (choices[i].getId() == defaultFileCompression) { + mFileCompression.setSelection(i); + break; + } + } + + mDeleteAfter = (CheckBox) view.findViewById(R.id.deleteAfterEncryption); + mShareAfter = (CheckBox) view.findViewById(R.id.shareAfterEncryption); + + mAsciiArmor = (CheckBox) view.findViewById(R.id.asciiArmor); + mAsciiArmor.setChecked(Preferences.getPreferences(getActivity()).getDefaultAsciiArmor()); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + String filename = getArguments().getString(ARG_FILENAME); + if (filename != null) { + mFilename.setText(filename); + } + boolean asciiArmor = getArguments().getBoolean(ARG_ASCII_ARMOR); + if (asciiArmor) { + mAsciiArmor.setChecked(asciiArmor); + } + } + + /** + * 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 = (mAsciiArmor.isChecked() ? ".asc" : ".gpg"); + String outputFilename = file.getParent() + File.separator + file.getName() + ending; + + return outputFilename; + } + + 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_encrypt_to_file), + getString(R.string.specify_file_to_encrypt_to), mOutputFilename, null); + + mFileDialog.show(getActivity().getSupportFragmentManager(), "fileDialog"); + } + + private void encryptClicked() { + String currentFilename = mFilename.getText().toString(); + if (mInputFilename == null || !mInputFilename.equals(currentFilename)) { + mInputFilename = mFilename.getText().toString(); + } + + mOutputFilename = guessOutputFilename(mInputFilename); + + if (mInputFilename.equals("")) { + AppMsg.makeText(getActivity(), R.string.no_file_selected, AppMsg.STYLE_ALERT).show(); + return; + } + + if (!mInputFilename.startsWith("content")) { + File file = new File(mInputFilename); + if (!file.exists() || !file.isFile()) { + AppMsg.makeText( + getActivity(), + getString(R.string.error_message, + getString(R.string.error_file_not_found)), AppMsg.STYLE_ALERT) + .show(); + return; + } + } + + if (mEncryptInterface.isModeSymmetric()) { + // symmetric encryption + + boolean gotPassphrase = (mEncryptInterface.getPassphrase() != null + && mEncryptInterface.getPassphrase().length() != 0); + if (!gotPassphrase) { + AppMsg.makeText(getActivity(), R.string.passphrase_must_not_be_empty, AppMsg.STYLE_ALERT) + .show(); + return; + } + + if (!mEncryptInterface.getPassphrase().equals(mEncryptInterface.getPassphraseAgain())) { + AppMsg.makeText(getActivity(), R.string.passphrases_do_not_match, AppMsg.STYLE_ALERT).show(); + return; + } + } else { + // asymmetric encryption + + boolean gotEncryptionKeys = (mEncryptInterface.getEncryptionKeys() != null + && mEncryptInterface.getEncryptionKeys().length > 0); + + if (!gotEncryptionKeys) { + AppMsg.makeText(getActivity(), R.string.select_encryption_key, AppMsg.STYLE_ALERT).show(); + return; + } + + if (!gotEncryptionKeys && mEncryptInterface.getSignatureKey() == 0) { + AppMsg.makeText(getActivity(), R.string.select_encryption_or_signature_key, + AppMsg.STYLE_ALERT).show(); + return; + } + + if (mEncryptInterface.getSignatureKey() != 0 && + PassphraseCacheService.getCachedPassphrase(getActivity(), + mEncryptInterface.getSignatureKey()) == null) { + PassphraseDialogFragment.show(getActivity(), mEncryptInterface.getSignatureKey(), + new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + showOutputFileDialog(); + } + } + }); + + return; + } + } + + showOutputFileDialog(); + } + + private void encryptStart() { + // Send all information needed to service to edit key in other thread + Intent intent = new Intent(getActivity(), KeychainIntentService.class); + + intent.setAction(KeychainIntentService.ACTION_ENCRYPT_SIGN); + + // fill values for this action + Bundle data = new Bundle(); + + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_URI); + + if (mEncryptInterface.isModeSymmetric()) { + Log.d(Constants.TAG, "Symmetric encryption enabled!"); + String passphrase = mEncryptInterface.getPassphrase(); + if (passphrase.length() == 0) { + passphrase = null; + } + data.putString(KeychainIntentService.ENCRYPT_SYMMETRIC_PASSPHRASE, passphrase); + } else { + data.putLong(KeychainIntentService.ENCRYPT_SIGNATURE_KEY_ID, + mEncryptInterface.getSignatureKey()); + data.putLongArray(KeychainIntentService.ENCRYPT_ENCRYPTION_KEYS_IDS, + mEncryptInterface.getEncryptionKeys()); + } + + Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename=" + + mOutputFilename); + + data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename); + data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename); + + boolean useAsciiArmor = mAsciiArmor.isChecked(); + data.putBoolean(KeychainIntentService.ENCRYPT_USE_ASCII_ARMOR, useAsciiArmor); + + int compressionId = ((Choice) mFileCompression.getSelectedItem()).getId(); + data.putInt(KeychainIntentService.ENCRYPT_COMPRESSION_ID, compressionId); +// data.putBoolean(KeychainIntentService.ENCRYPT_GENERATE_SIGNATURE, mGenerateSignature); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after encrypting is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(getActivity(), + getString(R.string.progress_encrypting), ProgressDialog.STYLE_HORIZONTAL) { + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentServiceHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + AppMsg.makeText(getActivity(), R.string.encryption_successful, + AppMsg.STYLE_INFO).show(); + + if (mDeleteAfter.isChecked()) { + // Create and show dialog to delete original file + DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment + .newInstance(mInputFilename); + deleteFileDialog.show(getActivity().getSupportFragmentManager(), "deleteDialog"); + } + + if (mShareAfter.isChecked()) { + // Share encrypted file + Intent sendFileIntent = new Intent(Intent.ACTION_SEND); + sendFileIntent.setType("*/*"); + sendFileIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(mOutputFilename)); + startActivity(Intent.createChooser(sendFileIntent, + getString(R.string.title_send_file))); + } + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + saveHandler.showProgressDialog(getActivity()); + + // start service with intent + getActivity().startService(intent); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case RESULT_CODE_FILE: { + if (resultCode == Activity.RESULT_OK && data != null) { + try { + String path = FileHelper.getPath(getActivity(), data.getData()); + Log.d(Constants.TAG, "path=" + path); + + mFilename.setText(path); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Nullpointer while retrieving path!"); + } + } + return; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptMessageFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptMessageFragment.java new file mode 100644 index 000000000..ba11074fc --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptMessageFragment.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +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.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import com.beardedhen.androidbootstrap.BootstrapButton; +import com.devspark.appmsg.AppMsg; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.helper.Preferences; +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; + +public class EncryptMessageFragment extends Fragment { + public static final String ARG_TEXT = "text"; + + private EditText mMessage = null; + private BootstrapButton mEncryptShare; + private BootstrapButton mEncryptClipboard; + + private EncryptActivityInterface mEncryptInterface; + + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + mEncryptInterface = (EncryptActivityInterface) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must implement EncryptActivityInterface"); + } + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.encrypt_message_fragment, container, false); + + mMessage = (EditText) view.findViewById(R.id.message); + mEncryptClipboard = (BootstrapButton) view.findViewById(R.id.action_encrypt_clipboard); + mEncryptShare = (BootstrapButton) view.findViewById(R.id.action_encrypt_share); + mEncryptClipboard.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + encryptClicked(true); + } + }); + mEncryptShare.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + encryptClicked(false); + } + }); + + return view; + } + + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + String text = getArguments().getString(ARG_TEXT); + if (text != null) { + mMessage.setText(text); + } + } + + /** + * 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 encryptClicked(final boolean toClipboard) { + if (mEncryptInterface.isModeSymmetric()) { + // symmetric encryption + + boolean gotPassphrase = (mEncryptInterface.getPassphrase() != null + && mEncryptInterface.getPassphrase().length() != 0); + if (!gotPassphrase) { + AppMsg.makeText(getActivity(), R.string.passphrase_must_not_be_empty, AppMsg.STYLE_ALERT) + .show(); + return; + } + + if (!mEncryptInterface.getPassphrase().equals(mEncryptInterface.getPassphraseAgain())) { + AppMsg.makeText(getActivity(), R.string.passphrases_do_not_match, AppMsg.STYLE_ALERT).show(); + return; + } + + } else { + // asymmetric encryption + + boolean gotEncryptionKeys = (mEncryptInterface.getEncryptionKeys() != null + && mEncryptInterface.getEncryptionKeys().length > 0); + + if (!gotEncryptionKeys && mEncryptInterface.getSignatureKey() == 0) { + AppMsg.makeText(getActivity(), R.string.select_encryption_or_signature_key, + AppMsg.STYLE_ALERT).show(); + return; + } + + if (mEncryptInterface.getSignatureKey() != 0 && + PassphraseCacheService.getCachedPassphrase(getActivity(), + mEncryptInterface.getSignatureKey()) == null) { + PassphraseDialogFragment.show(getActivity(), mEncryptInterface.getSignatureKey(), + new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) { + encryptStart(toClipboard); + } + } + }); + + return; + } + } + + encryptStart(toClipboard); + } + + private void encryptStart(final boolean toClipboard) { + // Send all information needed to service to edit key in other thread + Intent intent = new Intent(getActivity(), KeychainIntentService.class); + + intent.setAction(KeychainIntentService.ACTION_ENCRYPT_SIGN); + + // fill values for this action + Bundle data = new Bundle(); + + data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES); + + String message = mMessage.getText().toString(); + + if (mEncryptInterface.isModeSymmetric()) { + Log.d(Constants.TAG, "Symmetric encryption enabled!"); + String passphrase = mEncryptInterface.getPassphrase(); + if (passphrase.length() == 0) { + passphrase = null; + } + data.putString(KeychainIntentService.ENCRYPT_SYMMETRIC_PASSPHRASE, passphrase); + } else { + data.putLong(KeychainIntentService.ENCRYPT_SIGNATURE_KEY_ID, + mEncryptInterface.getSignatureKey()); + data.putLongArray(KeychainIntentService.ENCRYPT_ENCRYPTION_KEYS_IDS, + mEncryptInterface.getEncryptionKeys()); + + boolean signOnly = (mEncryptInterface.getEncryptionKeys() == null + || mEncryptInterface.getEncryptionKeys().length == 0); + if (signOnly) { + message = fixBadCharactersForGmail(message); + } + } + + data.putByteArray(KeychainIntentService.ENCRYPT_MESSAGE_BYTES, message.getBytes()); + + data.putBoolean(KeychainIntentService.ENCRYPT_USE_ASCII_ARMOR, true); + + int compressionId = Preferences.getPreferences(getActivity()).getDefaultMessageCompression(); + data.putInt(KeychainIntentService.ENCRYPT_COMPRESSION_ID, compressionId); +// data.putBoolean(KeychainIntentService.ENCRYPT_GENERATE_SIGNATURE, mGenerateSignature); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after encrypting is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(getActivity(), + getString(R.string.progress_encrypting), 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 data = message.getData(); + + String output = new String(data.getByteArray(KeychainIntentService.RESULT_BYTES)); + Log.d(Constants.TAG, "output: " + output); + + if (toClipboard) { + ClipboardReflection.copyToClipboard(getActivity(), output); + AppMsg.makeText(getActivity(), + R.string.encryption_to_clipboard_successful, AppMsg.STYLE_INFO) + .show(); + } else { + Intent sendIntent = new Intent(Intent.ACTION_SEND); + + // Type is set to text/plain so that encrypted messages can + // be sent with Whatsapp, Hangouts, SMS etc... + sendIntent.setType("text/plain"); + + sendIntent.putExtra(Intent.EXTRA_TEXT, output); + startActivity(Intent.createChooser(sendIntent, + getString(R.string.title_send_email))); + } + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + saveHandler.showProgressDialog(getActivity()); + + // start service with intent + getActivity().startService(intent); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptSymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptSymmetricFragment.java new file mode 100644 index 000000000..8efa07953 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptSymmetricFragment.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import org.sufficientlysecure.keychain.R; + +public class EncryptSymmetricFragment extends Fragment { + + OnSymmetricKeySelection mPassphraseUpdateListener; + + private EditText mPassphrase; + private EditText mPassphraseAgain; + + // Container Activity must implement this interface + public interface OnSymmetricKeySelection { + public void onPassphraseUpdate(String passphrase); + + public void onPassphraseAgainUpdate(String passphrase); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + mPassphraseUpdateListener = (OnSymmetricKeySelection) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must implement OnSymmetricKeySelection"); + } + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.encrypt_symmetric_fragment, container, false); + + mPassphrase = (EditText) view.findViewById(R.id.passphrase); + mPassphraseAgain = (EditText) view.findViewById(R.id.passphraseAgain); + mPassphrase.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + // update passphrase in EncryptActivity + mPassphraseUpdateListener.onPassphraseUpdate(s.toString()); + } + }); + mPassphraseAgain.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + // update passphrase in EncryptActivity + mPassphraseUpdateListener.onPassphraseAgainUpdate(s.toString()); + } + }); + + return view; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java new file mode 100644 index 000000000..a484b57de --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpAboutFragment.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import org.sufficientlysecure.htmltextview.HtmlTextView; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + + +public class HelpAboutFragment extends Fragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.help_about_fragment, container, false); + + TextView versionText = (TextView) view.findViewById(R.id.help_about_version); + versionText.setText(getString(R.string.help_about_version) + " " + getVersion()); + + HtmlTextView aboutTextView = (HtmlTextView) view.findViewById(R.id.help_about_text); + + // load html from raw resource (Parsing handled by HtmlTextView library) + aboutTextView.setHtmlFromRawResource(getActivity(), R.raw.help_about); + + // 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; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpActivity.java new file mode 100644 index 000000000..32f37a0a5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpActivity.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.adapter.TabsAdapter; + +public class HelpActivity extends ActionBarActivity { + public static final String EXTRA_SELECTED_TAB = "selected_tab"; + + ViewPager mViewPager; + TabsAdapter mTabsAdapter; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.help_activity); + + mViewPager = (ViewPager) findViewById(R.id.pager); + + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setHomeButtonEnabled(false); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + + 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(HelpHtmlFragment.ARG_HTML_FILE, R.raw.help_start); + mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_start)), + HelpHtmlFragment.class, startBundle, (selectedTab == 0)); + + Bundle faqBundle = new Bundle(); + faqBundle.putInt(HelpHtmlFragment.ARG_HTML_FILE, R.raw.help_faq); + mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_faq)), + HelpHtmlFragment.class, faqBundle, (selectedTab == 1)); + + Bundle nfcBundle = new Bundle(); + nfcBundle.putInt(HelpHtmlFragment.ARG_HTML_FILE, R.raw.help_nfc_beam); + mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_nfc_beam)), + HelpHtmlFragment.class, nfcBundle, (selectedTab == 2)); + + Bundle changelogBundle = new Bundle(); + changelogBundle.putInt(HelpHtmlFragment.ARG_HTML_FILE, R.raw.help_changelog); + mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_changelog)), + HelpHtmlFragment.class, changelogBundle, (selectedTab == 3)); + + mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.help_tab_about)), + HelpAboutFragment.class, null, (selectedTab == 4)); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpHtmlFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpHtmlFragment.java new file mode 100644 index 000000000..6b3c51b08 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/HelpHtmlFragment.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; +import org.sufficientlysecure.htmltextview.HtmlTextView; + +public class HelpHtmlFragment extends Fragment { + private Activity mActivity; + + private int mHtmlFile; + + public static final String ARG_HTML_FILE = "htmlFile"; + + /** + * Create a new instance of HelpHtmlFragment, providing "htmlFile" as an argument. + */ + static HelpHtmlFragment newInstance(int htmlFile) { + HelpHtmlFragment f = new HelpHtmlFragment(); + + // Supply html raw file input as an argument. + Bundle args = new Bundle(); + args.putInt(ARG_HTML_FILE, htmlFile); + f.setArguments(args); + + return f; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mActivity = getActivity(); + + mHtmlFile = getArguments().getInt(ARG_HTML_FILE); + + ScrollView scroller = new ScrollView(mActivity); + HtmlTextView text = new HtmlTextView(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 from raw resource (Parsing handled by HtmlTextView library) + text.setHtmlFromRawResource(getActivity(), mHtmlFile); + + // no flickering when clicking textview for Android < 4 + text.setTextColor(getResources().getColor(android.R.color.black)); + + return scroller; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java new file mode 100644 index 000000000..6ea79473a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -0,0 +1,489 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * 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 android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.nfc.NdefMessage; +import android.nfc.NfcAdapter; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcelable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; + +import com.beardedhen.androidbootstrap.BootstrapButton; +import com.devspark.appmsg.AppMsg; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ActionBarHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry; +import org.sufficientlysecure.keychain.ui.dialog.BadImportKeyDialogFragment; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; +import java.util.Locale; + +public class ImportKeysActivity extends ActionBarActivity implements ActionBar.OnNavigationListener { + public static final String ACTION_IMPORT_KEY = Constants.INTENT_PREFIX + "IMPORT_KEY"; + public static final String ACTION_IMPORT_KEY_FROM_QR_CODE = Constants.INTENT_PREFIX + + "IMPORT_KEY_FROM_QR_CODE"; + public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER = Constants.INTENT_PREFIX + + "IMPORT_KEY_FROM_KEYSERVER"; + public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN = Constants.INTENT_PREFIX + + "IMPORT_KEY_FROM_KEY_SERVER_AND_RETURN"; + + // Actions for internal use only: + public static final String ACTION_IMPORT_KEY_FROM_FILE = Constants.INTENT_PREFIX + + "IMPORT_KEY_FROM_FILE"; + public static final String ACTION_IMPORT_KEY_FROM_NFC = Constants.INTENT_PREFIX + + "IMPORT_KEY_FROM_NFC"; + + // only used by ACTION_IMPORT_KEY + public static final String EXTRA_KEY_BYTES = "key_bytes"; + + // only used by ACTION_IMPORT_KEY_FROM_KEYSERVER + public static final String EXTRA_QUERY = "query"; + public static final String EXTRA_KEY_ID = "key_id"; + public static final String EXTRA_FINGERPRINT = "fingerprint"; + + // only used by ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN when used from OpenPgpService + public static final String EXTRA_PENDING_INTENT_DATA = "data"; + private Intent mPendingIntentData; + + // view + private ImportKeysListFragment mListFragment; + private String[] mNavigationStrings; + private Fragment mCurrentFragment; + private BootstrapButton mImportButton; + + private static final Class[] NAVIGATION_CLASSES = new Class[]{ + ImportKeysServerFragment.class, + ImportKeysFileFragment.class, + ImportKeysQrCodeFragment.class, + ImportKeysClipboardFragment.class, + ImportKeysNFCFragment.class + }; + + private int mCurrentNavPosition = -1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.import_keys_activity); + + mImportButton = (BootstrapButton) findViewById(R.id.import_import); + mImportButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + importKeys(); + } + }); + + mNavigationStrings = getResources().getStringArray(R.array.import_action_list); + + if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN.equals(getIntent().getAction())) { + setTitle(R.string.nav_import); + } else { + ActionBarHelper.setBackButton(this); + getSupportActionBar().setDisplayShowTitleEnabled(false); + + // set drop down navigation + Context context = getSupportActionBar().getThemedContext(); + ArrayAdapter navigationAdapter = ArrayAdapter.createFromResource(context, + R.array.import_action_list, android.R.layout.simple_spinner_dropdown_item); + getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); + getSupportActionBar().setListNavigationCallbacks(navigationAdapter, this); + } + + handleActions(savedInstanceState, getIntent()); + } + + protected void handleActions(Bundle savedInstanceState, Intent intent) { + String action = intent.getAction(); + Bundle extras = intent.getExtras(); + Uri dataUri = intent.getData(); + String scheme = intent.getScheme(); + + if (extras == null) { + extras = new Bundle(); + } + + if (Intent.ACTION_VIEW.equals(action)) { + // Android's Action when opening file associated to Keychain (see AndroidManifest.xml) + // override action to delegate it to Keychain's ACTION_IMPORT_KEY + action = ACTION_IMPORT_KEY; + } + + if (scheme != null && scheme.toLowerCase(Locale.ENGLISH).equals(Constants.FINGERPRINT_SCHEME)) { + /* Scanning a fingerprint directly with Barcode Scanner */ + loadFromFingerprintUri(savedInstanceState, dataUri); + } else if (ACTION_IMPORT_KEY.equals(action)) { + /* Keychain's own Actions */ + + // display file fragment + loadNavFragment(1, null); + + if (dataUri != null) { + // action: directly load data + startListFragment(savedInstanceState, null, dataUri, null); + } else if (extras.containsKey(EXTRA_KEY_BYTES)) { + byte[] importData = intent.getByteArrayExtra(EXTRA_KEY_BYTES); + + // action: directly load data + startListFragment(savedInstanceState, importData, null, null); + } + } else if (ACTION_IMPORT_KEY_FROM_KEYSERVER.equals(action) + || ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN.equals(action)) { + + // only used for OpenPgpService + if (extras.containsKey(EXTRA_PENDING_INTENT_DATA)) { + mPendingIntentData = extras.getParcelable(EXTRA_PENDING_INTENT_DATA); + } + if (extras.containsKey(EXTRA_QUERY) || extras.containsKey(EXTRA_KEY_ID)) { + /* simple search based on query or key id */ + + String query = null; + if (extras.containsKey(EXTRA_QUERY)) { + query = extras.getString(EXTRA_QUERY); + } else if (extras.containsKey(EXTRA_KEY_ID)) { + long keyId = intent.getLongExtra(EXTRA_KEY_ID, 0); + if (keyId != 0) { + query = PgpKeyHelper.convertKeyIdToHex(keyId); + } + } + + if (query != null && query.length() > 0) { + // display keyserver fragment with query + Bundle args = new Bundle(); + args.putString(ImportKeysServerFragment.ARG_QUERY, query); + loadNavFragment(0, args); + + // action: search immediately + startListFragment(savedInstanceState, null, null, query); + } else { + Log.e(Constants.TAG, "Query is empty!"); + return; + } + } else if (extras.containsKey(EXTRA_FINGERPRINT)) { + /* + * search based on fingerprint, here we can enforce a check in the end + * if the right key has been downloaded + */ + + String fingerprint = intent.getStringExtra(EXTRA_FINGERPRINT); + loadFromFingerprint(savedInstanceState, fingerprint); + } else { + Log.e(Constants.TAG, + "IMPORT_KEY_FROM_KEYSERVER action needs to contain the 'query', 'key_id', or " + + "'fingerprint' extra!"); + return; + } + } else if (ACTION_IMPORT_KEY_FROM_FILE.equals(action)) { + + // NOTE: this only displays the appropriate fragment, no actions are taken + loadNavFragment(1, null); + + // no immediate actions! + startListFragment(savedInstanceState, null, null, null); + } else if (ACTION_IMPORT_KEY_FROM_QR_CODE.equals(action)) { + // also exposed in AndroidManifest + + // NOTE: this only displays the appropriate fragment, no actions are taken + loadNavFragment(2, null); + + // no immediate actions! + startListFragment(savedInstanceState, null, null, null); + } else if (ACTION_IMPORT_KEY_FROM_NFC.equals(action)) { + + // NOTE: this only displays the appropriate fragment, no actions are taken + loadNavFragment(3, null); + + // no immediate actions! + startListFragment(savedInstanceState, null, null, null); + } else { + startListFragment(savedInstanceState, null, null, null); + } + } + + private void startListFragment(Bundle savedInstanceState, byte[] bytes, Uri dataUri, String serverQuery) { + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; + } + + // Create an instance of the fragment + mListFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery); + + // Add the fragment to the 'fragment_container' FrameLayout + // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + getSupportFragmentManager().beginTransaction() + .replace(R.id.import_keys_list_container, mListFragment) + .commitAllowingStateLoss(); + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); + } + + /** + * "Basically, when using a list navigation, onNavigationItemSelected() is automatically + * called when your activity is created/re-created, whether you like it or not. To prevent + * your Fragment's onCreateView() from being called twice, this initial automatic call to + * onNavigationItemSelected() should check whether the Fragment is already in existence + * inside your Activity." + *

+ * from http://stackoverflow.com/a/14295474 + *

+ * In our case, if we start ImportKeysActivity with parameters to directly search using a fingerprint, + * the fragment would be loaded twice resulting in the query being empty after the second load. + *

+ * Our solution: + * To prevent that a fragment will be loaded again even if it was already loaded loadNavFragment + * checks against mCurrentNavPosition. + * + * @param itemPosition + * @param itemId + * @return + */ + @Override + public boolean onNavigationItemSelected(int itemPosition, long itemId) { + Log.d(Constants.TAG, "onNavigationItemSelected"); + + loadNavFragment(itemPosition, null); + + return true; + } + + private void loadNavFragment(int itemPosition, Bundle args) { + if (mCurrentNavPosition != itemPosition) { + if (ActionBar.NAVIGATION_MODE_LIST == getSupportActionBar().getNavigationMode()) { + getSupportActionBar().setSelectedNavigationItem(itemPosition); + } + loadFragment(NAVIGATION_CLASSES[itemPosition], args, mNavigationStrings[itemPosition]); + mCurrentNavPosition = itemPosition; + } + } + + private void loadFragment(Class clss, Bundle args, String tag) { + mCurrentFragment = Fragment.instantiate(this, clss.getName(), args); + + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + // Replace whatever is in the fragment container with this fragment + // and give the fragment a tag name equal to the string at the position selected + ft.replace(R.id.import_navigation_fragment, mCurrentFragment, tag); + // Apply changes + ft.commit(); + } + + public void loadFromFingerprintUri(Bundle savedInstanceState, Uri dataUri) { + String fingerprint = dataUri.toString().split(":")[1].toLowerCase(Locale.ENGLISH); + + Log.d(Constants.TAG, "fingerprint: " + fingerprint); + + loadFromFingerprint(savedInstanceState, fingerprint); + } + + public void loadFromFingerprint(Bundle savedInstanceState, String fingerprint) { + if (fingerprint == null || fingerprint.length() < 40) { + AppMsg.makeText(this, R.string.import_qr_code_too_short_fingerprint, + AppMsg.STYLE_ALERT).show(); + return; + } + + String query = "0x" + fingerprint; + + // display keyserver fragment with query + Bundle args = new Bundle(); + args.putString(ImportKeysServerFragment.ARG_QUERY, query); + args.putBoolean(ImportKeysServerFragment.ARG_DISABLE_QUERY_EDIT, true); + loadNavFragment(0, args); + + // action: search directly + startListFragment(savedInstanceState, null, null, query); + } + + public void loadCallback(byte[] importData, Uri dataUri, String serverQuery, String keyServer) { + mListFragment.loadNew(importData, dataUri, serverQuery, keyServer); + } + + /** + * Import keys with mImportData + */ + public void importKeys() { + // Message is received after importing is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler( + this, + getString(R.string.progress_importing), + 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 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) { + String addedStr = getResources().getQuantityString( + R.plurals.keys_added_and_updated_1, added, added); + String updatedStr = getResources().getQuantityString( + R.plurals.keys_added_and_updated_2, updated, updated); + toastMessage = addedStr + updatedStr; + } else if (added > 0) { + toastMessage = getResources().getQuantityString(R.plurals.keys_added, + added, added); + } else if (updated > 0) { + toastMessage = getResources().getQuantityString(R.plurals.keys_updated, + updated, updated); + } else { + toastMessage = getString(R.string.no_keys_added_or_updated); + } + AppMsg.makeText(ImportKeysActivity.this, toastMessage, AppMsg.STYLE_INFO) + .show(); + if (bad > 0) { + BadImportKeyDialogFragment badImportKeyDialogFragment = + BadImportKeyDialogFragment.newInstance(bad); + badImportKeyDialogFragment.show(getSupportFragmentManager(), "badKeyDialog"); + } + + if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN.equals(getIntent().getAction())) { + ImportKeysActivity.this.setResult(Activity.RESULT_OK, mPendingIntentData); + finish(); + } + } + } + }; + + if (mListFragment.getKeyBytes() != null || mListFragment.getDataUri() != 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.setAction(KeychainIntentService.ACTION_IMPORT_KEYRING); + + // fill values for this action + Bundle data = new Bundle(); + + // get selected key entries + ArrayList selectedEntries = mListFragment.getSelectedData(); + data.putParcelableArrayList(KeychainIntentService.IMPORT_KEY_LIST, selectedEntries); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + saveHandler.showProgressDialog(this); + + // start service with intent + startService(intent); + } else if (mListFragment.getServerQuery() != null) { + // Send all information needed to service to query keys in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.setAction(KeychainIntentService.ACTION_DOWNLOAD_AND_IMPORT_KEYS); + + // fill values for this action + Bundle data = new Bundle(); + + data.putString(KeychainIntentService.DOWNLOAD_KEY_SERVER, mListFragment.getKeyServer()); + + // get selected key entries + ArrayList selectedEntries = mListFragment.getSelectedData(); + data.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST, selectedEntries); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + saveHandler.showProgressDialog(this); + + // start service with intent + startService(intent); + } else { + AppMsg.makeText(this, R.string.error_nothing_import, AppMsg.STYLE_ALERT).show(); + } + } + + /** + * NFC + */ + @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()); + } + } + + /** + * NFC + */ + @Override + public void onNewIntent(Intent intent) { + // onResume gets called after this to handle the intent + setIntent(intent); + } + + /** + * NFC: Parses the NDEF Message from the intent and prints to the TextView + */ + @SuppressLint("NewApi") + 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_KEY); + importIntent.putExtra(ImportKeysActivity.EXTRA_KEY_BYTES, receivedKeyringBytes); + + handleActions(null, importIntent); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java new file mode 100644 index 000000000..28e2091a9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; + +import java.util.Locale; + +public class ImportKeysClipboardFragment extends Fragment { + + private ImportKeysActivity mImportActivity; + private BootstrapButton mButton; + + /** + * Creates new instance of this fragment + */ + public static ImportKeysClipboardFragment newInstance() { + ImportKeysClipboardFragment frag = new ImportKeysClipboardFragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.import_keys_clipboard_fragment, container, false); + + mButton = (BootstrapButton) view.findViewById(R.id.import_clipboard_button); + mButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + CharSequence clipboardText = ClipboardReflection.getClipboardText(getActivity()); + String sendText = ""; + if (clipboardText != null) { + sendText = clipboardText.toString(); + if (sendText.toLowerCase(Locale.ENGLISH).startsWith(Constants.FINGERPRINT_SCHEME)) { + mImportActivity.loadFromFingerprintUri(null, Uri.parse(sendText)); + return; + } + } + mImportActivity.loadCallback(sendText.getBytes(), null, null, null); + } + }); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mImportActivity = (ImportKeysActivity) getActivity(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java new file mode 100644 index 000000000..31d5f3fd0 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.beardedhen.androidbootstrap.BootstrapButton; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.FileHelper; + +public class ImportKeysFileFragment extends Fragment { + private ImportKeysActivity mImportActivity; + private BootstrapButton mBrowse; + + /** + * Creates new instance of this fragment + */ + public static ImportKeysFileFragment newInstance() { + ImportKeysFileFragment frag = new ImportKeysFileFragment(); + + Bundle args = new Bundle(); + + frag.setArguments(args); + return frag; + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.import_keys_file_fragment, container, false); + + mBrowse = (BootstrapButton) view.findViewById(R.id.import_keys_file_browse); + + mBrowse.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // open .asc or .gpg files + // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc + // or gpg types! + FileHelper.openFile(ImportKeysFileFragment.this, Constants.Path.APP_DIR + "/", + "*/*", Id.request.filename); + } + }); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mImportActivity = (ImportKeysActivity) getActivity(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode & 0xFFFF) { + case Id.request.filename: { + if (resultCode == Activity.RESULT_OK && data != null) { + + // load data + mImportActivity.loadCallback(null, data.getData(), null, null); + } + + break; + } + + default: + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java new file mode 100644 index 000000000..077fa0cab --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.view.View; +import android.widget.ListView; +import com.devspark.appmsg.AppMsg; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.ui.adapter.AsyncTaskResultWrapper; +import org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter; +import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry; +import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListLoader; +import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListServerLoader; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.KeyServer; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class ImportKeysListFragment extends ListFragment implements + LoaderManager.LoaderCallbacks>> { + private static final String ARG_DATA_URI = "uri"; + private static final String ARG_BYTES = "bytes"; + private static final String ARG_SERVER_QUERY = "query"; + + private Activity mActivity; + private ImportKeysAdapter mAdapter; + + private byte[] mKeyBytes; + private Uri mDataUri; + private String mServerQuery; + private String mKeyServer; + + private static final int LOADER_ID_BYTES = 0; + private static final int LOADER_ID_SERVER_QUERY = 1; + + public byte[] getKeyBytes() { + return mKeyBytes; + } + + public Uri getDataUri() { + return mDataUri; + } + + public String getServerQuery() { + return mServerQuery; + } + + public String getKeyServer() { + return mKeyServer; + } + + public List getData() { + return mAdapter.getData(); + } + + public ArrayList getSelectedData() { + return mAdapter.getSelectedData(); + } + + /** + * Creates new instance of this fragment + */ + public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery) { + ImportKeysListFragment frag = new ImportKeysListFragment(); + + Bundle args = new Bundle(); + args.putByteArray(ARG_BYTES, bytes); + args.putParcelable(ARG_DATA_URI, dataUri); + args.putString(ARG_SERVER_QUERY, serverQuery); + + frag.setArguments(args); + + return frag; + } + + /** + * Define Adapter and Loader on create of Activity + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mActivity = getActivity(); + + // 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_nothing_import)); + + // Create an empty adapter we will use to display the loaded data. + mAdapter = new ImportKeysAdapter(mActivity); + setListAdapter(mAdapter); + + mDataUri = getArguments().getParcelable(ARG_DATA_URI); + mKeyBytes = getArguments().getByteArray(ARG_BYTES); + mServerQuery = getArguments().getString(ARG_SERVER_QUERY); + + // TODO: this is used when scanning QR Code. Currently it simply uses keyserver nr 0 + mKeyServer = Preferences.getPreferences(getActivity()) + .getKeyServers()[0]; + + if (mDataUri != null || mKeyBytes != null) { + // Start out with a progress indicator. + setListShown(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + // give arguments to onCreateLoader() + getLoaderManager().initLoader(LOADER_ID_BYTES, null, this); + } + + if (mServerQuery != null && mKeyServer != null) { + // Start out with a progress indicator. + setListShown(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + // give arguments to onCreateLoader() + getLoaderManager().initLoader(LOADER_ID_SERVER_QUERY, null, this); + } + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + super.onListItemClick(l, v, position, id); + + // Select checkbox! + // Update underlying data and notify adapter of change. The adapter will + // update the view automatically. + ImportKeysListEntry entry = mAdapter.getItem(position); + entry.setSelected(!entry.isSelected()); + mAdapter.notifyDataSetChanged(); + } + + public void loadNew(byte[] keyBytes, Uri dataUri, String serverQuery, String keyServer) { + mKeyBytes = keyBytes; + mDataUri = dataUri; + mServerQuery = serverQuery; + mKeyServer = keyServer; + + if (mKeyBytes != null || mDataUri != null) { + // Start out with a progress indicator. + setListShown(false); + + getLoaderManager().restartLoader(LOADER_ID_BYTES, null, this); + } + + if (mServerQuery != null && mKeyServer != null) { + // Start out with a progress indicator. + setListShown(false); + + getLoaderManager().restartLoader(LOADER_ID_SERVER_QUERY, null, this); + } + } + + @Override + public Loader>> + onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_ID_BYTES: { + InputData inputData = getInputData(mKeyBytes, mDataUri); + return new ImportKeysListLoader(mActivity, inputData); + } + case LOADER_ID_SERVER_QUERY: { + return new ImportKeysListServerLoader(getActivity(), mServerQuery, mKeyServer); + } + + default: + return null; + } + } + + @Override + public void onLoadFinished(Loader>> loader, + AsyncTaskResultWrapper> data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + + Log.d(Constants.TAG, "data: " + data.getResult()); + + // swap in the real data! + mAdapter.setData(data.getResult()); + mAdapter.notifyDataSetChanged(); + + setListAdapter(mAdapter); + + // The list should now be shown. + if (isResumed()) { + setListShown(true); + } else { + setListShownNoAnimation(true); + } + + Exception error = data.getError(); + + switch (loader.getId()) { + case LOADER_ID_BYTES: + + if (error == null) { + // No error + } else if (error instanceof ImportKeysListLoader.FileHasNoContent) { + AppMsg.makeText(getActivity(), R.string.error_import_file_no_content, + AppMsg.STYLE_ALERT).show(); + } else if (error instanceof ImportKeysListLoader.NonPgpPart) { + AppMsg.makeText(getActivity(), + ((ImportKeysListLoader.NonPgpPart) error).getCount() + " " + getResources(). + getQuantityString(R.plurals.error_import_non_pgp_part, + ((ImportKeysListLoader.NonPgpPart) error).getCount()), + new AppMsg.Style(AppMsg.LENGTH_LONG, R.color.confirm)).show(); + } else { + AppMsg.makeText(getActivity(), R.string.error_generic_report_bug, + new AppMsg.Style(AppMsg.LENGTH_LONG, R.color.alert)).show(); + } + break; + + case LOADER_ID_SERVER_QUERY: + + if (error == null) { + AppMsg.makeText( + getActivity(), getResources().getQuantityString(R.plurals.keys_found, + mAdapter.getCount(), mAdapter.getCount()), + AppMsg.STYLE_INFO + ).show(); + } else if (error instanceof KeyServer.InsufficientQuery) { + AppMsg.makeText(getActivity(), R.string.error_keyserver_insufficient_query, + AppMsg.STYLE_ALERT).show(); + } else if (error instanceof KeyServer.QueryException) { + AppMsg.makeText(getActivity(), R.string.error_keyserver_query, + AppMsg.STYLE_ALERT).show(); + } else if (error instanceof KeyServer.TooManyResponses) { + AppMsg.makeText(getActivity(), R.string.error_keyserver_too_many_responses, + AppMsg.STYLE_ALERT).show(); + } + break; + + default: + break; + } + } + + @Override + public void onLoaderReset(Loader>> loader) { + switch (loader.getId()) { + case LOADER_ID_BYTES: + // Clear the data in the adapter. + mAdapter.clear(); + break; + case LOADER_ID_SERVER_QUERY: + // Clear the data in the adapter. + mAdapter.clear(); + break; + default: + break; + } + } + + private InputData getInputData(byte[] importBytes, Uri dataUri) { + InputData inputData = null; + if (importBytes != null) { + inputData = new InputData(new ByteArrayInputStream(importBytes), importBytes.length); + } else if (dataUri != null) { + try { + InputStream inputStream = getActivity().getContentResolver().openInputStream(dataUri); + int length = inputStream.available(); + + inputData = new InputData(inputStream, length); + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "FileNotFoundException!", e); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException!", e); + } + } + + return inputData; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysNFCFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysNFCFragment.java new file mode 100644 index 000000000..110647284 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysNFCFragment.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import com.beardedhen.androidbootstrap.BootstrapButton; +import org.sufficientlysecure.keychain.R; + +public class ImportKeysNFCFragment extends Fragment { + + private BootstrapButton mButton; + + /** + * Creates new instance of this fragment + */ + public static ImportKeysNFCFragment newInstance() { + ImportKeysNFCFragment frag = new ImportKeysNFCFragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.import_keys_nfc_fragment, container, false); + + mButton = (BootstrapButton) view.findViewById(R.id.import_nfc_button); + mButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + // show nfc help + Intent intent = new Intent(getActivity(), HelpActivity.class); + intent.putExtra(HelpActivity.EXTRA_SELECTED_TAB, 2); + startActivityForResult(intent, 0); + } + }); + + return view; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java new file mode 100644 index 000000000..8b553d273 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import com.google.zxing.integration.android.IntentResult; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.IntentIntegratorSupportV4; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; +import java.util.Locale; + +public class ImportKeysQrCodeFragment extends Fragment { + + private ImportKeysActivity mImportActivity; + private BootstrapButton mButton; + private TextView mText; + private ProgressBar mProgress; + + private String[] mScannedContent; + + /** + * Creates new instance of this fragment + */ + public static ImportKeysQrCodeFragment newInstance() { + ImportKeysQrCodeFragment frag = new ImportKeysQrCodeFragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.import_keys_qr_code_fragment, container, false); + + mButton = (BootstrapButton) view.findViewById(R.id.import_qrcode_button); + mText = (TextView) view.findViewById(R.id.import_qrcode_text); + mProgress = (ProgressBar) view.findViewById(R.id.import_qrcode_progress); + + mButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + // scan using xzing's Barcode Scanner + new IntentIntegratorSupportV4(ImportKeysQrCodeFragment.this).initiateScan(); + } + }); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mImportActivity = (ImportKeysActivity) getActivity(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode & 0xFFFF) { + case IntentIntegratorSupportV4.REQUEST_CODE: { + IntentResult scanResult = IntentIntegratorSupportV4.parseActivityResult(requestCode, + resultCode, data); + if (scanResult != null && scanResult.getFormatName() != null) { + String scannedContent = scanResult.getContents(); + + Log.d(Constants.TAG, "scannedContent: " + scannedContent); + + // look if it's fingerprint only + if (scannedContent.toLowerCase(Locale.ENGLISH).startsWith(Constants.FINGERPRINT_SCHEME)) { + importFingerprint(Uri.parse(scanResult.getContents())); + return; + } + + // look if it is the whole key + String[] parts = scannedContent.split(","); + if (parts.length == 3) { + importParts(parts); + return; + } + + // is this a full key encoded as qr code? + if (scannedContent.startsWith("-----BEGIN PGP")) { + mImportActivity.loadCallback(scannedContent.getBytes(), null, null, null); + return; + } + + // fail... + Toast.makeText(getActivity(), R.string.import_qr_code_wrong, Toast.LENGTH_LONG) + .show(); + } + + break; + } + + default: + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + + public void importFingerprint(Uri dataUri) { + mImportActivity.loadFromFingerprintUri(null, dataUri); + } + + private void importParts(String[] parts) { + int counter = Integer.valueOf(parts[0]); + int size = Integer.valueOf(parts[1]); + String content = parts[2]; + + Log.d(Constants.TAG, "" + counter); + Log.d(Constants.TAG, "" + size); + Log.d(Constants.TAG, "" + content); + + // first qr code -> setup + if (counter == 0) { + mScannedContent = new String[size]; + mProgress.setMax(size); + mProgress.setVisibility(View.VISIBLE); + mText.setVisibility(View.VISIBLE); + } + + if (mScannedContent == null || counter > mScannedContent.length) { + Toast.makeText(getActivity(), R.string.import_qr_code_start_with_one, Toast.LENGTH_LONG) + .show(); + return; + } + + // save scanned content + mScannedContent[counter] = content; + + // get missing numbers + ArrayList missing = new ArrayList(); + for (int i = 0; i < mScannedContent.length; i++) { + if (mScannedContent[i] == null) { + missing.add(i); + } + } + + // update progress and text + int alreadyScanned = mScannedContent.length - missing.size(); + mProgress.setProgress(alreadyScanned); + + String missingString = ""; + for (int m : missing) { + if (!missingString.equals("")) { + missingString += ", "; + } + missingString += String.valueOf(m + 1); + } + + String missingText = getResources().getQuantityString(R.plurals.import_qr_code_missing, + missing.size(), missingString); + mText.setText(missingText); + + // finished! + if (missing.size() == 0) { + mText.setText(R.string.import_qr_code_finished); + String result = ""; + for (String in : mScannedContent) { + result += in; + } + mImportActivity.loadCallback(result.getBytes(), null, null, null); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java new file mode 100644 index 000000000..3eb463dac --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; +import com.beardedhen.androidbootstrap.BootstrapButton; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.util.Log; + +public class ImportKeysServerFragment extends Fragment { + public static final String ARG_QUERY = "query"; + public static final String ARG_KEY_SERVER = "key_server"; + public static final String ARG_DISABLE_QUERY_EDIT = "disable_query_edit"; + + private ImportKeysActivity mImportActivity; + + private BootstrapButton mSearchButton; + private EditText mQueryEditText; + private Spinner mServerSpinner; + private ArrayAdapter mServerAdapter; + + /** + * Creates new instance of this fragment + */ + public static ImportKeysServerFragment newInstance(String query, String keyServer) { + ImportKeysServerFragment frag = new ImportKeysServerFragment(); + + Bundle args = new Bundle(); + args.putString(ARG_QUERY, query); + args.putString(ARG_KEY_SERVER, keyServer); + + frag.setArguments(args); + + return frag; + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.import_keys_server_fragment, container, false); + + mSearchButton = (BootstrapButton) view.findViewById(R.id.import_server_search); + mQueryEditText = (EditText) view.findViewById(R.id.import_server_query); + mServerSpinner = (Spinner) view.findViewById(R.id.import_server_spinner); + + // add keyservers to spinner + mServerAdapter = new ArrayAdapter(getActivity(), + android.R.layout.simple_spinner_item, Preferences.getPreferences(getActivity()) + .getKeyServers()); + mServerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mServerSpinner.setAdapter(mServerAdapter); + if (mServerAdapter.getCount() > 0) { + mServerSpinner.setSelection(0); + } else { + mSearchButton.setEnabled(false); + } + + mSearchButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + String query = mQueryEditText.getText().toString(); + String keyServer = (String) mServerSpinner.getSelectedItem(); + search(query, keyServer); + + // close keyboard after pressing search + InputMethodManager imm = + (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mQueryEditText.getWindowToken(), 0); + } + }); + + mQueryEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + String query = mQueryEditText.getText().toString(); + String keyServer = (String) mServerSpinner.getSelectedItem(); + search(query, keyServer); + + // Don't return true to let the keyboard close itself after pressing search + return false; + } + return false; + } + }); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mImportActivity = (ImportKeysActivity) getActivity(); + + // set displayed values + if (getArguments() != null) { + if (getArguments().containsKey(ARG_QUERY)) { + String query = getArguments().getString(ARG_QUERY); + mQueryEditText.setText(query, TextView.BufferType.EDITABLE); + + Log.d(Constants.TAG, "query: " + query); + } + + if (getArguments().containsKey(ARG_KEY_SERVER)) { + String keyServer = getArguments().getString(ARG_KEY_SERVER); + int keyServerPos = mServerAdapter.getPosition(keyServer); + mServerSpinner.setSelection(keyServerPos); + + Log.d(Constants.TAG, "keyServer: " + keyServer); + } + + if (getArguments().getBoolean(ARG_DISABLE_QUERY_EDIT, false)) { + mQueryEditText.setEnabled(false); + } + } + } + + private void search(String query, String keyServer) { + mImportActivity.loadCallback(null, null, query, keyServer); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java new file mode 100644 index 000000000..8db643583 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ExportHelper; + +public class KeyListActivity extends DrawerActivity { + + ExportHelper mExportHelper; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mExportHelper = new ExportHelper(this); + + setContentView(R.layout.key_list_activity); + + // now setup navigation drawer in DrawerActivity... + setupDrawerNavigation(savedInstanceState); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.key_list, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_key_list_import: + importKeys(); + return true; + + case R.id.menu_key_list_create: + createKey(); + return true; + + case R.id.menu_key_list_create_expert: + createKeyExpert(); + return true; + + case R.id.menu_key_list_export: + mExportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + private void importKeys() { + Intent intent = new Intent(this, ImportKeysActivity.class); + startActivityForResult(intent, 0); + } + + private void createKey() { + Intent intent = new Intent(this, EditKeyActivity.class); + intent.setAction(EditKeyActivity.ACTION_CREATE_KEY); + intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true); + intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view + startActivityForResult(intent, 0); + } + + private void createKeyExpert() { + Intent intent = new Intent(this, EditKeyActivity.class); + intent.setAction(EditKeyActivity.ACTION_CREATE_KEY); + startActivityForResult(intent, 0); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java new file mode 100644 index 000000000..3e2c96464 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -0,0 +1,700 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.widget.SearchView; +import android.text.TextUtils; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.AbsListView.MultiChoiceModeListener; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import com.beardedhen.androidbootstrap.BootstrapButton; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ExportHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; +import org.sufficientlysecure.keychain.ui.adapter.HighlightQueryCursorAdapter; +import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; +import org.sufficientlysecure.keychain.util.Log; +import se.emilsjolander.stickylistheaders.ApiLevelTooLowException; +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +import java.util.HashMap; + +/** + * Public key list with sticky list headers. It does _not_ extend ListFragment because it uses + * StickyListHeaders library which does not extend upon ListView. + */ +public class KeyListFragment extends Fragment + implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener, + LoaderManager.LoaderCallbacks { + + private KeyListAdapter mAdapter; + private StickyListHeadersListView mStickyList; + + // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097 + boolean mListShown; + View mProgressContainer; + View mListContainer; + + private String mCurQuery; + private SearchView mSearchView; + // empty list layout + private BootstrapButton mButtonEmptyCreate; + private BootstrapButton mButtonEmptyImport; + + + /** + * Load custom layout with StickyListView from library + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.key_list_fragment, container, false); + + mStickyList = (StickyListHeadersListView) root.findViewById(R.id.key_list_list); + mStickyList.setOnItemClickListener(this); + + + // empty view + mButtonEmptyCreate = (BootstrapButton) root.findViewById(R.id.key_list_empty_button_create); + mButtonEmptyCreate.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(getActivity(), EditKeyActivity.class); + intent.setAction(EditKeyActivity.ACTION_CREATE_KEY); + intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true); + intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view + startActivityForResult(intent, 0); + } + }); + mButtonEmptyImport = (BootstrapButton) root.findViewById(R.id.key_list_empty_button_import); + mButtonEmptyImport.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(getActivity(), ImportKeysActivity.class); + intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE); + startActivityForResult(intent, 0); + } + }); + + // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097 + mListContainer = root.findViewById(R.id.key_list_list_container); + mProgressContainer = root.findViewById(R.id.key_list_progress_container); + mListShown = true; + + return root; + } + + /** + * Define Adapter and Loader on create of Activity + */ + @SuppressLint("NewApi") + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mStickyList.setOnItemClickListener(this); + mStickyList.setAreHeadersSticky(true); + mStickyList.setDrawingListUnderStickyHeader(false); + mStickyList.setFastScrollEnabled(true); + try { + mStickyList.setFastScrollAlwaysVisible(true); + } catch (ApiLevelTooLowException e) { + } + + /* + * ActionBarSherlock does not support MultiChoiceModeListener. Thus multi-selection is only + * available for Android >= 3.0 + */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); + mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() { + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + android.view.MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.key_list_multi, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + + // get IDs for checked positions as long array + long[] ids; + + switch (item.getItemId()) { + case R.id.menu_key_list_multi_encrypt: { + ids = mAdapter.getCurrentSelectedMasterKeyIds(); + encrypt(mode, ids); + break; + } + case R.id.menu_key_list_multi_delete: { + ids = mAdapter.getCurrentSelectedMasterKeyIds(); + showDeleteKeyDialog(mode, ids); + break; + } + case R.id.menu_key_list_multi_export: { + ids = mAdapter.getCurrentSelectedMasterKeyIds(); + ExportHelper mExportHelper = new ExportHelper((ActionBarActivity) getActivity()); + mExportHelper.showExportKeysDialog( + ids, Constants.Path.APP_DIR_FILE, mAdapter.isAnySecretSelected()); + break; + } + case R.id.menu_key_list_multi_select_all: { + // select all + for (int i = 0; i < mStickyList.getCount(); i++) { + mStickyList.setItemChecked(i, true); + } + break; + } + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mAdapter.clearSelection(); + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + if (checked) { + mAdapter.setNewSelection(position, checked); + } else { + mAdapter.removeSelection(position); + } + int count = mStickyList.getCheckedItemCount(); + String keysSelected = getResources().getQuantityString( + R.plurals.key_list_selected_keys, count, count); + mode.setTitle(keysSelected); + } + + }); + } + + // We have a menu item to show in action bar. + setHasOptionsMenu(true); + + // NOTE: Not supported by StickyListHeader, but reimplemented here + // Start out with a progress indicator. + setListShown(false); + + // Create an empty adapter we will use to display the loaded data. + mAdapter = new KeyListAdapter(getActivity(), null, Id.type.public_key); + mStickyList.setAdapter(mAdapter); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); + } + + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[]{ + KeyRings._ID, + KeyRings.MASTER_KEY_ID, + KeyRings.USER_ID, + KeyRings.IS_REVOKED, + KeyRings.VERIFIED, + KeyRings.HAS_SECRET + }; + + static final int INDEX_MASTER_KEY_ID = 1; + static final int INDEX_USER_ID = 2; + static final int INDEX_IS_REVOKED = 3; + static final int INDEX_VERIFIED = 4; + static final int INDEX_HAS_SECRET = 5; + + @Override + public Loader 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.buildUnifiedKeyRingsUri(); + String where = null; + String whereArgs[] = null; + if (mCurQuery != null) { + where = KeyRings.USER_ID + " LIKE ?"; + whereArgs = new String[]{"%" + mCurQuery + "%"}; + } + // 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, where, whereArgs, null); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.setSearchQuery(mCurQuery); + mAdapter.swapCursor(data); + + mStickyList.setAdapter(mAdapter); + + // this view is made visible if no data is available + mStickyList.setEmptyView(getActivity().findViewById(R.id.key_list_empty)); + + // NOTE: Not supported by StickyListHeader, but reimplemented here + // The list should now be shown. + if (isResumed()) { + setListShown(true); + } else { + setListShownNoAnimation(true); + } + } + + @Override + public void onLoaderReset(Loader 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); + } + + /** + * On click on item, start key view activity + */ + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + Intent viewIntent = null; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + viewIntent = new Intent(getActivity(), ViewKeyActivity.class); + } else { + viewIntent = new Intent(getActivity(), ViewKeyActivityJB.class); + } + viewIntent.setData( + KeyRings.buildGenericKeyRingUri(Long.toString(mAdapter.getMasterKeyId(position)))); + startActivity(viewIntent); + } + + @TargetApi(11) + protected void encrypt(ActionMode mode, long[] masterKeyIds) { + Intent intent = new Intent(getActivity(), EncryptActivity.class); + intent.setAction(EncryptActivity.ACTION_ENCRYPT); + intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, masterKeyIds); + // used instead of startActivity set actionbar based on callingPackage + startActivityForResult(intent, 0); + + mode.finish(); + } + + /** + * Show dialog to delete key + * + * @param masterKeyIds + */ + @TargetApi(11) + // TODO: this method needs an overhaul to handle both public and secret keys gracefully! + public void showDeleteKeyDialog(final ActionMode mode, long[] masterKeyIds) { + // Message is received after key is deleted + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) { + mode.finish(); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger, + masterKeyIds); + + deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog"); + } + + + @Override + public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { + // Get the searchview + MenuItem searchItem = menu.findItem(R.id.menu_key_list_search); + mSearchView = (SearchView) MenuItemCompat.getActionView(searchItem); + + // Execute this when searching + mSearchView.setOnQueryTextListener(this); + + // Erase search result without focus + MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + mCurQuery = null; + mSearchView.setQuery("", true); + getLoaderManager().restartLoader(0, null, KeyListFragment.this); + return true; + } + }); + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onQueryTextSubmit(String s) { + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + // Called when the action bar search text has changed. Update + // the search filter, and restart the loader to do a new query + // with this filter. + mCurQuery = !TextUtils.isEmpty(s) ? s : null; + getLoaderManager().restartLoader(0, null, this); + return true; + } + + // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097 + public void setListShown(boolean shown, boolean animate) { + if (mListShown == shown) { + return; + } + mListShown = shown; + if (shown) { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + } + mProgressContainer.setVisibility(View.GONE); + mListContainer.setVisibility(View.VISIBLE); + } else { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + } + mProgressContainer.setVisibility(View.VISIBLE); + mListContainer.setVisibility(View.INVISIBLE); + } + } + + // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097 + public void setListShown(boolean shown) { + setListShown(shown, true); + } + + // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097 + public void setListShownNoAnimation(boolean shown) { + setListShown(shown, false); + } + + /** + * Implements StickyListHeadersAdapter from library + */ + private class KeyListAdapter extends HighlightQueryCursorAdapter implements StickyListHeadersAdapter { + private LayoutInflater mInflater; + + private HashMap mSelection = new HashMap(); + + public KeyListAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + } + + @Override + public Cursor swapCursor(Cursor newCursor) { + return super.swapCursor(newCursor); + } + + /** + * Bind cursor data to the item list view + *

+ * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. + * Thus no ViewHolder is required here. + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + + { // set name and stuff, common to both key types + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + + String userId = cursor.getString(INDEX_USER_ID); + String[] userIdSplit = PgpKeyHelper.splitUserId(userId); + if (userIdSplit[0] != null) { + mainUserId.setText(highlightSearchQuery(userIdSplit[0])); + } else { + mainUserId.setText(R.string.user_id_no_name); + } + if (userIdSplit[1] != null) { + mainUserIdRest.setText(highlightSearchQuery(userIdSplit[1])); + mainUserIdRest.setVisibility(View.VISIBLE); + } else { + mainUserIdRest.setVisibility(View.GONE); + } + } + + { // set edit button and revoked info, specific by key type + View statusDivider = (View) view.findViewById(R.id.status_divider); + FrameLayout statusLayout = (FrameLayout) view.findViewById(R.id.status_layout); + Button button = (Button) view.findViewById(R.id.edit); + TextView revoked = (TextView) view.findViewById(R.id.revoked); + ImageView verified = (ImageView) view.findViewById(R.id.verified); + + if (cursor.getInt(KeyListFragment.INDEX_HAS_SECRET) != 0) { + // this is a secret key - show the edit button + statusDivider.setVisibility(View.VISIBLE); + statusLayout.setVisibility(View.VISIBLE); + revoked.setVisibility(View.GONE); + verified.setVisibility(View.GONE); + button.setVisibility(View.VISIBLE); + + final long id = cursor.getLong(INDEX_MASTER_KEY_ID); + button.setOnClickListener(new OnClickListener() { + public void onClick(View view) { + Intent editIntent = new Intent(getActivity(), EditKeyActivity.class); + editIntent.setData(KeyRingData.buildSecretKeyRingUri(Long.toString(id))); + editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY); + startActivityForResult(editIntent, 0); + } + }); + } else { + // this is a public key - hide the edit button, show if it's revoked + statusDivider.setVisibility(View.GONE); + button.setVisibility(View.GONE); + + boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; + if(isRevoked) { + statusLayout.setVisibility(isRevoked ? View.VISIBLE : View.GONE); + revoked.setVisibility(isRevoked ? View.VISIBLE : View.GONE); + verified.setVisibility(View.GONE); + } else { + boolean isVerified = cursor.getInt(INDEX_VERIFIED) > 0; + statusLayout.setVisibility(isVerified ? View.VISIBLE : View.GONE); + revoked.setVisibility(View.GONE); + verified.setVisibility(isVerified ? View.VISIBLE : View.GONE); + } + } + } + + } + + public boolean isSecretAvailable(int id) { + if (!mCursor.moveToPosition(id)) { + throw new IllegalStateException("couldn't move cursor to position " + id); + } + + return mCursor.getInt(INDEX_HAS_SECRET) != 0; + } + public long getMasterKeyId(int id) { + if (!mCursor.moveToPosition(id)) { + throw new IllegalStateException("couldn't move cursor to position " + id); + } + + return mCursor.getLong(INDEX_MASTER_KEY_ID); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.key_list_item, parent, false); + } + + /** + * Creates a new header view and binds the section headers to it. It uses the ViewHolder + * pattern. Most functionality is similar to getView() from Android's CursorAdapter. + *

+ * NOTE: The variables mDataValid and mCursor are available due to the super class + * CursorAdapter. + */ + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + HeaderViewHolder holder; + if (convertView == null) { + holder = new HeaderViewHolder(); + convertView = mInflater.inflate(R.layout.key_list_header, parent, false); + holder.mText = (TextView) convertView.findViewById(R.id.stickylist_header_text); + holder.mCount = (TextView) convertView.findViewById(R.id.contacts_num); + convertView.setTag(holder); + } else { + holder = (HeaderViewHolder) convertView.getTag(); + } + + if (!mDataValid) { + // no data available at this point + Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); + return convertView; + } + + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + if (mCursor.getInt(KeyListFragment.INDEX_HAS_SECRET) != 0) { + { // set contact count + int num = mCursor.getCount(); + String contactsTotal = getResources().getQuantityString(R.plurals.n_contacts, num, num); + holder.mCount.setText(contactsTotal); + holder.mCount.setVisibility(View.VISIBLE); + } + + holder.mText.setText(convertView.getResources().getString(R.string.my_keys)); + return convertView; + } + + // set header text as first char in user id + String userId = mCursor.getString(KeyListFragment.INDEX_USER_ID); + String headerText = convertView.getResources().getString(R.string.user_id_no_name); + if (userId != null && userId.length() > 0) { + headerText = "" + userId.subSequence(0, 1).charAt(0); + } + holder.mText.setText(headerText); + holder.mCount.setVisibility(View.GONE); + return convertView; + } + + /** + * Header IDs should be static, position=1 should always return the same Id that is. + */ + @Override + public long getHeaderId(int position) { + if (!mDataValid) { + // no data available at this point + Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); + return -1; + } + + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + // early breakout: all secret keys are assigned id 0 + if (mCursor.getInt(KeyListFragment.INDEX_HAS_SECRET) != 0) { + return 1L; + } + // otherwise, return the first character of the name as ID + String userId = mCursor.getString(KeyListFragment.INDEX_USER_ID); + if (userId != null && userId.length() > 0) { + return userId.charAt(0); + } else { + return Long.MAX_VALUE; + } + } + + class HeaderViewHolder { + TextView mText; + TextView mCount; + } + + /** + * -------------------------- MULTI-SELECTION METHODS -------------- + */ + public void setNewSelection(int position, boolean value) { + mSelection.put(position, value); + notifyDataSetChanged(); + } + + public boolean isAnySecretSelected() { + for (int pos : mSelection.keySet()) { + if(mAdapter.isSecretAvailable(pos)) + return true; + } + return false; + } + + public long[] getCurrentSelectedMasterKeyIds() { + long[] ids = new long[mSelection.size()]; + int i = 0; + // get master key ids + for (int pos : mSelection.keySet()) { + ids[i++] = mAdapter.getMasterKeyId(pos); + } + return ids; + } + + public void removeSelection(int position) { + mSelection.remove(position); + notifyDataSetChanged(); + } + + public void clearSelection() { + mSelection.clear(); + notifyDataSetChanged(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // let the adapter handle setting up the row views + View v = super.getView(position, convertView, parent); + + /** + * Change color for multi-selection + */ + if (mSelection.get(position) != null) { + // selected position color + v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis)); + } else { + // default color + v.setBackgroundColor(Color.TRANSPARENT); + } + + return v; + } + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesActivity.java new file mode 100644 index 000000000..265bb2139 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesActivity.java @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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 android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; + +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.openpgp.PGPEncryptedData; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference; + +import java.util.List; + +@SuppressLint("NewApi") +public class PreferencesActivity extends PreferenceActivity { + + public static final String ACTION_PREFS_GEN = "org.sufficientlysecure.keychain.ui.PREFS_GEN"; + public static final String ACTION_PREFS_ADV = "org.sufficientlysecure.keychain.ui.PREFS_ADV"; + + private PreferenceScreen mKeyServerPreference = null; + private static Preferences sPreferences; + + @Override + protected void onCreate(Bundle savedInstanceState) { + sPreferences = Preferences.getPreferences(this); + super.onCreate(savedInstanceState); + +// final ActionBar actionBar = getSupportActionBar(); +// actionBar.setDisplayShowTitleEnabled(true); +// actionBar.setDisplayHomeAsUpEnabled(false); +// actionBar.setHomeButtonEnabled(false); + + String action = getIntent().getAction(); + + if (action != null && action.equals(ACTION_PREFS_GEN)) { + addPreferencesFromResource(R.xml.gen_preferences); + + initializePassPassphraceCacheTtl( + (IntegerListPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_TTL)); + + mKeyServerPreference = (PreferenceScreen) findPreference(Constants.Pref.KEY_SERVERS); + String servers[] = sPreferences.getKeyServers(); + mKeyServerPreference.setSummary(getResources().getQuantityString(R.plurals.n_key_servers, + servers.length, 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, + sPreferences.getKeyServers()); + startActivityForResult(intent, Id.request.key_server_preference); + return false; + } + }); + + } else if (action != null && action.equals(ACTION_PREFS_ADV)) { + addPreferencesFromResource(R.xml.adv_preferences); + + initializeEncryptionAlgorithm( + (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_ENCRYPTION_ALGORITHM)); + + int[] valueIds = new int[]{Id.choice.compression.none, Id.choice.compression.zip, + Id.choice.compression.zlib, Id.choice.compression.bzip2, }; + String[] entries = new String[]{ + getString(R.string.choice_none) + " (" + getString(R.string.compression_fast) + ")", + "ZIP (" + getString(R.string.compression_fast) + ")", + "ZLIB (" + getString(R.string.compression_fast) + ")", + "BZIP2 (" + getString(R.string.compression_very_slow) + ")", }; + String[] values = new String[valueIds.length]; + for (int i = 0; i < values.length; ++i) { + values[i] = "" + valueIds[i]; + } + + initializeHashAlgorithm( + (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_HASH_ALGORITHM), + valueIds, entries, values); + + initializeMessageCompression( + (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_MESSAGE_COMPRESSION), + valueIds, entries, values); + + initializeFileCompression( + (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_FILE_COMPRESSION), + entries, values); + + initializeAsciiArmor( + (CheckBoxPreference) findPreference(Constants.Pref.DEFAULT_ASCII_ARMOR)); + + initializeForceV3Signatures( + (CheckBoxPreference) findPreference(Constants.Pref.FORCE_V3_SIGNATURES)); + + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + // Load the legacy preferences headers + addPreferencesFromResource(R.xml.preference_headers_legacy); + } + } + + @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); + sPreferences.setKeyServers(servers); + mKeyServerPreference.setSummary(getResources().getQuantityString( + R.plurals.n_key_servers, servers.length, servers.length)); + break; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + break; + } + } + } + + /* Called only on Honeycomb and later */ + @Override + public void onBuildHeaders(List

target) { + super.onBuildHeaders(target); + loadHeadersFromResource(R.xml.preference_headers, target); + } + + /** + * This fragment shows the general preferences in android 3.0+ + */ + public static class GeneralPrefsFragment extends PreferenceFragment { + + private PreferenceScreen mKeyServerPreference = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.gen_preferences); + + initializePassPassphraceCacheTtl( + (IntegerListPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_TTL)); + + mKeyServerPreference = (PreferenceScreen) findPreference(Constants.Pref.KEY_SERVERS); + String servers[] = sPreferences.getKeyServers(); + mKeyServerPreference.setSummary(getResources().getQuantityString(R.plurals.n_key_servers, + servers.length, servers.length)); + mKeyServerPreference + .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(getActivity(), + PreferencesKeyServerActivity.class); + intent.putExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS, + sPreferences.getKeyServers()); + startActivityForResult(intent, Id.request.key_server_preference); + return false; + } + }); + } + + @Override + public 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); + sPreferences.setKeyServers(servers); + mKeyServerPreference.setSummary(getResources().getQuantityString( + R.plurals.n_key_servers, servers.length, servers.length)); + break; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + break; + } + } + } + } + + /** + * This fragment shows the advanced preferences in android 3.0+ + */ + public static class AdvancedPrefsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.adv_preferences); + + initializeEncryptionAlgorithm( + (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_ENCRYPTION_ALGORITHM)); + + int[] valueIds = new int[]{Id.choice.compression.none, Id.choice.compression.zip, + Id.choice.compression.zlib, Id.choice.compression.bzip2, }; + String[] entries = new String[]{ + getString(R.string.choice_none) + " (" + getString(R.string.compression_fast) + ")", + "ZIP (" + getString(R.string.compression_fast) + ")", + "ZLIB (" + getString(R.string.compression_fast) + ")", + "BZIP2 (" + getString(R.string.compression_very_slow) + ")", }; + String[] values = new String[valueIds.length]; + for (int i = 0; i < values.length; ++i) { + values[i] = "" + valueIds[i]; + } + + initializeHashAlgorithm( + (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_HASH_ALGORITHM), + valueIds, entries, values); + + initializeMessageCompression( + (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_MESSAGE_COMPRESSION), + valueIds, entries, values); + + initializeFileCompression( + (IntegerListPreference) findPreference(Constants.Pref.DEFAULT_FILE_COMPRESSION), + entries, values); + + initializeAsciiArmor( + (CheckBoxPreference) findPreference(Constants.Pref.DEFAULT_ASCII_ARMOR)); + + initializeForceV3Signatures( + (CheckBoxPreference) findPreference(Constants.Pref.FORCE_V3_SIGNATURES)); + } + } + + protected boolean isValidFragment(String fragmentName) { + return AdvancedPrefsFragment.class.getName().equals(fragmentName) + || GeneralPrefsFragment.class.getName().equals(fragmentName) + || super.isValidFragment(fragmentName); + } + + private static void initializePassPassphraceCacheTtl(final IntegerListPreference mPassphraseCacheTtl) { + mPassphraseCacheTtl.setValue("" + sPreferences.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()); + sPreferences.setPassphraseCacheTtl(Integer.parseInt(newValue.toString())); + return false; + } + }); + } + + private static void initializeEncryptionAlgorithm(final IntegerListPreference mEncryptionAlgorithm) { + 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("" + sPreferences.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()); + sPreferences.setDefaultEncryptionAlgorithm(Integer.parseInt(newValue + .toString())); + return false; + } + }); + } + + private static void initializeHashAlgorithm + (final IntegerListPreference mHashAlgorithm, int[] valueIds, String[] entries, String[] values) { + 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("" + sPreferences.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()); + sPreferences.setDefaultHashAlgorithm(Integer.parseInt(newValue.toString())); + return false; + } + }); + } + + private static void initializeMessageCompression( + final IntegerListPreference mMessageCompression, + int[] valueIds, String[] entries, String[] values) { + mMessageCompression.setEntries(entries); + mMessageCompression.setEntryValues(values); + mMessageCompression.setValue("" + sPreferences.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()); + sPreferences.setDefaultMessageCompression(Integer.parseInt(newValue + .toString())); + return false; + } + }); + } + + private static void initializeFileCompression + (final IntegerListPreference mFileCompression, String[] entries, String[] values) { + mFileCompression.setEntries(entries); + mFileCompression.setEntryValues(values); + mFileCompression.setValue("" + sPreferences.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()); + sPreferences.setDefaultFileCompression(Integer.parseInt(newValue.toString())); + return false; + } + }); + } + + private static void initializeAsciiArmor(final CheckBoxPreference mAsciiArmor) { + mAsciiArmor.setChecked(sPreferences.getDefaultAsciiArmor()); + mAsciiArmor.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mAsciiArmor.setChecked((Boolean) newValue); + sPreferences.setDefaultAsciiArmor((Boolean) newValue); + return false; + } + }); + } + + private static void initializeForceV3Signatures(final CheckBoxPreference mForceV3Signatures) { + mForceV3Signatures.setChecked(sPreferences.getForceV3Signatures()); + mForceV3Signatures + .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mForceV3Signatures.setChecked((Boolean) newValue); + sPreferences.setForceV3Signatures((Boolean) newValue); + return false; + } + }); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java new file mode 100644 index 000000000..719378274 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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 android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ActionBarHelper; +import org.sufficientlysecure.keychain.ui.widget.Editor; +import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener; +import org.sufficientlysecure.keychain.ui.widget.KeyServerEditor; + +import java.util.Vector; + +public class PreferencesKeyServerActivity extends ActionBarActivity implements OnClickListener, + EditorListener { + + public static final String EXTRA_KEY_SERVERS = "key_servers"; + + private LayoutInflater mInflater; + private ViewGroup mEditors; + private View mAdd; + private TextView mTitle; + private TextView mSummary; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Inflate a "Done"/"Cancel" custom action bar view + ActionBarHelper.setTwoButtonView(getSupportActionBar(), R.string.btn_okay, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // ok + okClicked(); + } + }, R.string.btn_do_not_save, R.drawable.ic_action_cancel, new View.OnClickListener() { + @Override + public void onClick(View v) { + // cancel + cancelClicked(); + } + } + ); + + setContentView(R.layout.key_server_preference); + + mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + mTitle = (TextView) findViewById(R.id.title); + mSummary = (TextView) findViewById(R.id.summary); + + mTitle.setText(R.string.label_key_servers); + + 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 (String serv : servers) { + KeyServerEditor view = (KeyServerEditor) mInflater.inflate( + R.layout.key_server_editor, mEditors, false); + view.setEditorListener(this); + view.setValue(serv); + mEditors.addView(view); + } + } + } + + public void onDeleted(Editor editor, boolean wasNewItem) { + // nothing to do + } + + @Override + public void onEdited() { + + } + + 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 servers = new Vector(); + for (int i = 0; i < mEditors.getChildCount(); ++i) { + KeyServerEditor editor = (KeyServerEditor) mEditors.getChildAt(i); + String tmp = editor.getValue(); + if (tmp.length() > 0) { + servers.add(tmp); + } + } + String[] dummy = new String[0]; + data.putExtra(EXTRA_KEY_SERVERS, servers.toArray(dummy)); + setResult(RESULT_OK, data); + finish(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java new file mode 100644 index 000000000..874703704 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2012 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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 android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.view.View; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ActionBarHelper; + +public class SelectPublicKeyActivity extends ActionBarActivity { + + // Actions for internal use only: + public static final String ACTION_SELECT_PUBLIC_KEYS = Constants.INTENT_PREFIX + + "SELECT_PUBLIC_KEYRINGS"; + + public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "master_key_ids"; + + public static final String RESULT_EXTRA_MASTER_KEY_IDS = "master_key_ids"; + public static final String RESULT_EXTRA_USER_IDS = "user_ids"; + + SelectPublicKeyFragment mSelectFragment; + + long mSelectedMasterKeyIds[]; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Inflate a "Done"/"Cancel" custom action bar view + ActionBarHelper.setTwoButtonView(getSupportActionBar(), R.string.btn_okay, R.drawable.ic_action_done, + new View.OnClickListener() { + @Override + public void onClick(View v) { + // ok + okClicked(); + } + }, R.string.btn_do_not_save, R.drawable.ic_action_cancel, new View.OnClickListener() { + @Override + public void onClick(View v) { + // cancel + cancelClicked(); + } + } + ); + + setContentView(R.layout.select_public_key_activity); + + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + + handleIntent(getIntent()); + + // Check that the activity is using the layout version with + // the fragment_container FrameLayout + if (findViewById(R.id.select_public_key_fragment_container) != null) { + + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; + } + + // Create an instance of the fragment + mSelectFragment = SelectPublicKeyFragment.newInstance(mSelectedMasterKeyIds); + + // Add the fragment to the 'fragment_container' FrameLayout + getSupportFragmentManager().beginTransaction() + .add(R.id.select_public_key_fragment_container, mSelectFragment).commit(); + } + + // 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()); + // } + // }); + + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); + } + + private void handleIntent(Intent intent) { + // TODO: reimplement search! + + // 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 + mSelectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS); + } + + 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(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java new file mode 100644 index 000000000..9bfe3eaa9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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 android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround; +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; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter; + +import java.util.Date; +import java.util.Vector; + +public class SelectPublicKeyFragment extends ListFragmentWorkaround implements TextWatcher, + LoaderManager.LoaderCallbacks { + public static final String ARG_PRESELECTED_KEY_IDS = "preselected_key_ids"; + + private SelectKeyCursorAdapter mAdapter; + private EditText mSearchView; + private long mSelectedMasterKeyIds[]; + private String mCurQuery; + + // copied from ListFragment + static final int INTERNAL_EMPTY_ID = 0x00ff0001; + static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002; + static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003; + // added for search view + static final int SEARCH_ID = 0x00ff0004; + + /** + * Creates new instance of this fragment + */ + public static SelectPublicKeyFragment newInstance(long[] preselectedKeyIds) { + SelectPublicKeyFragment frag = new SelectPublicKeyFragment(); + Bundle args = new Bundle(); + + args.putLongArray(ARG_PRESELECTED_KEY_IDS, preselectedKeyIds); + + frag.setArguments(args); + + return frag; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSelectedMasterKeyIds = getArguments().getLongArray(ARG_PRESELECTED_KEY_IDS); + } + + /** + * Copied from ListFragment and added EditText for search on top of list. + * We do not use a custom layout here, because this breaks the progress bar functionality + * of ListFragment. + * + * @param inflater + * @param container + * @param savedInstanceState + * @return + */ + @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.FILL_PARENT, ViewGroup.LayoutParams.FILL_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.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + // Added for search view: linearLayout, mSearchView + LinearLayout linearLayout = new LinearLayout(context); + linearLayout.setOrientation(LinearLayout.VERTICAL); + + mSearchView = new EditText(context); + mSearchView.setId(SEARCH_ID); + mSearchView.setHint(R.string.menu_search); + mSearchView.setCompoundDrawablesWithIntrinsicBounds( + getResources().getDrawable(R.drawable.ic_action_search), null, null, null); + + linearLayout.addView(mSearchView, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + ListView lv = new ListView(getActivity()); + lv.setId(android.R.id.list); + lv.setDrawSelectorOnTop(false); + linearLayout.addView(lv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + lframe.addView(linearLayout, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + root.addView(lframe, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + // ------------------------------------------------------------------ + + root.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + return root; + } + + /** + * Define Adapter and Loader on create of Activity + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText(getString(R.string.list_empty)); + + mSearchView.addTextChangedListener(this); + + mAdapter = new SelectKeyCursorAdapter(getActivity(), null, 0, getListView(), 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 < getListView().getCount(); ++i) { + long keyId = mAdapter.getMasterKeyId(i); + for (long masterKeyId : masterKeyIds) { + if (keyId == masterKeyId) { + getListView().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 vector = new Vector(); + for (int i = 0; i < getListView().getCount(); ++i) { + if (getListView().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 userIds = new Vector(); + for (int i = 0; i < getListView().getCount(); ++i) { + if (getListView().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 onCreateLoader(int id, Bundle args) { + Uri baseUri = KeyRings.buildUnifiedKeyRingsUri(); + + // 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(*) FROM " + Tables.KEYS + " AS k" + +" WHERE k." + Keys.MASTER_KEY_ID + " = " + + KeychainDatabase.Tables.KEYS + "." + Keys.MASTER_KEY_ID + + " AND k." + Keys.IS_REVOKED + " = '0'" + + " AND k." + Keys.CAN_ENCRYPT + " = '1'" + + ") AS " + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE, + "(SELECT COUNT(*) FROM " + Tables.KEYS + " AS k" + + " WHERE k." + Keys.MASTER_KEY_ID + " = " + + KeychainDatabase.Tables.KEYS + "." + Keys.MASTER_KEY_ID + + " AND k." + Keys.IS_REVOKED + " = '0'" + + " AND k." + Keys.CAN_ENCRYPT + " = '1'" + + " AND k." + Keys.CREATION + " <= '" + now + "'" + + " AND ( k." + Keys.EXPIRY + " IS NULL OR k." + Keys.EXPIRY + " >= '" + now + "' )" + + ") AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, }; + + String inMasterKeyList = null; + if (mSelectedMasterKeyIds != null && mSelectedMasterKeyIds.length > 0) { + inMasterKeyList = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + " IN ("; + for (int i = 0; i < mSelectedMasterKeyIds.length; ++i) { + if (i != 0) { + inMasterKeyList += ", "; + } + inMasterKeyList += DatabaseUtils.sqlEscapeString("" + mSelectedMasterKeyIds[i]); + } + inMasterKeyList += ")"; + } + + String orderBy = UserIds.USER_ID + " ASC"; + if (inMasterKeyList != null) { + // sort by selected master keys + orderBy = inMasterKeyList + " DESC, " + orderBy; + } + String where = null; + String whereArgs[] = null; + if (mCurQuery != null) { + where = UserIds.USER_ID + " LIKE ?"; + whereArgs = new String[]{"%" + mCurQuery + "%"}; + } + + // 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, where, whereArgs, orderBy); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.setSearchQuery(mCurQuery); + 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 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); + } + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { + + } + + @Override + public void afterTextChanged(Editable editable) { + mCurQuery = !TextUtils.isEmpty(editable.toString()) ? editable.toString() : null; + getLoaderManager().restartLoader(0, null, this); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java new file mode 100644 index 000000000..0ff88d97c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; + +import org.sufficientlysecure.keychain.R; + +public class SelectSecretKeyActivity extends ActionBarActivity { + + public static final String EXTRA_FILTER_CERTIFY = "filter_certify"; + + public static final String RESULT_EXTRA_MASTER_KEY_ID = "master_key_id"; + + private boolean mFilterCertify; + private SelectSecretKeyFragment mSelectFragment; + + @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); + + mFilterCertify = getIntent().getBooleanExtra(EXTRA_FILTER_CERTIFY, false); + + // Check that the activity is using the layout version with + // the fragment_container FrameLayout + if (findViewById(R.id.select_secret_key_fragment_container) != null) { + + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; + } + + // Create an instance of the fragment + mSelectFragment = SelectSecretKeyFragment.newInstance(mFilterCertify); + + // Add the fragment to the 'fragment_container' FrameLayout + getSupportFragmentManager().beginTransaction() + .add(R.id.select_secret_key_fragment_container, mSelectFragment).commit(); + } + } + + /** + * This is executed by SelectSecretKeyFragment after clicking on an item + * + * @param selectedUri + */ + public void afterListSelection(Uri selectedUri) { + Intent data = new Intent(); + data.setData(selectedUri); + + setResult(RESULT_OK, data); + finish(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java new file mode 100644 index 000000000..9987facbc --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2010 Thialfihar + * + * 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 android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +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; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter; + +import java.util.Date; + +public class SelectSecretKeyFragment extends ListFragment implements + LoaderManager.LoaderCallbacks { + + private SelectSecretKeyActivity mActivity; + private SelectKeyCursorAdapter mAdapter; + private ListView mListView; + + private boolean mFilterCertify; + + private static final String ARG_FILTER_CERTIFY = "filter_certify"; + + /** + * Creates new instance of this fragment + */ + public static SelectSecretKeyFragment newInstance(boolean filterCertify) { + SelectSecretKeyFragment frag = new SelectSecretKeyFragment(); + + Bundle args = new Bundle(); + args.putBoolean(ARG_FILTER_CERTIFY, filterCertify); + frag.setArguments(args); + + return frag; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mFilterCertify = getArguments().getBoolean(ARG_FILTER_CERTIFY); + } + + /** + * Define Adapter and Loader on create of Activity + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mActivity = (SelectSecretKeyActivity) getActivity(); + mListView = getListView(); + + mListView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + long masterKeyId = mAdapter.getMasterKeyId(position); + Uri result = KeyRings.buildGenericKeyRingUri(String.valueOf(masterKeyId)); + + // return data to activity, which results in finishing it + mActivity.afterListSelection(result); + } + }); + + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText(getString(R.string.list_empty)); + + mAdapter = new SelectKeyCursorAdapter(mActivity, null, 0, mListView, 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 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.buildUnifiedKeyRingsUri(); + + // 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(*) FROM " + Tables.KEYS + " AS k" + + " WHERE k." + Keys.MASTER_KEY_ID + " = " + + KeychainDatabase.Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + + " AND k." + Keys.CAN_CERTIFY + " = '1'" + + ") AS cert", + "(SELECT COUNT(*) FROM " + Tables.KEYS + " AS k" + +" WHERE k." + Keys.MASTER_KEY_ID + " = " + + KeychainDatabase.Tables.KEYS + "." + Keys.MASTER_KEY_ID + + " AND k." + Keys.IS_REVOKED + " = '0'" + + " AND k." + Keys.CAN_SIGN + " = '1'" + + ") AS " + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE, + "(SELECT COUNT(*) FROM " + Tables.KEYS + " AS k" + + " WHERE k." + Keys.MASTER_KEY_ID + " = " + + KeychainDatabase.Tables.KEYS + "." + Keys.MASTER_KEY_ID + + " AND k." + Keys.IS_REVOKED + " = '0'" + + " AND k." + Keys.CAN_SIGN + " = '1'" + + " AND k." + Keys.CREATION + " <= '" + now + "'" + + " AND ( k." + Keys.EXPIRY + " IS NULL OR k." + Keys.EXPIRY + " >= '" + now + "' )" + + ") AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, }; + + String orderBy = UserIds.USER_ID + " ASC"; + + String where = Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + " IS NOT NULL"; + if (mFilterCertify) { + where += " AND (cert > 0)"; + } + + // 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, where, null, orderBy); + } + + @Override + public void onLoadFinished(Loader 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 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyLayoutFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyLayoutFragment.java new file mode 100644 index 000000000..514951385 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SelectSecretKeyLayoutFragment.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; + +public class SelectSecretKeyLayoutFragment extends Fragment implements LoaderManager.LoaderCallbacks { + + private TextView mKeyUserId; + private TextView mKeyUserIdRest; + private TextView mKeyMasterKeyIdHex; + private TextView mNoKeySelected; + private BootstrapButton mSelectKeyButton; + private Boolean mFilterCertify; + + private Uri mReceivedUri = null; + + private SelectSecretKeyCallback mCallback; + + private static final int REQUEST_CODE_SELECT_KEY = 8882; + + private static final int LOADER_ID = 0; + + //The Projection we will retrieve, Master Key ID is for convenience sake, + //to avoid having to pass the Key Around + final String[] PROJECTION = new String[] { + KeychainContract.Keys.MASTER_KEY_ID, + KeychainContract.UserIds.USER_ID + }; + final int INDEX_MASTER_KEY_ID = 0; + final int INDEX_USER_ID = 1; + + public interface SelectSecretKeyCallback { + void onKeySelected(long secretKeyId); + } + + public void setCallback(SelectSecretKeyCallback callback) { + mCallback = callback; + } + + public void setFilterCertify(Boolean filterCertify) { + mFilterCertify = filterCertify; + } + + public void setNoKeySelected() { + mNoKeySelected.setVisibility(View.VISIBLE); + mKeyUserId.setVisibility(View.GONE); + mKeyUserIdRest.setVisibility(View.GONE); + mKeyMasterKeyIdHex.setVisibility(View.GONE); + } + + public void setSelectedKeyData(String userName, String email, String masterKeyHex) { + + mNoKeySelected.setVisibility(View.GONE); + + mKeyUserId.setText(userName); + mKeyUserIdRest.setText(email); + mKeyMasterKeyIdHex.setText(masterKeyHex); + + mKeyUserId.setVisibility(View.VISIBLE); + mKeyUserIdRest.setVisibility(View.VISIBLE); + mKeyMasterKeyIdHex.setVisibility(View.VISIBLE); + + } + + public void setError(String error) { + mNoKeySelected.requestFocus(); + mNoKeySelected.setError(error); + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.select_secret_key_layout_fragment, container, false); + + mNoKeySelected = (TextView) view.findViewById(R.id.no_key_selected); + mKeyUserId = (TextView) view.findViewById(R.id.select_secret_key_user_id); + mKeyUserIdRest = (TextView) view.findViewById(R.id.select_secret_key_user_id_rest); + mKeyMasterKeyIdHex = (TextView) view.findViewById(R.id.select_secret_key_master_key_hex); + mSelectKeyButton = (BootstrapButton) view + .findViewById(R.id.select_secret_key_select_key_button); + mFilterCertify = false; + mSelectKeyButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + startSelectKeyActivity(); + } + }); + + return view; + } + + //For AppSettingsFragment + public void selectKey(long masterKeyId) { + Uri buildUri = KeychainContract.KeyRings.buildGenericKeyRingUri(String.valueOf(masterKeyId)); + mReceivedUri = buildUri; + getActivity().getSupportLoaderManager().restartLoader(LOADER_ID, null, this); + } + + private void startSelectKeyActivity() { + Intent intent = new Intent(getActivity(), SelectSecretKeyActivity.class); + intent.putExtra(SelectSecretKeyActivity.EXTRA_FILTER_CERTIFY, mFilterCertify); + startActivityForResult(intent, REQUEST_CODE_SELECT_KEY); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + Uri uri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mReceivedUri); + //We don't care about the Loader id + return new CursorLoader(getActivity(), uri, PROJECTION, null, null, null); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (data.moveToFirst()) { + String userName, email, masterKeyHex; + String userID = data.getString(INDEX_USER_ID); + long masterKeyID = data.getLong(INDEX_MASTER_KEY_ID); + + String splitUserID[] = PgpKeyHelper.splitUserId(userID); + + if (splitUserID[0] != null) { + userName = splitUserID[0]; + } else { + userName = getActivity().getResources().getString(R.string.user_id_no_name); + } + + if (splitUserID[1] != null) { + email = splitUserID[1]; + } else { + email = getActivity().getResources().getString(R.string.error_user_id_no_email); + } + + //TODO Can the cursor return invalid values for the Master Key ? + masterKeyHex = PgpKeyHelper.convertKeyIdToHexShort(masterKeyID); + + //Set the data + setSelectedKeyData(userName, email, masterKeyHex); + + //Give value to the callback + mCallback.onKeySelected(masterKeyID); + } else { + //Set The empty View + setNoKeySelected(); + } + + } + + @Override + public void onLoaderReset(Loader loader) { + return; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_SELECT_KEY: { + if (resultCode == Activity.RESULT_OK) { + mReceivedUri = data.getData(); + + //Must be restartLoader() or the data will not be updated on selecting a new key + getActivity().getSupportLoaderManager().restartLoader(0, null, this); + + mKeyUserId.setError(null); + } + break; + } + + default: + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java new file mode 100644 index 000000000..0e231e6a8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * 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 android.app.ProgressDialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.support.v7.app.ActionBarActivity; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.Toast; +import com.beardedhen.androidbootstrap.BootstrapButton; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.Preferences; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.util.Log; + +/** + * Sends the selected public key to a keyserver + */ +public class UploadKeyActivity extends ActionBarActivity { + private BootstrapButton mUploadButton; + private Spinner mKeyServerSpinner; + + private Uri mDataUri; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.key_server_export); + + mUploadButton = (BootstrapButton) findViewById(R.id.btn_export_to_server); + mKeyServerSpinner = (Spinner) findViewById(R.id.sign_key_keyserver); + + ArrayAdapter adapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, Preferences.getPreferences(this) + .getKeyServers()); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mKeyServerSpinner.setAdapter(adapter); + if (adapter.getCount() > 0) { + mKeyServerSpinner.setSelection(0); + } else { + mUploadButton.setEnabled(false); + } + + mUploadButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + uploadKey(); + } + }); + + mDataUri = getIntent().getData(); + if (mDataUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!"); + finish(); + return; + } + } + + private void uploadKey() { + // Send all information needed to service to upload key in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.setAction(KeychainIntentService.ACTION_UPLOAD_KEYRING); + + // set data uri as path to keyring + intent.setData(mDataUri); + + // fill values for this action + Bundle data = new Bundle(); + + String server = (String) mKeyServerSpinner.getSelectedItem(); + data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Message is received after uploading is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, + 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) { + + Toast.makeText(UploadKeyActivity.this, R.string.key_send_success, + Toast.LENGTH_SHORT).show(); + finish(); + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + saveHandler.showProgressDialog(this); + + // start service with intent + startService(intent); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java new file mode 100644 index 000000000..294fadab2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.text.format.DateFormat; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; + +import org.spongycastle.bcpg.SignatureSubpacket; +import org.spongycastle.bcpg.SignatureSubpacketTags; +import org.spongycastle.bcpg.sig.RevocationReason; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpConversionHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; + +import java.security.SignatureException; +import java.util.Date; + +public class ViewCertActivity extends ActionBarActivity + implements LoaderManager.LoaderCallbacks { + + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[]{ + Certs.MASTER_KEY_ID, + Certs.USER_ID, + Certs.TYPE, + Certs.CREATION, + Certs.KEY_ID_CERTIFIER, + Certs.SIGNER_UID, + Certs.DATA, + }; + private static final int INDEX_MASTER_KEY_ID = 0; + private static final int INDEX_USER_ID = 1; + private static final int INDEX_TYPE = 2; + private static final int INDEX_CREATION = 3; + private static final int INDEX_KEY_ID_CERTIFIER = 4; + private static final int INDEX_SIGNER_UID = 5; + private static final int INDEX_DATA = 6; + + private Uri mDataUri; + + private long mSignerKeyId; + + private TextView mSigneeKey, mSigneeUid, mAlgorithm, mType, mRReason, mCreation; + private TextView mSignerKey, mSignerUid, mStatus; + private View mRowReason; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + + setContentView(R.layout.view_cert_activity); + + mStatus = (TextView) findViewById(R.id.status); + mSigneeKey = (TextView) findViewById(R.id.signee_key); + mSigneeUid = (TextView) findViewById(R.id.signee_uid); + mAlgorithm = (TextView) findViewById(R.id.algorithm); + mType = (TextView) findViewById(R.id.signature_type); + mRReason = (TextView) findViewById(R.id.reason); + mCreation = (TextView) findViewById(R.id.creation); + + mSignerKey = (TextView) findViewById(R.id.signer_key_id); + mSignerUid = (TextView) findViewById(R.id.signer_uid); + + mRowReason = findViewById(R.id.row_reason); + + mDataUri = getIntent().getData(); + if (mDataUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!"); + finish(); + return; + } + + getSupportLoaderManager().initLoader(0, null, this); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(this, mDataUri, PROJECTION, null, null, null); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (data.moveToFirst()) { + String signeeKey = "0x" + PgpKeyHelper.convertKeyIdToHex(data.getLong(INDEX_MASTER_KEY_ID)); + mSigneeKey.setText(signeeKey); + + String signeeUid = data.getString(INDEX_USER_ID); + mSigneeUid.setText(signeeUid); + + Date creationDate = new Date(data.getLong(INDEX_CREATION) * 1000); + mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format(creationDate)); + + mSignerKeyId = data.getLong(INDEX_KEY_ID_CERTIFIER); + String signerKey = "0x" + PgpKeyHelper.convertKeyIdToHex(mSignerKeyId); + mSignerKey.setText(signerKey); + + String signerUid = data.getString(INDEX_SIGNER_UID); + if (signerUid != null) { + mSignerUid.setText(signerUid); + } else { + mSignerUid.setText(R.string.unknown_uid); + } + + PGPSignature sig = PgpConversionHelper.BytesToPGPSignature(data.getBlob(INDEX_DATA)); + PGPKeyRing signeeRing = ProviderHelper.getPGPKeyRing(this, + KeychainContract.KeyRingData.buildPublicKeyRingUri( + Long.toString(data.getLong(INDEX_MASTER_KEY_ID)))); + PGPKeyRing signerRing = ProviderHelper.getPGPKeyRing(this, + KeychainContract.KeyRingData.buildPublicKeyRingUri( + Long.toString(sig.getKeyID()))); + + if (signerRing != null) { + try { + sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider( + Constants.BOUNCY_CASTLE_PROVIDER_NAME), signeeRing.getPublicKey()); + if (sig.verifyCertification(signeeUid, signerRing.getPublicKey())) { + mStatus.setText("ok"); + mStatus.setTextColor(getResources().getColor(R.color.bbutton_success)); + } else { + mStatus.setText("failed!"); + mStatus.setTextColor(getResources().getColor(R.color.alert)); + } + } catch (SignatureException e) { + mStatus.setText("error!"); + mStatus.setTextColor(getResources().getColor(R.color.alert)); + } catch (PGPException e) { + mStatus.setText("error!"); + mStatus.setTextColor(getResources().getColor(R.color.alert)); + } + } else { + mStatus.setText("key unavailable"); + mStatus.setTextColor(getResources().getColor(R.color.black)); + } + + String algorithmStr = PgpKeyHelper.getAlgorithmInfo(sig.getKeyAlgorithm(), 0); + mAlgorithm.setText(algorithmStr); + + mRowReason.setVisibility(View.GONE); + switch (data.getInt(INDEX_TYPE)) { + case PGPSignature.DEFAULT_CERTIFICATION: + mType.setText(R.string.cert_default); + break; + case PGPSignature.NO_CERTIFICATION: + mType.setText(R.string.cert_none); + break; + case PGPSignature.CASUAL_CERTIFICATION: + mType.setText(R.string.cert_casual); + break; + case PGPSignature.POSITIVE_CERTIFICATION: + mType.setText(R.string.cert_positive); + break; + case PGPSignature.CERTIFICATION_REVOCATION: { + mType.setText(R.string.cert_revoke); + if (sig.getHashedSubPackets().hasSubpacket(SignatureSubpacketTags.REVOCATION_REASON)) { + SignatureSubpacket p = sig.getHashedSubPackets().getSubpacket( + SignatureSubpacketTags.REVOCATION_REASON); + // For some reason, this is missing in SignatureSubpacketInputStream:146 + if (!(p instanceof RevocationReason)) { + p = new RevocationReason(false, p.getData()); + } + String reason = ((RevocationReason) p).getRevocationDescription(); + mRReason.setText(reason); + mRowReason.setVisibility(View.VISIBLE); + } + break; + } + } + } + } + + @Override + public void onLoaderReset(Loader loader) { + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.view_cert, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_view_cert_view_signer: + // can't do this before the data is initialized + Intent viewIntent = null; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + viewIntent = new Intent(this, ViewKeyActivity.class); + } else { + viewIntent = new Intent(this, ViewKeyActivityJB.class); + } + // + long signerMasterKeyId = ProviderHelper.getMasterKeyId(this, + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(Long.toString(mSignerKeyId)) + ); + // TODO notify user of this, maybe offer download? + if (mSignerKeyId == 0L) + return true; + viewIntent.setData(KeyRings.buildGenericKeyRingUri( + Long.toString(signerMasterKeyId)) + ); + startActivity(viewIntent); + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java new file mode 100644 index 000000000..cce34139c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * Copyright (C) 2013 Bahtiar 'kalkin' Gadimov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.Window; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.helper.ExportHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.adapter.TabsAdapter; +import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.ShareQrCodeDialogFragment; + +import java.util.ArrayList; +import java.util.HashMap; + +public class ViewKeyActivity extends ActionBarActivity { + + ExportHelper mExportHelper; + + protected Uri mDataUri; + + public static final String EXTRA_SELECTED_TAB = "selectedTab"; + + ViewPager mViewPager; + TabsAdapter mTabsAdapter; + + private static final int RESULT_CODE_LOOKUP_KEY = 0x00007006; + + @Override + protected void onCreate(Bundle savedInstanceState) { + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + super.onCreate(savedInstanceState); + + mExportHelper = new ExportHelper(this); + + // let the actionbar look like Android's contact app + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setIcon(android.R.color.transparent); + actionBar.setHomeButtonEnabled(true); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + + setContentView(R.layout.view_key_activity); + + mViewPager = (ViewPager) findViewById(R.id.pager); + + 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); + } + + mDataUri = getIntent().getData(); + + Bundle mainBundle = new Bundle(); + mainBundle.putParcelable(ViewKeyMainFragment.ARG_DATA_URI, mDataUri); + mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.key_view_tab_main)), + ViewKeyMainFragment.class, mainBundle, (selectedTab == 0)); + + Bundle certBundle = new Bundle(); + certBundle.putParcelable(ViewKeyCertsFragment.ARG_DATA_URI, mDataUri); + mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.key_view_tab_certs)), + ViewKeyCertsFragment.class, certBundle, (selectedTab == 1)); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.key_view, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent homeIntent = new Intent(this, KeyListActivity.class); + homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(homeIntent); + return true; + case R.id.menu_key_view_update: + updateFromKeyserver(mDataUri); + return true; + case R.id.menu_key_view_export_keyserver: + uploadToKeyserver(mDataUri); + return true; + case R.id.menu_key_view_export_file: + exportToFile(mDataUri); + return true; + case R.id.menu_key_view_share_default_fingerprint: + shareKey(mDataUri, true); + return true; + case R.id.menu_key_view_share_default: + shareKey(mDataUri, false); + return true; + case R.id.menu_key_view_share_qr_code_fingerprint: + shareKeyQrCode(mDataUri, true); + return true; + case R.id.menu_key_view_share_qr_code: + shareKeyQrCode(mDataUri, false); + return true; + case R.id.menu_key_view_share_nfc: + shareNfc(); + return true; + case R.id.menu_key_view_share_clipboard: + copyToClipboard(mDataUri); + return true; + case R.id.menu_key_view_delete: { + deleteKey(mDataUri); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + private void exportToFile(Uri dataUri) { + Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri); + + HashMap data = ProviderHelper.getGenericData(this, + baseUri, + new String[]{KeychainContract.Keys.MASTER_KEY_ID, KeychainContract.KeyRings.HAS_SECRET}, + new int[]{ProviderHelper.FIELD_TYPE_INTEGER, ProviderHelper.FIELD_TYPE_INTEGER}); + + mExportHelper.showExportKeysDialog( + new long[]{(Long) data.get(KeychainContract.KeyRings.MASTER_KEY_ID)}, + Constants.Path.APP_DIR_FILE, + ((Long) data.get(KeychainContract.KeyRings.HAS_SECRET) == 1) + ); + } + + private void uploadToKeyserver(Uri dataUri) { + Intent uploadIntent = new Intent(this, UploadKeyActivity.class); + uploadIntent.setData(dataUri); + startActivityForResult(uploadIntent, Id.request.export_to_server); + } + + private void updateFromKeyserver(Uri dataUri) { + byte[] blob = (byte[]) ProviderHelper.getGenericData( + this, KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri), + KeychainContract.Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); + String fingerprint = PgpKeyHelper.convertFingerprintToHex(blob); + + Intent queryIntent = new Intent(this, ImportKeysActivity.class); + queryIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN); + queryIntent.putExtra(ImportKeysActivity.EXTRA_FINGERPRINT, fingerprint); + + startActivityForResult(queryIntent, RESULT_CODE_LOOKUP_KEY); + } + + private void shareKey(Uri dataUri, boolean fingerprintOnly) { + String content; + if (fingerprintOnly) { + byte[] data = (byte[]) ProviderHelper.getGenericData( + this, KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri), + KeychainContract.Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); + if (data != null) { + String fingerprint = PgpKeyHelper.convertFingerprintToHex(data); + content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; + } else { + Toast.makeText(getApplicationContext(), "Bad key selected!", + Toast.LENGTH_LONG).show(); + return; + } + } else { + // get public keyring as ascii armored string + long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); + ArrayList keyringArmored = ProviderHelper.getKeyRingsAsArmoredString( + this, new long[]{masterKeyId}); + + content = keyringArmored.get(0); + + // Android will fail with android.os.TransactionTooLargeException if key is too big + // see http://www.lonestarprod.com/?p=34 + if (content.length() >= 86389) { + Toast.makeText(getApplicationContext(), R.string.key_too_big_for_sharing, + Toast.LENGTH_LONG).show(); + return; + } + } + + // let user choose application + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, content); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, + getResources().getText(R.string.action_share_key_with))); + } + + private void shareKeyQrCode(Uri dataUri, boolean fingerprintOnly) { + ShareQrCodeDialogFragment dialog = ShareQrCodeDialogFragment.newInstance(dataUri, + fingerprintOnly); + dialog.show(getSupportFragmentManager(), "shareQrCodeDialog"); + } + + private void copyToClipboard(Uri dataUri) { + // get public keyring as ascii armored string + long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); + ArrayList keyringArmored = ProviderHelper.getKeyRingsAsArmoredString( + this, new long[]{masterKeyId}); + + ClipboardReflection.copyToClipboard(this, keyringArmored.get(0)); + Toast.makeText(getApplicationContext(), R.string.key_copied_to_clipboard, Toast.LENGTH_LONG) + .show(); + } + + private void shareNfc() { + ShareNfcDialogFragment dialog = ShareNfcDialogFragment.newInstance(); + dialog.show(getSupportFragmentManager(), "shareNfcDialog"); + } + + private void deleteKey(Uri dataUri) { + // Message is received after key is deleted + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + setResult(RESULT_CANCELED); + finish(); + } + }; + + mExportHelper.deleteKey(dataUri, returnHandler); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case RESULT_CODE_LOOKUP_KEY: { + if (resultCode == Activity.RESULT_OK) { + // TODO: reload key??? move this into fragment? + } + break; + } + + default: { + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivityJB.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivityJB.java new file mode 100644 index 000000000..6dc0413bb --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivityJB.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.annotation.TargetApi; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.NfcAdapter.CreateNdefMessageCallback; +import android.nfc.NfcAdapter.OnNdefPushCompleteCallback; +import android.nfc.NfcEvent; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.widget.Toast; + +import com.devspark.appmsg.AppMsg; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.IOException; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public class ViewKeyActivityJB extends ViewKeyActivity implements CreateNdefMessageCallback, + OnNdefPushCompleteCallback { + + private NfcAdapter mNfcAdapter; + private byte[] mSharedKeyringBytes; + private static final int NFC_SENT = 1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initNfc(); + } + + /** + * NFC: Initialize NFC sharing if OS and device supports it + */ + private void initNfc() { + // check if NFC Beam is supported (>= Android 4.1) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // Check for available NFC Adapter + mNfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (mNfcAdapter != null) { + // init nfc + // Register callback to set NDEF message + mNfcAdapter.setNdefPushMessageCallback(this, this); + // Register callback to listen for message-sent success + mNfcAdapter.setOnNdefPushCompleteCallback(this, this); + } + } + } + + /** + * NFC: 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. + */ + try { + // get public keyring as byte array + mSharedKeyringBytes = ProviderHelper.getPGPKeyRing(this, mDataUri).getEncoded(); + + NdefMessage msg = new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME, + mSharedKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME)); + return msg; + } catch(IOException e) { + Log.e(Constants.TAG, "Error parsing keyring", e); + return null; + } + } + + /** + * NFC: 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 + mNfcHandler.obtainMessage(NFC_SENT).sendToTarget(); + } + + /** + * NFC: This handler receives a message from onNdefPushComplete + */ + private final Handler mNfcHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case NFC_SENT: + AppMsg.makeText(ViewKeyActivityJB.this, R.string.nfc_successfull, + AppMsg.STYLE_INFO).show(); + break; + } + } + }; + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java new file mode 100644 index 000000000..b738970f1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.TextView; + +import org.spongycastle.openpgp.PGPSignature; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.util.Log; + +import se.emilsjolander.stickylistheaders.ApiLevelTooLowException; +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + + +public class ViewKeyCertsFragment extends Fragment + implements LoaderManager.LoaderCallbacks, AdapterView.OnItemClickListener { + + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[] { + Certs._ID, + Certs.MASTER_KEY_ID, + Certs.VERIFIED, + Certs.TYPE, + Certs.RANK, + Certs.KEY_ID_CERTIFIER, + Certs.USER_ID, + Certs.SIGNER_UID + }; + + // sort by our user id, + static final String SORT_ORDER = + Tables.CERTS + "." + Certs.RANK + " ASC, " + + Certs.VERIFIED + " DESC, " + Certs.TYPE + " DESC, " + Certs.SIGNER_UID + " ASC"; + + public static final String ARG_DATA_URI = "data_uri"; + + private StickyListHeadersListView mStickyList; + private CertListAdapter mAdapter; + + private Uri mDataUri; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.view_key_certs_fragment, container, false); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mStickyList = (StickyListHeadersListView) getActivity().findViewById(R.id.list); + + if (!getArguments().containsKey(ARG_DATA_URI)) { + Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); + getActivity().finish(); + return; + } + + Uri uri = getArguments().getParcelable(ARG_DATA_URI); + mDataUri = Certs.buildCertsUri(uri); + + mStickyList.setAreHeadersSticky(true); + mStickyList.setDrawingListUnderStickyHeader(false); + mStickyList.setFastScrollEnabled(true); + mStickyList.setOnItemClickListener(this); + + try { + mStickyList.setFastScrollAlwaysVisible(true); + } catch (ApiLevelTooLowException e) { + } + + mStickyList.setEmptyView(getActivity().findViewById(R.id.empty)); + + // TODO this view is made visible if no data is available + // mStickyList.setEmptyView(getActivity().findViewById(R.id.empty)); + + + // Create an empty adapter we will use to display the loaded data. + mAdapter = new CertListAdapter(getActivity(), null); + mStickyList.setAdapter(mAdapter); + + getLoaderManager().initLoader(0, null, this); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(getActivity(), mDataUri, PROJECTION, null, null, SORT_ORDER); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.swapCursor(data); + + mStickyList.setAdapter(mAdapter); + } + + /** + * On click on item, start key view activity + */ + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + if(view.getTag(R.id.tag_mki) != null) { + long masterKeyId = (Long) view.getTag(R.id.tag_mki); + long rank = (Long) view.getTag(R.id.tag_rank); + long certifierId = (Long) view.getTag(R.id.tag_certifierId); + + Intent viewIntent = new Intent(getActivity(), ViewCertActivity.class); + viewIntent.setData(Certs.buildCertsSpecificUri( + Long.toString(masterKeyId), Long.toString(rank), Long.toString(certifierId))); + startActivity(viewIntent); + } + } + + @Override + public void onLoaderReset(Loader 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); + } + + /** + * Implements StickyListHeadersAdapter from library + */ + private class CertListAdapter extends CursorAdapter implements StickyListHeadersAdapter { + private LayoutInflater mInflater; + private int mIndexMasterKeyId, mIndexUserId, mIndexRank; + private int mIndexSignerKeyId, mIndexSignerUserId; + private int mIndexVerified, mIndexType; + + public CertListAdapter(Context context, Cursor c) { + super(context, c, 0); + + mInflater = LayoutInflater.from(context); + initIndex(c); + } + + @Override + public Cursor swapCursor(Cursor newCursor) { + initIndex(newCursor); + + return super.swapCursor(newCursor); + } + + /** + * Get column indexes for performance reasons just once in constructor and swapCursor. For a + * performance comparison see http://stackoverflow.com/a/17999582 + * + * @param cursor + */ + private void initIndex(Cursor cursor) { + if (cursor != null) { + mIndexMasterKeyId = cursor.getColumnIndexOrThrow(Certs.MASTER_KEY_ID); + mIndexUserId = cursor.getColumnIndexOrThrow(Certs.USER_ID); + mIndexRank = cursor.getColumnIndexOrThrow(Certs.RANK); + mIndexType = cursor.getColumnIndexOrThrow(Certs.TYPE); + mIndexVerified = cursor.getColumnIndexOrThrow(Certs.VERIFIED); + mIndexSignerKeyId = cursor.getColumnIndexOrThrow(Certs.KEY_ID_CERTIFIER); + mIndexSignerUserId = cursor.getColumnIndexOrThrow(Certs.SIGNER_UID); + } + } + + /** + * Bind cursor data to the item list view + *

+ * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. + * Thus no ViewHolder is required here. + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + + // set name and stuff, common to both key types + TextView wSignerKeyId = (TextView) view.findViewById(R.id.signerKeyId); + TextView wSignerUserId = (TextView) view.findViewById(R.id.signerUserId); + TextView wSignStatus = (TextView) view.findViewById(R.id.signStatus); + + String signerKeyId = PgpKeyHelper.convertKeyIdToHex(cursor.getLong(mIndexSignerKeyId)); + String signerUserId = cursor.getString(mIndexSignerUserId); + switch(cursor.getInt(mIndexType)) { + case PGPSignature.DEFAULT_CERTIFICATION: // 0x10 + wSignStatus.setText(R.string.cert_default); break; + case PGPSignature.NO_CERTIFICATION: // 0x11 + wSignStatus.setText(R.string.cert_none); break; + case PGPSignature.CASUAL_CERTIFICATION: // 0x12 + wSignStatus.setText(R.string.cert_casual); break; + case PGPSignature.POSITIVE_CERTIFICATION: // 0x13 + wSignStatus.setText(R.string.cert_positive); break; + case PGPSignature.CERTIFICATION_REVOCATION: // 0x30 + wSignStatus.setText(R.string.cert_revoke); break; + } + + wSignerUserId.setText(signerUserId); + wSignerKeyId.setText(signerKeyId); + + view.setTag(R.id.tag_mki, cursor.getLong(mIndexMasterKeyId)); + view.setTag(R.id.tag_rank, cursor.getLong(mIndexRank)); + view.setTag(R.id.tag_certifierId, cursor.getLong(mIndexSignerKeyId)); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.view_key_certs_item, parent, false); + } + + /** + * Creates a new header view and binds the section headers to it. It uses the ViewHolder + * pattern. Most functionality is similar to getView() from Android's CursorAdapter. + *

+ * NOTE: The variables mDataValid and mCursor are available due to the super class + * CursorAdapter. + */ + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + HeaderViewHolder holder; + if (convertView == null) { + holder = new HeaderViewHolder(); + convertView = mInflater.inflate(R.layout.view_key_certs_header, parent, false); + holder.text = (TextView) convertView.findViewById(R.id.stickylist_header_text); + holder.count = (TextView) convertView.findViewById(R.id.certs_num); + convertView.setTag(holder); + } else { + holder = (HeaderViewHolder) convertView.getTag(); + } + + if (!mDataValid) { + // no data available at this point + Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); + return convertView; + } + + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + // set header text as first char in user id + String userId = mCursor.getString(mIndexUserId); + holder.text.setText(userId); + holder.count.setVisibility(View.GONE); + return convertView; + } + + /** + * Header IDs should be static, position=1 should always return the same Id that is. + */ + @Override + public long getHeaderId(int position) { + if (!mDataValid) { + // no data available at this point + Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); + return -1; + } + + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + // otherwise, return the first character of the name as ID + return mCursor.getInt(mIndexRank); + + // sort by the first four characters (should be enough I guess?) + // return ByteBuffer.wrap(userId.getBytes()).asLongBuffer().get(0); + } + + class HeaderViewHolder { + TextView text; + TextView count; + } + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java new file mode 100644 index 000000000..6e96a338a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; + +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +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.ProviderHelper; +import org.sufficientlysecure.keychain.ui.adapter.ViewKeyKeysAdapter; +import org.sufficientlysecure.keychain.ui.adapter.ViewKeyUserIdsAdapter; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.Date; + + +public class ViewKeyMainFragment extends Fragment implements + LoaderManager.LoaderCallbacks { + + public static final String ARG_DATA_URI = "uri"; + + private LinearLayout mContainer; + private TextView mName; + private TextView mEmail; + private TextView mComment; + private TextView mAlgorithm; + private TextView mKeyId; + private TextView mExpiry; + private TextView mCreation; + private TextView mFingerprint; + private TextView mSecretKey; + private BootstrapButton mActionEdit; + private BootstrapButton mActionEncrypt; + private BootstrapButton mActionCertify; + + private ListView mUserIds; + private ListView mKeys; + + private static final int LOADER_ID_UNIFIED = 0; + private static final int LOADER_ID_USER_IDS = 1; + private static final int LOADER_ID_KEYS = 2; + + private ViewKeyUserIdsAdapter mUserIdsAdapter; + private ViewKeyKeysAdapter mKeysAdapter; + + private Uri mDataUri; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.view_key_main_fragment, container, false); + + mContainer = (LinearLayout) view.findViewById(R.id.container); + mName = (TextView) view.findViewById(R.id.name); + mEmail = (TextView) view.findViewById(R.id.email); + mComment = (TextView) view.findViewById(R.id.comment); + mKeyId = (TextView) view.findViewById(R.id.key_id); + mAlgorithm = (TextView) view.findViewById(R.id.algorithm); + mCreation = (TextView) view.findViewById(R.id.creation); + mExpiry = (TextView) view.findViewById(R.id.expiry); + mFingerprint = (TextView) view.findViewById(R.id.fingerprint); + mSecretKey = (TextView) view.findViewById(R.id.secret_key); + mUserIds = (ListView) view.findViewById(R.id.user_ids); + mKeys = (ListView) view.findViewById(R.id.keys); + mActionEdit = (BootstrapButton) view.findViewById(R.id.action_edit); + mActionEncrypt = (BootstrapButton) view.findViewById(R.id.action_encrypt); + mActionCertify = (BootstrapButton) view.findViewById(R.id.action_certify); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); + if (dataUri == null) { + Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); + getActivity().finish(); + return; + } + + loadData(dataUri); + } + + private void loadData(Uri dataUri) { + if (dataUri.equals(mDataUri)) { + Log.d(Constants.TAG, "Same URI, no need to load the data again!"); + return; + } + + getActivity().setProgressBarIndeterminateVisibility(Boolean.TRUE); + mContainer.setVisibility(View.GONE); + + mDataUri = dataUri; + + Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); + + mActionEncrypt.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + encryptToContact(mDataUri); + } + }); + mActionCertify.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + certifyKey(mDataUri); + } + }); + + mUserIdsAdapter = new ViewKeyUserIdsAdapter(getActivity(), null, 0); + mUserIds.setAdapter(mUserIdsAdapter); + + mKeysAdapter = new ViewKeyKeysAdapter(getActivity(), null, 0); + mKeys.setAdapter(mKeysAdapter); + + // Prepare the loaders. Either re-connect with an existing ones, + // or start new ones. + getActivity().getSupportLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); + getActivity().getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); + getActivity().getSupportLoaderManager().initLoader(LOADER_ID_KEYS, null, this); + } + + static final String[] UNIFIED_PROJECTION = new String[] { + KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.HAS_SECRET, + KeyRings.USER_ID, KeyRings.FINGERPRINT, + KeyRings.ALGORITHM, KeyRings.KEY_SIZE, KeyRings.CREATION, KeyRings.EXPIRY, + + }; + static final int INDEX_UNIFIED_MKI = 1; + static final int INDEX_UNIFIED_HAS_SECRET = 2; + static final int INDEX_UNIFIED_UID = 3; + static final int INDEX_UNIFIED_FINGERPRINT = 4; + static final int INDEX_UNIFIED_ALGORITHM = 5; + static final int INDEX_UNIFIED_KEY_SIZE = 6; + static final int INDEX_UNIFIED_CREATION = 7; + static final int INDEX_UNIFIED_EXPIRY = 8; + + static final String[] KEYS_PROJECTION = new String[] { + Keys._ID, + Keys.KEY_ID, Keys.RANK, Keys.ALGORITHM, Keys.KEY_SIZE, + Keys.CAN_CERTIFY, Keys.CAN_ENCRYPT, Keys.CAN_SIGN, Keys.IS_REVOKED, + Keys.CREATION, Keys.EXPIRY, Keys.FINGERPRINT + }; + static final int KEYS_INDEX_CAN_ENCRYPT = 6; + + public Loader onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_ID_UNIFIED: { + Uri baseUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); + } + case LOADER_ID_USER_IDS: { + Uri baseUri = UserIds.buildUserIdsUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, ViewKeyUserIdsAdapter.USER_IDS_PROJECTION, null, null, null); + } + case LOADER_ID_KEYS: { + Uri baseUri = Keys.buildKeysUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, KEYS_PROJECTION, null, null, null); + } + + default: + return null; + } + } + + public void onLoadFinished(Loader loader, Cursor data) { + /* TODO better error handling? May cause problems when a key is deleted, + * because the notification triggers faster than the activity closes. + */ + // Avoid NullPointerExceptions... + if(data.getCount() == 0) + return; + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + switch (loader.getId()) { + case LOADER_ID_UNIFIED: { + if (data.moveToFirst()) { + // get name, email, and comment from USER_ID + String[] mainUserId = PgpKeyHelper.splitUserId(data.getString(INDEX_UNIFIED_UID)); + if (mainUserId[0] != null) { + getActivity().setTitle(mainUserId[0]); + mName.setText(mainUserId[0]); + } else { + getActivity().setTitle(R.string.user_id_no_name); + mName.setText(R.string.user_id_no_name); + } + mEmail.setText(mainUserId[1]); + mComment.setText(mainUserId[2]); + + if (data.getInt(INDEX_UNIFIED_HAS_SECRET) != 0) { + mSecretKey.setTextColor(getResources().getColor(R.color.emphasis)); + mSecretKey.setText(R.string.secret_key_yes); + + // edit button + mActionEdit.setVisibility(View.VISIBLE); + mActionEdit.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + Intent editIntent = new Intent(getActivity(), EditKeyActivity.class); + editIntent.setData(mDataUri); + editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY); + startActivityForResult(editIntent, 0); + } + }); + } else { + mSecretKey.setTextColor(Color.BLACK); + mSecretKey.setText(getResources().getString(R.string.secret_key_no)); + + // certify button + mActionCertify.setVisibility(View.VISIBLE); + // edit button + mActionEdit.setVisibility(View.GONE); + } + + // get key id from MASTER_KEY_ID + long masterKeyId = data.getLong(INDEX_UNIFIED_MKI); + String keyIdStr = PgpKeyHelper.convertKeyIdToHex(masterKeyId); + mKeyId.setText(keyIdStr); + + // get creation date from CREATION + if (data.isNull(INDEX_UNIFIED_CREATION)) { + mCreation.setText(R.string.none); + } else { + Date creationDate = new Date(data.getLong(INDEX_UNIFIED_CREATION) * 1000); + + mCreation.setText( + DateFormat.getDateFormat(getActivity().getApplicationContext()).format( + creationDate)); + } + + // get expiry date from EXPIRY + if (data.isNull(INDEX_UNIFIED_EXPIRY)) { + mExpiry.setText(R.string.none); + } else { + Date expiryDate = new Date(data.getLong(INDEX_UNIFIED_EXPIRY) * 1000); + + mExpiry.setText( + DateFormat.getDateFormat(getActivity().getApplicationContext()).format( + expiryDate)); + } + + String algorithmStr = PgpKeyHelper.getAlgorithmInfo( + data.getInt(INDEX_UNIFIED_ALGORITHM), data.getInt(INDEX_UNIFIED_KEY_SIZE)); + mAlgorithm.setText(algorithmStr); + + byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); + String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob); + mFingerprint.setText(PgpKeyHelper.colorizeFingerprint(fingerprint)); + + break; + } + } + + case LOADER_ID_USER_IDS: + mUserIdsAdapter.swapCursor(data); + break; + + case LOADER_ID_KEYS: + // hide encrypt button if no encryption key is available + boolean canEncrypt = false; + data.moveToFirst(); + do { + if (data.getInt(KEYS_INDEX_CAN_ENCRYPT) == 1) { + canEncrypt = true; + break; + } + } while (data.moveToNext()); + if (!canEncrypt) { + mActionEncrypt.setVisibility(View.GONE); + } + + mKeysAdapter.swapCursor(data); + break; + } + getActivity().setProgressBarIndeterminateVisibility(Boolean.FALSE); + mContainer.setVisibility(View.VISIBLE); + } + + /** + * 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. + */ + public void onLoaderReset(Loader loader) { + switch (loader.getId()) { + case LOADER_ID_USER_IDS: + mUserIdsAdapter.swapCursor(null); + break; + case LOADER_ID_KEYS: + mKeysAdapter.swapCursor(null); + break; + } + } + + private void encryptToContact(Uri dataUri) { + // TODO preselect from uri? should be feasible without trivial query + long keyId = ProviderHelper.getMasterKeyId(getActivity(), dataUri); + + long[] encryptionKeyIds = new long[]{ keyId }; + Intent intent = new Intent(getActivity(), EncryptActivity.class); + intent.setAction(EncryptActivity.ACTION_ENCRYPT); + intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); + // used instead of startActivity set actionbar based on callingPackage + startActivityForResult(intent, 0); + } + + private void certifyKey(Uri dataUri) { + Intent signIntent = new Intent(getActivity(), CertifyKeyActivity.class); + signIntent.setData(dataUri); + startActivity(signIntent); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java new file mode 100644 index 000000000..5f2aec4fe --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/AsyncTaskResultWrapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +/** + * The AsyncTaskResultWrapper is used to wrap a result from a AsyncTask (for example: Loader). + * You can pass the result and an exception in it if an error occurred. + * Concept found at: + * https://stackoverflow.com/questions/19593577/how-to-handle-errors-in-custom-asynctaskloader + * + * @param - Typ of the result which is wrapped + */ +public class AsyncTaskResultWrapper { + + private final T mResult; + private final Exception mError; + + public AsyncTaskResultWrapper(T result, Exception error) { + this.mResult = result; + this.mError = error; + } + + public T getResult() { + return mResult; + } + + public Exception getError() { + return mError; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/HighlightQueryCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/HighlightQueryCursorAdapter.java new file mode 100644 index 000000000..a3ed08a4c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/HighlightQueryCursorAdapter.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +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 abstract class HighlightQueryCursorAdapter extends CursorAdapter { + + private String mCurQuery; + + public HighlightQueryCursorAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + mCurQuery = null; + } + + public void setSearchQuery(String searchQuery) { + mCurQuery = searchQuery; + } + + public String getSearchQuery() { + return mCurQuery; + } + + protected Spannable highlightSearchQuery(String text) { + Spannable highlight = Spannable.Factory.getInstance().newSpannable(text); + + if (mCurQuery != null) { + Pattern pattern = Pattern.compile("(?i)" + mCurQuery); + Matcher matcher = pattern.matcher(text); + if (matcher.find()) { + highlight.setSpan( + new ForegroundColorSpan(mContext.getResources().getColor(R.color.emphasis)), + matcher.start(), + matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return highlight; + } else { + return highlight; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java new file mode 100644 index 000000000..f322ea980 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; + +import java.util.ArrayList; +import java.util.List; + +public class ImportKeysAdapter extends ArrayAdapter { + protected LayoutInflater mInflater; + protected Activity mActivity; + + protected List mData; + + static class ViewHolder { + public TextView mainUserId; + public TextView mainUserIdRest; + public TextView keyId; + public TextView fingerprint; + public TextView algorithm; + public TextView status; + } + + public ImportKeysAdapter(Activity activity) { + super(activity, -1); + mActivity = activity; + mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @SuppressLint("NewApi") + public void setData(List data) { + clear(); + if (data != null) { + this.mData = data; + + // add data to extended ArrayAdapter + if (Build.VERSION.SDK_INT >= 11) { + addAll(data); + } else { + for (ImportKeysListEntry entry : data) { + add(entry); + } + } + } + } + + public List getData() { + return mData; + } + + public ArrayList getSelectedData() { + ArrayList selectedData = new ArrayList(); + for (ImportKeysListEntry entry : mData) { + if (entry.isSelected()) { + selectedData.add(entry); + } + } + return selectedData; + } + + @Override + public boolean hasStableIds() { + return true; + } + + public View getView(int position, View convertView, ViewGroup parent) { + ImportKeysListEntry entry = mData.get(position); + ViewHolder holder; + if (convertView == null) { + holder = new ViewHolder(); + convertView = mInflater.inflate(R.layout.import_keys_list_entry, null); + holder.mainUserId = (TextView) convertView.findViewById(R.id.mainUserId); + holder.mainUserIdRest = (TextView) convertView.findViewById(R.id.mainUserIdRest); + holder.keyId = (TextView) convertView.findViewById(R.id.keyId); + holder.fingerprint = (TextView) convertView.findViewById(R.id.fingerprint); + holder.algorithm = (TextView) convertView.findViewById(R.id.algorithm); + holder.status = (TextView) convertView.findViewById(R.id.status); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + // main user id + String userId = entry.userIds.get(0); + String[] userIdSplit = PgpKeyHelper.splitUserId(userId); + + // name + if (userIdSplit[0] != null) { + // show red user id if it is a secret key + if (entry.secretKey) { + userIdSplit[0] = mActivity.getString(R.string.secret_key) + " " + userIdSplit[0]; + holder.mainUserId.setTextColor(Color.RED); + } + holder.mainUserId.setText(userIdSplit[0]); + } else { + holder.mainUserId.setText(R.string.user_id_no_name); + } + + // email + if (userIdSplit[1] != null) { + holder.mainUserIdRest.setText(userIdSplit[1]); + holder.mainUserIdRest.setVisibility(View.VISIBLE); + } else { + holder.mainUserIdRest.setVisibility(View.GONE); + } + + holder.keyId.setText(entry.keyIdHex); + + if (entry.fingerPrintHex != null) { + holder.fingerprint.setText(PgpKeyHelper.colorizeFingerprint(entry.fingerPrintHex)); + holder.fingerprint.setVisibility(View.VISIBLE); + } else { + holder.fingerprint.setVisibility(View.GONE); + } + + holder.algorithm.setText("" + entry.bitStrength + "/" + entry.algorithm); + + if (entry.revoked) { + holder.status.setText(R.string.revoked); + } else { + holder.status.setVisibility(View.GONE); + } + + LinearLayout ll = (LinearLayout) convertView.findViewById(R.id.list); + ll.removeAllViews(); + if (entry.userIds.size() == 1) { + ll.setVisibility(View.GONE); + } else { + boolean first = true; + boolean second = true; + for (String uid : entry.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.import_keys_list_entry_user_id, null); + uidView.setText(uid); + ll.addView(uidView); + second = false; + } + } + + CheckBox cBox = (CheckBox) convertView.findViewById(R.id.selected); + cBox.setChecked(entry.isSelected()); + + return convertView; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java new file mode 100644 index 000000000..5631d40ea --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.SparseArray; + +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPSecretKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; + +public class ImportKeysListEntry implements Serializable, Parcelable { + private static final long serialVersionUID = -7797972103284992662L; + + public ArrayList userIds; + public long keyId; + public String keyIdHex; + public boolean revoked; + public Date date; // TODO: not displayed + public String fingerPrintHex; + public int bitStrength; + public String algorithm; + public boolean secretKey; + + private boolean mSelected; + + private byte[] mBytes = new byte[]{}; + + public ImportKeysListEntry(ImportKeysListEntry b) { + this.userIds = b.userIds; + this.keyId = b.keyId; + this.revoked = b.revoked; + this.date = b.date; + this.fingerPrintHex = b.fingerPrintHex; + this.keyIdHex = b.keyIdHex; + this.bitStrength = b.bitStrength; + this.algorithm = b.algorithm; + this.secretKey = b.secretKey; + this.mSelected = b.mSelected; + this.mBytes = b.mBytes; + } + + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringList(userIds); + dest.writeLong(keyId); + dest.writeByte((byte) (revoked ? 1 : 0)); + dest.writeSerializable(date); + dest.writeString(fingerPrintHex); + dest.writeString(keyIdHex); + dest.writeInt(bitStrength); + dest.writeString(algorithm); + dest.writeByte((byte) (secretKey ? 1 : 0)); + dest.writeByte((byte) (mSelected ? 1 : 0)); + dest.writeInt(mBytes.length); + dest.writeByteArray(mBytes); + } + + public static final Creator CREATOR = new Creator() { + public ImportKeysListEntry createFromParcel(final Parcel source) { + ImportKeysListEntry vr = new ImportKeysListEntry(); + vr.userIds = new ArrayList(); + source.readStringList(vr.userIds); + vr.keyId = source.readLong(); + vr.revoked = source.readByte() == 1; + vr.date = (Date) source.readSerializable(); + vr.fingerPrintHex = source.readString(); + vr.keyIdHex = source.readString(); + vr.bitStrength = source.readInt(); + vr.algorithm = source.readString(); + vr.secretKey = source.readByte() == 1; + vr.mSelected = source.readByte() == 1; + vr.mBytes = new byte[source.readInt()]; + source.readByteArray(vr.mBytes); + + return vr; + } + + public ImportKeysListEntry[] newArray(final int size) { + return new ImportKeysListEntry[size]; + } + }; + + public String getKeyIdHex() { + return keyIdHex; + } + + public byte[] getBytes() { + return mBytes; + } + + public void setBytes(byte[] bytes) { + this.mBytes = bytes; + } + + public boolean isSelected() { + return mSelected; + } + + public void setSelected(boolean selected) { + this.mSelected = selected; + } + + public long getKeyId() { + return keyId; + } + + public void setKeyId(long keyId) { + this.keyId = keyId; + } + + public void setKeyIdHex(String keyIdHex) { + this.keyIdHex = keyIdHex; + } + + public boolean isRevoked() { + return revoked; + } + + public void setRevoked(boolean revoked) { + this.revoked = revoked; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public String getFingerPrintHex() { + return fingerPrintHex; + } + + public void setFingerPrintHex(String fingerPrintHex) { + this.fingerPrintHex = fingerPrintHex; + } + + public int getBitStrength() { + return bitStrength; + } + + public void setBitStrength(int bitStrength) { + this.bitStrength = bitStrength; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public boolean isSecretKey() { + return secretKey; + } + + public void setSecretKey(boolean secretKey) { + this.secretKey = secretKey; + } + + public ArrayList getUserIds() { + return userIds; + } + + public void setUserIds(ArrayList userIds) { + this.userIds = userIds; + } + + /** + * Constructor for later querying from keyserver + */ + public ImportKeysListEntry() { + // keys from keyserver are always public keys + secretKey = false; + // do not select by default + mSelected = false; + userIds = new ArrayList(); + } + + /** + * Constructor based on key object, used for import from NFC, QR Codes, files + */ + @SuppressWarnings("unchecked") + public ImportKeysListEntry(PGPKeyRing pgpKeyRing) { + // save actual key object into entry, used to import it later + try { + this.mBytes = pgpKeyRing.getEncoded(); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException on pgpKeyRing.getEncoded()", e); + } + + // selected is default + this.mSelected = true; + + if (pgpKeyRing instanceof PGPSecretKeyRing) { + secretKey = true; + } else { + secretKey = false; + } + + userIds = new ArrayList(); + for (String userId : new IterableIterator(pgpKeyRing.getPublicKey().getUserIDs())) { + userIds.add(userId); + } + + this.keyId = pgpKeyRing.getPublicKey().getKeyID(); + this.keyIdHex = PgpKeyHelper.convertKeyIdToHex(keyId); + + this.revoked = pgpKeyRing.getPublicKey().isRevoked(); + this.fingerPrintHex = PgpKeyHelper.convertFingerprintToHex(pgpKeyRing.getPublicKey() + .getFingerprint()); + this.bitStrength = pgpKeyRing.getPublicKey().getBitStrength(); + final int algorithm = pgpKeyRing.getPublicKey().getAlgorithm(); + this.algorithm = getAlgorithmFromId(algorithm); + } + + /** + * Based on OpenPGP Message Format + */ + private static final SparseArray ALGORITHM_IDS = new SparseArray() {{ + put(-1, "unknown"); // TODO: with resources + put(0, "unencrypted"); + put(PGPPublicKey.RSA_GENERAL, "RSA"); + put(PGPPublicKey.RSA_ENCRYPT, "RSA"); + put(PGPPublicKey.RSA_SIGN, "RSA"); + put(PGPPublicKey.ELGAMAL_ENCRYPT, "ElGamal"); + put(PGPPublicKey.ELGAMAL_GENERAL, "ElGamal"); + put(PGPPublicKey.DSA, "DSA"); + put(PGPPublicKey.EC, "ECC"); + put(PGPPublicKey.ECDSA, "ECC"); + put(PGPPublicKey.ECDH, "ECC"); + }}; + + /** + * Based on OpenPGP Message Format + */ + public static String getAlgorithmFromId(int algorithmId) { + return (ALGORITHM_IDS.get(algorithmId) != null ? + ALGORITHM_IDS.get(algorithmId) : + ALGORITHM_IDS.get(-1)); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java new file mode 100644 index 000000000..c9983213c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.support.v4.content.AsyncTaskLoader; +import org.spongycastle.openpgp.PGPKeyRing; +import org.spongycastle.openpgp.PGPObjectFactory; +import org.spongycastle.openpgp.PGPUtil; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.PositionAwareInputStream; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.util.ArrayList; + +public class ImportKeysListLoader + extends AsyncTaskLoader>> { + + public static class FileHasNoContent extends Exception { + + } + + public static class NonPgpPart extends Exception { + private int mCount; + + public NonPgpPart(int count) { + this.mCount = count; + } + + public int getCount() { + return mCount; + } + } + + Context mContext; + + InputData mInputData; + + ArrayList mData = new ArrayList(); + AsyncTaskResultWrapper> mEntryListWrapper; + + public ImportKeysListLoader(Context context, InputData inputData) { + super(context); + this.mContext = context; + this.mInputData = inputData; + } + + @Override + public AsyncTaskResultWrapper> loadInBackground() { + + mEntryListWrapper = new AsyncTaskResultWrapper>(mData, null); + + if (mInputData == null) { + Log.e(Constants.TAG, "Input data is null!"); + return mEntryListWrapper; + } + + generateListOfKeyrings(mInputData); + + return mEntryListWrapper; + } + + @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(AsyncTaskResultWrapper> data) { + super.deliverResult(data); + } + + /** + * Reads all PGPKeyRing objects from input + * + * @param inputData + * @return + */ + private void generateListOfKeyrings(InputData inputData) { + + boolean isEmpty = true; + int nonPgpCounter = 0; + + 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 + // armor blocks + BufferedInputStream bufferedInput = new BufferedInputStream(progressIn); + try { + + // read all available blocks... (asc files can contain many blocks with BEGIN END) + while (bufferedInput.available() > 0) { + isEmpty = false; + 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!"); + nonPgpCounter++; + } + } + } + } catch (Exception e) { + Log.e(Constants.TAG, "Exception on parsing key file!", e); + mEntryListWrapper = new AsyncTaskResultWrapper>(mData, e); + nonPgpCounter = 0; + } + + if (isEmpty) { + Log.e(Constants.TAG, "File has no content!", new FileHasNoContent()); + mEntryListWrapper = new AsyncTaskResultWrapper> + (mData, new FileHasNoContent()); + } + + if (nonPgpCounter > 0) { + mEntryListWrapper = new AsyncTaskResultWrapper> + (mData, new NonPgpPart(nonPgpCounter)); + } + } + + private void addToData(PGPKeyRing keyring) { + ImportKeysListEntry item = new ImportKeysListEntry(keyring); + mData.add(item); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListServerLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListServerLoader.java new file mode 100644 index 000000000..259e14319 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListServerLoader.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.support.v4.content.AsyncTaskLoader; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.HkpKeyServer; +import org.sufficientlysecure.keychain.util.KeyServer; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; + +public class ImportKeysListServerLoader + extends AsyncTaskLoader>> { + Context mContext; + + String mServerQuery; + String mKeyServer; + + private ArrayList mEntryList = new ArrayList(); + private AsyncTaskResultWrapper> mEntryListWrapper; + + public ImportKeysListServerLoader(Context context, String serverQuery, String keyServer) { + super(context); + mContext = context; + mServerQuery = serverQuery; + mKeyServer = keyServer; + } + + @Override + public AsyncTaskResultWrapper> loadInBackground() { + + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, null); + + if (mServerQuery == null) { + Log.e(Constants.TAG, "mServerQuery is null!"); + return mEntryListWrapper; + } + + if (mServerQuery.startsWith("0x") && mServerQuery.length() == 42) { + Log.d(Constants.TAG, "This search is based on a unique fingerprint. Enforce a fingerprint check!"); + queryServer(mServerQuery, mKeyServer, true); + } else { + queryServer(mServerQuery, mKeyServer, false); + } + + return mEntryListWrapper; + } + + @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(AsyncTaskResultWrapper> data) { + super.deliverResult(data); + } + + /** + * Query keyserver + */ + private void queryServer(String query, String keyServer, boolean enforceFingerprint) { + HkpKeyServer server = new HkpKeyServer(keyServer); + try { + ArrayList searchResult = server.search(query); + + mEntryList.clear(); + // add result to data + if (enforceFingerprint) { + String fingerprint = query.substring(2); + Log.d(Constants.TAG, "fingerprint: " + fingerprint); + // query must return only one result! + if (searchResult.size() > 0) { + ImportKeysListEntry uniqueEntry = searchResult.get(0); + /* + * set fingerprint explicitly after query + * to enforce a check when the key is imported by KeychainIntentService + */ + uniqueEntry.setFingerPrintHex(fingerprint); + uniqueEntry.setSelected(true); + mEntryList.add(uniqueEntry); + } + } else { + mEntryList.addAll(searchResult); + } + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, null); + } catch (KeyServer.InsufficientQuery e) { + Log.e(Constants.TAG, "InsufficientQuery", e); + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, e); + } catch (KeyServer.QueryException e) { + Log.e(Constants.TAG, "QueryException", e); + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, e); + } catch (KeyServer.TooManyResponses e) { + Log.e(Constants.TAG, "TooManyResponses", e); + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, e); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyValueSpinnerAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyValueSpinnerAdapter.java new file mode 100644 index 000000000..5b5d316b6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyValueSpinnerAdapter.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.widget.ArrayAdapter; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +public class KeyValueSpinnerAdapter extends ArrayAdapter { + private final HashMap mData; + private final int[] mKeys; + private final String[] mValues; + + static > SortedSet> entriesSortedByValues( + Map map) { + SortedSet> sortedEntries = new TreeSet>( + new Comparator>() { + @Override + public int compare(Map.Entry e1, Map.Entry e2) { + return e1.getValue().compareTo(e2.getValue()); + } + }); + sortedEntries.addAll(map.entrySet()); + return sortedEntries; + } + + public KeyValueSpinnerAdapter(Context context, HashMap objects) { + // To make the drop down a simple text box + super(context, android.R.layout.simple_spinner_item); + mData = objects; + + // To make the drop down view a radio button list + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + SortedSet> sorted = entriesSortedByValues(objects); + + // Assign hash keys with a position so that we can present and retrieve them + int i = 0; + mKeys = new int[mData.size()]; + mValues = new String[mData.size()]; + for (Map.Entry entry : sorted) { + mKeys[i] = entry.getKey(); + mValues[i] = entry.getValue(); + i++; + } + } + + public int getCount() { + return mData.size(); + } + + /** + * Returns the value + */ + @Override + public String getItem(int position) { + // return the value based on the position. This is displayed in the list. + return mValues[position]; + } + + /** + * Returns item key + */ + public long getItemId(int position) { + // Return an id to represent the item. + + return mKeys[position]; + } + + /** + * Find position from key + */ + public int getPosition(long itemId) { + for (int i = 0; i < mKeys.length; i++) { + if ((int) itemId == mKeys[i]) { + return i; + } + } + return -1; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java new file mode 100644 index 000000000..fd864eb09 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v7.app.ActionBarActivity; + +import java.util.ArrayList; + +public class PagerTabStripAdapter extends FragmentPagerAdapter { + private final Context mContext; + private final ArrayList mTabs = new ArrayList(); + + static final class TabInfo { + public final Class clss; + public final Bundle args; + public final String title; + + TabInfo(Class clss, Bundle args, String title) { + this.clss = clss; + this.args = args; + this.title = title; + } + } + + public PagerTabStripAdapter(ActionBarActivity activity) { + super(activity.getSupportFragmentManager()); + mContext = activity; + } + + public void addTab(Class clss, Bundle args, String title) { + TabInfo info = new TabInfo(clss, args, title); + mTabs.add(info); + 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); + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabs.get(position).title; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java new file mode 100644 index 000000000..fbbb9caa4 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.database.Cursor; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ListView; +import android.widget.TextView; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; + + +public class SelectKeyCursorAdapter extends HighlightQueryCursorAdapter { + + protected int mKeyType; + + private LayoutInflater mInflater; + private ListView mListView; + + private int mIndexUserId; + private int mIndexMasterKeyId; + private int mIndexProjectionValid; + private int mIndexProjectionAvailable; + + public static final String PROJECTION_ROW_AVAILABLE = "available"; + public static final String PROJECTION_ROW_VALID = "valid"; + + public SelectKeyCursorAdapter(Context context, Cursor c, int flags, ListView listView, + int keyType) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + mListView = listView; + mKeyType = keyType; + initIndex(c); + } + + @Override + public Cursor swapCursor(Cursor newCursor) { + initIndex(newCursor); + + return super.swapCursor(newCursor); + } + + /** + * Get column indexes for performance reasons just once in constructor and swapCursor. For a + * performance comparison see http://stackoverflow.com/a/17999582 + * + * @param cursor + */ + private void initIndex(Cursor cursor) { + if (cursor != null) { + mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID); + mIndexMasterKeyId = cursor.getColumnIndexOrThrow(KeyRings.MASTER_KEY_ID); + mIndexProjectionValid = cursor.getColumnIndexOrThrow(PROJECTION_ROW_VALID); + mIndexProjectionAvailable = cursor.getColumnIndexOrThrow(PROJECTION_ROW_AVAILABLE); + } + } + + public String getUserId(int position) { + mCursor.moveToPosition(position); + return mCursor.getString(mIndexUserId); + } + + public long getMasterKeyId(int position) { + mCursor.moveToPosition(position); + return mCursor.getLong(mIndexMasterKeyId); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + boolean valid = cursor.getInt(mIndexProjectionValid) > 0; + + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + TextView keyId = (TextView) view.findViewById(R.id.keyId); + TextView status = (TextView) view.findViewById(R.id.status); + + String userId = cursor.getString(mIndexUserId); + String[] userIdSplit = PgpKeyHelper.splitUserId(userId); + + if (userIdSplit[0] != null) { + mainUserId.setText(highlightSearchQuery(userIdSplit[0])); + } else { + mainUserId.setText(R.string.user_id_no_name); + } + if (userIdSplit[1] != null) { + mainUserIdRest.setText(highlightSearchQuery(userIdSplit[1])); + } else { + mainUserIdRest.setText(""); + } + + // TODO: needed to key id to no? + keyId.setText(R.string.no_key); + long masterKeyId = cursor.getLong(mIndexMasterKeyId); + keyId.setText(PgpKeyHelper.convertKeyIdToHexShort(masterKeyId)); + + // TODO: needed to set unknown_status? + status.setText(R.string.unknown_status); + if (valid) { + if (mKeyType == Id.type.public_key) { + status.setText(R.string.can_encrypt); + } else { + status.setText(R.string.can_sign); + } + } else { + if (cursor.getInt(mIndexProjectionAvailable) > 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.no_key); + } + } + + 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); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/TabsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/TabsAdapter.java new file mode 100644 index 000000000..9ddfa90be --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/TabsAdapter.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; + +import java.util.ArrayList; + +public class TabsAdapter extends FragmentStatePagerAdapter implements ActionBar.TabListener, + ViewPager.OnPageChangeListener { + private final Context mContext; + private final ActionBar mActionBar; + private final ViewPager mViewPager; + private final ArrayList mTabs = new ArrayList(); + + static final class TabInfo { + public final Class clss; + public final Bundle args; + + TabInfo(Class clss, Bundle args) { + this.clss = clss; + this.args = args; + } + } + + public TabsAdapter(ActionBarActivity 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(ActionBar.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(ActionBar.Tab tab, FragmentTransaction ft) { + } + + public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) { + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java new file mode 100644 index 000000000..64b735bfa --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; + +import java.util.Date; + +public class ViewKeyKeysAdapter extends CursorAdapter { + private LayoutInflater mInflater; + + private int mIndexKeyId; + private int mIndexAlgorithm; + private int mIndexKeySize; + private int mIndexRank; + private int mIndexCanCertify; + private int mIndexCanEncrypt; + private int mIndexCanSign; + private int mIndexRevokedKey; + private int mIndexExpiry; + + private ColorStateList mDefaultTextColor; + + public ViewKeyKeysAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + + initIndex(c); + } + + @Override + public Cursor swapCursor(Cursor newCursor) { + initIndex(newCursor); + + return super.swapCursor(newCursor); + } + + /** + * Get column indexes for performance reasons just once in constructor and swapCursor. For a + * performance comparison see http://stackoverflow.com/a/17999582 + * + * @param cursor + */ + private void initIndex(Cursor cursor) { + if (cursor != null) { + mIndexKeyId = cursor.getColumnIndexOrThrow(Keys.KEY_ID); + mIndexAlgorithm = cursor.getColumnIndexOrThrow(Keys.ALGORITHM); + mIndexKeySize = cursor.getColumnIndexOrThrow(Keys.KEY_SIZE); + mIndexRank = cursor.getColumnIndexOrThrow(Keys.RANK); + mIndexCanCertify = cursor.getColumnIndexOrThrow(Keys.CAN_CERTIFY); + mIndexCanEncrypt = cursor.getColumnIndexOrThrow(Keys.CAN_ENCRYPT); + mIndexCanSign = cursor.getColumnIndexOrThrow(Keys.CAN_SIGN); + mIndexRevokedKey = cursor.getColumnIndexOrThrow(Keys.IS_REVOKED); + mIndexExpiry = cursor.getColumnIndexOrThrow(Keys.EXPIRY); + } + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView keyId = (TextView) view.findViewById(R.id.keyId); + TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails); + TextView keyExpiry = (TextView) view.findViewById(R.id.keyExpiry); + ImageView masterKeyIcon = (ImageView) view.findViewById(R.id.ic_masterKey); + ImageView certifyIcon = (ImageView) view.findViewById(R.id.ic_certifyKey); + ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey); + ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey); + ImageView revokedKeyIcon = (ImageView) view.findViewById(R.id.ic_revokedKey); + + String keyIdStr = PgpKeyHelper.convertKeyIdToHexShort(cursor.getLong(mIndexKeyId)); + String algorithmStr = PgpKeyHelper.getAlgorithmInfo(cursor.getInt(mIndexAlgorithm), + cursor.getInt(mIndexKeySize)); + + keyId.setText(keyIdStr); + keyDetails.setText("(" + algorithmStr + ")"); + + if (cursor.getInt(mIndexRank) == 0) { + masterKeyIcon.setVisibility(View.INVISIBLE); + } else { + masterKeyIcon.setVisibility(View.VISIBLE); + } + + if (cursor.getInt(mIndexCanCertify) != 1) { + certifyIcon.setVisibility(View.GONE); + } else { + certifyIcon.setVisibility(View.VISIBLE); + } + + if (cursor.getInt(mIndexCanEncrypt) != 1) { + encryptIcon.setVisibility(View.GONE); + } else { + encryptIcon.setVisibility(View.VISIBLE); + } + + if (cursor.getInt(mIndexCanSign) != 1) { + signIcon.setVisibility(View.GONE); + } else { + signIcon.setVisibility(View.VISIBLE); + } + + boolean valid = true; + if (cursor.getInt(mIndexRevokedKey) > 0) { + revokedKeyIcon.setVisibility(View.VISIBLE); + + valid = false; + } else { + keyId.setTextColor(mDefaultTextColor); + keyDetails.setTextColor(mDefaultTextColor); + keyExpiry.setTextColor(mDefaultTextColor); + + revokedKeyIcon.setVisibility(View.GONE); + } + + if (!cursor.isNull(mIndexExpiry)) { + Date expiryDate = new Date(cursor.getLong(mIndexExpiry) * 1000); + + valid = valid && expiryDate.after(new Date()); + keyExpiry.setText("(" + + context.getString(R.string.label_expiry) + ": " + + DateFormat.getDateFormat(context).format(expiryDate) + ")"); + + keyExpiry.setVisibility(View.VISIBLE); + } else { + keyExpiry.setVisibility(View.GONE); + } + // if key is expired or revoked, strike through text + if (!valid) { + keyId.setText(OtherHelper.strikeOutText(keyId.getText())); + keyDetails.setText(OtherHelper.strikeOutText(keyDetails.getText())); + keyExpiry.setText(OtherHelper.strikeOutText(keyExpiry.getText())); + } + keyId.setEnabled(valid); + keyDetails.setEnabled(valid); + keyExpiry.setEnabled(valid); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = mInflater.inflate(R.layout.view_key_keys_item, null); + if (mDefaultTextColor == null) { + TextView keyId = (TextView) view.findViewById(R.id.keyId); + mDefaultTextColor = keyId.getTextColors(); + } + return view; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java new file mode 100644 index 000000000..09137f745 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +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.AdapterView; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.TextView; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; + +import java.util.ArrayList; + +public class ViewKeyUserIdsAdapter extends CursorAdapter implements AdapterView.OnItemClickListener { + private LayoutInflater mInflater; + + private int mIndexUserId, mIndexRank; + private int mVerifiedId, mIsRevoked, mIsPrimary; + + private final ArrayList mCheckStates; + + public static final String[] USER_IDS_PROJECTION = new String[] { + UserIds._ID, UserIds.USER_ID, UserIds.RANK, + UserIds.VERIFIED, UserIds.IS_PRIMARY, UserIds.IS_REVOKED + }; + + public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags, boolean showCheckBoxes) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + + mCheckStates = showCheckBoxes ? new ArrayList() : null; + + initIndex(c); + } + public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags) { + this(context, c, flags, false); + } + + @Override + public Cursor swapCursor(Cursor newCursor) { + initIndex(newCursor); + if (mCheckStates != null) { + mCheckStates.clear(); + if (newCursor != null) { + int count = newCursor.getCount(); + mCheckStates.ensureCapacity(count); + // initialize to true (use case knowledge: we usually want to sign all uids) + for(int i = 0; i < count; i++) { + newCursor.moveToPosition(i); + int verified = newCursor.getInt(mVerifiedId); + mCheckStates.add(verified != Certs.VERIFIED_SECRET); + } + } + } + + return super.swapCursor(newCursor); + } + + /** + * Get column indexes for performance reasons just once in constructor and swapCursor. For a + * performance comparison see http://stackoverflow.com/a/17999582 + * + * @param cursor + */ + private void initIndex(Cursor cursor) { + if (cursor != null) { + mIndexUserId = cursor.getColumnIndexOrThrow(UserIds.USER_ID); + mIndexRank = cursor.getColumnIndexOrThrow(UserIds.RANK); + mVerifiedId = cursor.getColumnIndexOrThrow(UserIds.VERIFIED); + mIsRevoked = cursor.getColumnIndexOrThrow(UserIds.IS_REVOKED); + mIsPrimary = cursor.getColumnIndexOrThrow(UserIds.IS_PRIMARY); + } + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + + TextView vRank = (TextView) view.findViewById(R.id.rank); + TextView vUserId = (TextView) view.findViewById(R.id.userId); + TextView vAddress = (TextView) view.findViewById(R.id.address); + ImageView vVerified = (ImageView) view.findViewById(R.id.certified); + + if(cursor.getInt(mIsPrimary) > 0) { + vRank.setText("+"); + } else { + vRank.setText(Integer.toString(cursor.getInt(mIndexRank))); + } + + String[] userId = PgpKeyHelper.splitUserId(cursor.getString(mIndexUserId)); + if (userId[0] != null) { + vUserId.setText(userId[0]); + } else { + vUserId.setText(R.string.user_id_no_name); + } + vAddress.setText(userId[1]); + + if(cursor.getInt(mIsRevoked) > 0) { + vRank.setText(" "); + vVerified.setImageResource(android.R.drawable.presence_away); + } else { + int verified = cursor.getInt(mVerifiedId); + // TODO introduce own resources for this :) + if(verified == Certs.VERIFIED_SECRET) + vVerified.setImageResource(android.R.drawable.presence_online); + else if(verified == Certs.VERIFIED_SELF) + vVerified.setImageResource(android.R.drawable.presence_invisible); + else + vVerified.setImageResource(android.R.drawable.presence_busy); + } + + // don't care further if checkboxes aren't shown + if (mCheckStates == null) { + return; + } + + final CheckBox vCheckBox = (CheckBox) view.findViewById(R.id.checkBox); + final int position = cursor.getPosition(); + vCheckBox.setOnCheckedChangeListener(null); + vCheckBox.setChecked(mCheckStates.get(position)); + vCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + mCheckStates.set(position, b); + } + }); + vCheckBox.setClickable(false); + + } + + public void onItemClick(AdapterView adapter, View view, int position, long id) { + CheckBox box = ((CheckBox) view.findViewById(R.id.checkBox)); + if(box != null) { + box.toggle(); + } + } + + public ArrayList getSelectedUserIds() { + ArrayList result = new ArrayList(); + for (int i = 0; i < mCheckStates.size(); i++) { + if (mCheckStates.get(i)) { + mCursor.moveToPosition(i); + result.add(mCursor.getString(mIndexUserId)); + } + } + return result; + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = mInflater.inflate(R.layout.view_key_userids_item, null); + // only need to do this once ever, since mShowCheckBoxes is final + view.findViewById(R.id.checkBox).setVisibility(mCheckStates != null ? View.VISIBLE : View.GONE); + return view; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java new file mode 100644 index 000000000..20b70658c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/BadImportKeyDialogFragment.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import org.sufficientlysecure.keychain.R; + +public class BadImportKeyDialogFragment extends DialogFragment { + private static final String ARG_BAD_IMPORT = "bad_import"; + + /** + * Creates a new instance of this Bad Import Key DialogFragment + * + * @param bad + * @return + */ + public static BadImportKeyDialogFragment newInstance(int bad) { + BadImportKeyDialogFragment frag = new BadImportKeyDialogFragment(); + Bundle args = new Bundle(); + + args.putInt(ARG_BAD_IMPORT, bad); + frag.setArguments(args); + + return frag; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final FragmentActivity activity = getActivity(); + final int badImport = getArguments().getInt(ARG_BAD_IMPORT); + + AlertDialog.Builder alert = new AlertDialog.Builder(activity); + alert.setIcon(R.drawable.ic_dialog_alert_holo_light); + alert.setTitle(R.string.warning); + alert.setMessage(activity.getResources() + .getQuantityString(R.plurals.bad_keys_encountered, badImport, badImport)); + alert.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + alert.setCancelable(true); + + return alert.create(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CreateKeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CreateKeyDialogFragment.java new file mode 100644 index 000000000..ad558a81e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/CreateKeyDialogFragment.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Choice; + +import java.util.ArrayList; + +public class CreateKeyDialogFragment extends DialogFragment { + + public interface OnAlgorithmSelectedListener { + public void onAlgorithmSelected(Choice algorithmChoice, int keySize); + } + + private static final String ARG_EDITOR_CHILD_COUNT = "child_count"; + + private int mNewKeySize; + private Choice mNewKeyAlgorithmChoice; + private OnAlgorithmSelectedListener mAlgorithmSelectedListener; + + public void setOnAlgorithmSelectedListener(OnAlgorithmSelectedListener listener) { + mAlgorithmSelectedListener = listener; + } + + public static CreateKeyDialogFragment newInstance(int mEditorChildCount) { + CreateKeyDialogFragment frag = new CreateKeyDialogFragment(); + Bundle args = new Bundle(); + + args.putInt(ARG_EDITOR_CHILD_COUNT, mEditorChildCount); + + frag.setArguments(args); + + return frag; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final FragmentActivity context = getActivity(); + final LayoutInflater mInflater; + + final int childCount = getArguments().getInt(ARG_EDITOR_CHILD_COUNT); + mInflater = context.getLayoutInflater(); + + AlertDialog.Builder dialog = new AlertDialog.Builder(context); + + View view = mInflater.inflate(R.layout.create_key_dialog, null); + dialog.setView(view); + dialog.setTitle(R.string.title_create_key); + + boolean wouldBeMasterKey = (childCount == 0); + + final Spinner algorithm = (Spinner) view.findViewById(R.id.create_key_algorithm); + ArrayList choices = new ArrayList(); + 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 adapter = new ArrayAdapter(context, + 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 Spinner keySize = (Spinner) view.findViewById(R.id.create_key_size); + ArrayAdapter keySizeAdapter = ArrayAdapter.createFromResource( + context, R.array.key_size_spinner_values, + android.R.layout.simple_spinner_item); + keySizeAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + keySize.setAdapter(keySizeAdapter); + keySize.setSelection(3); // Default to 4096 for the key length + dialog.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface di, int id) { + di.dismiss(); + try { + final String selectedItem = (String) keySize.getSelectedItem(); + mNewKeySize = Integer.parseInt(selectedItem); + } catch (NumberFormatException e) { + mNewKeySize = 0; + } + + mNewKeyAlgorithmChoice = (Choice) algorithm.getSelectedItem(); + mAlgorithmSelectedListener.onAlgorithmSelected(mNewKeyAlgorithmChoice, mNewKeySize); + } + }); + + dialog.setCancelable(true); + dialog.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface di, int id) { + di.dismiss(); + } + }); + + final AlertDialog alertDialog = dialog.create(); + + final AdapterView.OnItemSelectedListener weakRsaListener = new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + final Choice selectedAlgorithm = (Choice) algorithm.getSelectedItem(); + final int selectedKeySize = Integer.parseInt((String) keySize.getSelectedItem()); + final boolean isWeakRsa = (selectedAlgorithm.getId() == Id.choice.algorithm.rsa && + selectedKeySize <= 1024); + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!isWeakRsa); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }; + + keySize.setOnItemSelectedListener(weakRsaListener); + algorithm.setOnItemSelectedListener(weakRsaListener); + + return alertDialog; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java new file mode 100644 index 000000000..b4c38184c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +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; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; + +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(R.drawable.ic_dialog_alert_holo_light); + alert.setTitle(R.string.warning); + alert.setMessage(this.getString(R.string.file_delete_confirmation, 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.setAction(KeychainIntentService.ACTION_DELETE_FILE_SECURELY); + data.putString(KeychainIntentService.DELETE_FILE, deleteFile); + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + ProgressDialogFragment deletingDialog = ProgressDialogFragment.newInstance( + getString(R.string.progress_deleting_securely), + ProgressDialog.STYLE_HORIZONTAL, + false, + null); + + // Message is received after deleting is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = + new KeychainIntentServiceHandler(activity, deletingDialog) { + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + Toast.makeText(activity, R.string.file_delete_successful, + 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(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java new file mode 100644 index 000000000..72ea4c013 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +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; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.HashMap; + +public class DeleteKeyDialogFragment extends DialogFragment { + private static final String ARG_MESSENGER = "messenger"; + private static final String ARG_DELETE_MASTER_KEY_IDS = "delete_master_key_ids"; + + public static final int MESSAGE_OKAY = 1; + public static final int MESSAGE_ERROR = 0; + + private boolean mIsSingleSelection = false; + + private TextView mMainMessage; + private CheckBox mCheckDeleteSecret; + private LinearLayout mDeleteSecretKeyView; + private View mInflateView; + + private Messenger mMessenger; + + /** + * Creates new instance of this delete file dialog fragment + */ + public static DeleteKeyDialogFragment newInstance(Messenger messenger, + long[] masterKeyIds) { + DeleteKeyDialogFragment frag = new DeleteKeyDialogFragment(); + Bundle args = new Bundle(); + + args.putParcelable(ARG_MESSENGER, messenger); + args.putLongArray(ARG_DELETE_MASTER_KEY_IDS, masterKeyIds); + //We don't need the key type + + frag.setArguments(args); + + return frag; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + + final FragmentActivity activity = getActivity(); + mMessenger = getArguments().getParcelable(ARG_MESSENGER); + + final long[] masterKeyIds = getArguments().getLongArray(ARG_DELETE_MASTER_KEY_IDS); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + + //Setup custom View to display in AlertDialog + LayoutInflater inflater = activity.getLayoutInflater(); + mInflateView = inflater.inflate(R.layout.view_key_delete_fragment, null); + builder.setView(mInflateView); + + mDeleteSecretKeyView = (LinearLayout) mInflateView.findViewById(R.id.deleteSecretKeyView); + mMainMessage = (TextView) mInflateView.findViewById(R.id.mainMessage); + mCheckDeleteSecret = (CheckBox) mInflateView.findViewById(R.id.checkDeleteSecret); + + builder.setTitle(R.string.warning); + + // If only a single key has been selected + if (masterKeyIds.length == 1) { + mIsSingleSelection = true; + + long masterKeyId = masterKeyIds[0]; + + HashMap data = ProviderHelper.getUnifiedData(activity, masterKeyId, new String[]{ + KeyRings.USER_ID, + KeyRings.HAS_SECRET + }, new int[] { ProviderHelper.FIELD_TYPE_STRING, ProviderHelper.FIELD_TYPE_INTEGER }); + String userId = (String) data.get(KeyRings.USER_ID); + boolean hasSecret = ((Long) data.get(KeyRings.HAS_SECRET)) == 1; + + // Hide the Checkbox and TextView since this is a single selection,user will be notified through message + mDeleteSecretKeyView.setVisibility(View.GONE); + // Set message depending on which key it is. + mMainMessage.setText(getString( + hasSecret ? R.string.secret_key_deletion_confirmation + : R.string.public_key_deletetion_confirmation, + userId)); + } else { + mDeleteSecretKeyView.setVisibility(View.VISIBLE); + mMainMessage.setText(R.string.key_deletion_confirmation_multi); + } + + builder.setIcon(R.drawable.ic_dialog_alert_holo_light); + builder.setPositiveButton(R.string.btn_delete, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + boolean success = false; + for(long masterKeyId : masterKeyIds) { + int count = activity.getContentResolver().delete( + KeyRingData.buildPublicKeyRingUri(Long.toString(masterKeyId)), null, null + ); + success = count > 0; + } + if (success) { + sendMessageToHandler(MESSAGE_OKAY, null); + } else { + sendMessageToHandler(MESSAGE_ERROR, null); + } + dismiss(); + } + }); + 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, 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); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java new file mode 100644 index 000000000..a4285c8e9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.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.TextView; +import com.beardedhen.androidbootstrap.BootstrapButton; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.FileHelper; +import org.sufficientlysecure.keychain.util.Log; + +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"; + + 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; + + private EditText mFilename; + private BootstrapButton mBrowse; + private CheckBox mCheckBox; + private TextView mMessageTextView; + + private static final int REQUEST_CODE = 0x00007004; + + /** + * Creates new instance of this file dialog fragment + */ + public static FileDialogFragment newInstance(Messenger messenger, String title, String message, + String defaultFile, String checkboxText) { + 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); + + 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); + + LayoutInflater inflater = (LayoutInflater) activity + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + AlertDialog.Builder alert = new AlertDialog.Builder(activity); + alert.setTitle(title); + + View view = inflater.inflate(R.layout.file_dialog, null); + + mMessageTextView = (TextView) view.findViewById(R.id.message); + mMessageTextView.setText(message); + + mFilename = (EditText) view.findViewById(R.id.input); + mFilename.setText(defaultFile); + mBrowse = (BootstrapButton) view.findViewById(R.id.btn_browse); + mBrowse.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // only .asc or .gpg files + // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc + // or gpg types! + FileHelper.openFile(FileDialogFragment.this, mFilename.getText().toString(), "*/*", + REQUEST_CODE); + } + }); + + 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 + */ + private void setFilename(String filename) { + AlertDialog dialog = (AlertDialog) getDialog(); + EditText filenameEditText = (EditText) dialog.findViewById(R.id.input); + + if (filenameEditText != null) { + filenameEditText.setText(filename); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode & 0xFFFF) { + case REQUEST_CODE: { + if (resultCode == Activity.RESULT_OK && data != null) { + try { + String path = data.getData().getPath(); + Log.d(Constants.TAG, "path=" + path); + + // set filename used in export/import dialogs + setFilename(path); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Nullpointer while retrieving path!", e); + } + } + + break; + } + + default: + super.onActivityResult(requestCode, resultCode, data); + + break; + } + } + + /** + * 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); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java new file mode 100644 index 000000000..a3feab959 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +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.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.view.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; +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.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.util.Log; + +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; + public static final int MESSAGE_CANCEL = 2; + + public static final String MESSAGE_DATA_PASSPHRASE = "passphrase"; + + private Messenger mMessenger; + private EditText mPassphraseEditText; + private boolean mCanKB; + + /** + * Shows passphrase dialog to cache a new passphrase the user enters for using it later for + * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks + * for a symmetric passphrase + */ + public static void show(FragmentActivity context, long keyId, Handler returnHandler) { + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + try { + PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(context, + messenger, keyId); + + passphraseDialog.show(context.getSupportFragmentManager(), "passphraseDialog"); + } catch (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); + } + } + + /** + * 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 (!PassphraseCacheService.hasPassphrase(context, secretKeyId)) { + throw new 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; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + final 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.passphrase_for_symmetric_encryption); + } else { + secretKey = ProviderHelper.getPGPSecretKeyRing(activity, secretKeyId).getSecretKey(); + + if (secretKey == null) { + alert.setTitle(R.string.title_key_not_found); + alert.setMessage(getString(R.string.key_not_found, secretKeyId)); + alert.setPositiveButton(android.R.string.ok, new OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }); + alert.setCancelable(false); + mCanKB = false; + return alert.create(); + } + String userId = PgpKeyHelper.getMainUserIdSafe(activity, secretKey); + + Log.d(Constants.TAG, "User id: '" + userId + "'"); + alert.setMessage(getString(R.string.passphrase_for, userId)); + } + + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.passphrase_dialog, 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(); + long curKeyIndex = 1; + boolean keyOK = true; + String passphrase = mPassphraseEditText.getText().toString(); + long keyId; + PGPSecretKey clickSecretKey = secretKey; + + if (clickSecretKey != null) { + while (keyOK) { + if (clickSecretKey != null) { // check again for loop + try { + PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + passphrase.toCharArray()); + PGPPrivateKey testKey = clickSecretKey + .extractPrivateKey(keyDecryptor); + if (testKey == null) { + if (!clickSecretKey.isMasterKey()) { + Toast.makeText(activity, + R.string.error_could_not_extract_private_key, + Toast.LENGTH_SHORT).show(); + + sendMessageToHandler(MESSAGE_CANCEL); + return; + } else { + clickSecretKey = PgpKeyHelper.getKeyNum(ProviderHelper + .getPGPSecretKeyRingWithKeyId(activity, secretKeyId), + curKeyIndex); + curKeyIndex++; // does post-increment work like C? + continue; + } + } else { + keyOK = false; + } + } catch (PGPException e) { + Toast.makeText(activity, R.string.wrong_passphrase, + Toast.LENGTH_SHORT).show(); + + sendMessageToHandler(MESSAGE_CANCEL); + return; + } + } else { + Toast.makeText(activity, R.string.error_could_not_extract_private_key, + Toast.LENGTH_SHORT).show(); + + sendMessageToHandler(MESSAGE_CANCEL); + return; // ran out of keys to try + } + } + 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); + if (!keyOK && clickSecretKey.getKeyID() != keyId) { + PassphraseCacheService.addCachedPassphrase(activity, clickSecretKey.getKeyID(), + passphrase); + } + + // also return passphrase back to activity + Bundle data = new Bundle(); + data.putString(MESSAGE_DATA_PASSPHRASE, passphrase); + + sendMessageToHandler(MESSAGE_OKAY, data); + } + }); + + alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + + mCanKB = true; + return alert.create(); + } + + @Override + public void onActivityCreated(Bundle arg0) { + super.onActivityCreated(arg0); + if (mCanKB) { + // request focus and open soft keyboard + mPassphraseEditText.requestFocus(); + getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE); + + mPassphraseEditText.setOnEditorActionListener(this); + } + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + + dismiss(); + sendMessageToHandler(MESSAGE_CANCEL); + } + + /** + * 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); + } + } + + /** + * 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); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java new file mode 100644 index 000000000..132a2ce86 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnKeyListener; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.KeyEvent; +import org.sufficientlysecure.keychain.R; + +public class ProgressDialogFragment extends DialogFragment { + private static final String ARG_MESSAGE = "message"; + private static final String ARG_STYLE = "style"; + private static final String ARG_CANCELABLE = "cancelable"; + + private OnCancelListener mOnCancelListener; + + /** + * Creates new instance of this fragment + * + * @param message + * @param style + * @param cancelable + * @return + */ + public static ProgressDialogFragment newInstance(String message, int style, boolean cancelable, + OnCancelListener onCancelListener) { + ProgressDialogFragment frag = new ProgressDialogFragment(); + Bundle args = new Bundle(); + args.putString(ARG_MESSAGE, message); + args.putInt(ARG_STYLE, style); + args.putBoolean(ARG_CANCELABLE, cancelable); + + frag.setArguments(args); + frag.mOnCancelListener = onCancelListener; + + 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 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 message + * @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); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + + if (this.mOnCancelListener != null) { + this.mOnCancelListener.onCancel(dialog); + } + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + ProgressDialog dialog = new ProgressDialog(activity); + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + dialog.setCancelable(false); + dialog.setCanceledOnTouchOutside(false); + + String message = getArguments().getString(ARG_MESSAGE); + int style = getArguments().getInt(ARG_STYLE); + boolean cancelable = getArguments().getBoolean(ARG_CANCELABLE); + + dialog.setMessage(message); + dialog.setProgressStyle(style); + + if (cancelable) { + dialog.setButton(DialogInterface.BUTTON_NEGATIVE, + activity.getString(R.string.progress_cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + } + + // 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; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java new file mode 100644 index 000000000..ae61c1470 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +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.TextView.OnEditorActionListener; +import android.widget.Toast; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + +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.enter_passphrase_twice); + + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.passphrase_repeat_dialog, 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.error_message, + getString(R.string.passphrases_do_not_match)), Toast.LENGTH_SHORT) + .show(); + return; + } + + if (passphrase1.equals("")) { + Toast.makeText( + activity, + getString(R.string.error_message, + getString(R.string.passphrase_must_not_be_empty)), + 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); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java new file mode 100644 index 000000000..741530b1d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareNfcDialogFragment.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.nfc.NfcAdapter; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import org.sufficientlysecure.htmltextview.HtmlTextView; +import org.sufficientlysecure.keychain.R; + +@TargetApi(Build.VERSION_CODES.JELLY_BEAN) +public class ShareNfcDialogFragment extends DialogFragment { + + /** + * Creates new instance of this fragment + */ + public static ShareNfcDialogFragment newInstance() { + ShareNfcDialogFragment frag = new ShareNfcDialogFragment(); + + return frag; + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final FragmentActivity activity = getActivity(); + + AlertDialog.Builder alert = new AlertDialog.Builder(activity); + + alert.setTitle(R.string.share_nfc_dialog); + alert.setCancelable(true); + + alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + dismiss(); + } + }); + + HtmlTextView textView = new HtmlTextView(getActivity()); + textView.setPadding(8, 8, 8, 8); + alert.setView(textView); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + textView.setText(getString(R.string.error) + ": " + + getString(R.string.error_jelly_bean_needed)); + } else { + // check if NFC Adapter is available + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); + if (nfcAdapter == null) { + textView.setText(getString(R.string.error) + ": " + + getString(R.string.error_nfc_needed)); + } else { + // nfc works... + textView.setHtmlFromRawResource(getActivity(), R.raw.nfc_beam_share); + + alert.setNegativeButton(R.string.menu_beam_preferences, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + Intent intentSettings = new Intent( + Settings.ACTION_NFCSHARING_SETTINGS); + startActivity(intentSettings); + } + }); + } + } + + // no flickering when clicking textview for Android < 4 + // aboutTextView.setTextColor(getResources().getColor(android.R.color.black)); + + return alert.create(); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareQrCodeDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareQrCodeDialogFragment.java new file mode 100644 index 000000000..b6ff139df --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareQrCodeDialogFragment.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.dialog; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.QrCodeUtils; + +import java.util.ArrayList; + +public class ShareQrCodeDialogFragment extends DialogFragment { + private static final String ARG_KEY_URI = "uri"; + private static final String ARG_FINGERPRINT_ONLY = "fingerprint_only"; + + private ImageView mImage; + private TextView mText; + + private boolean mFingerprintOnly; + + private ArrayList mContentList; + private int mCounter; + + private static final int QR_CODE_SIZE = 1000; + + /** + * Creates new instance of this dialog fragment + */ + public static ShareQrCodeDialogFragment newInstance(Uri dataUri, boolean fingerprintOnly) { + ShareQrCodeDialogFragment frag = new ShareQrCodeDialogFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_KEY_URI, dataUri); + args.putBoolean(ARG_FINGERPRINT_ONLY, fingerprintOnly); + + frag.setArguments(args); + + return frag; + } + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + + Uri dataUri = getArguments().getParcelable(ARG_KEY_URI); + mFingerprintOnly = getArguments().getBoolean(ARG_FINGERPRINT_ONLY); + + AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); + + alert.setTitle(R.string.share_qr_code_dialog_title); + + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.share_qr_code_dialog, null); + alert.setView(view); + + mImage = (ImageView) view.findViewById(R.id.share_qr_code_dialog_image); + mText = (TextView) view.findViewById(R.id.share_qr_code_dialog_text); + + String content = null; + if (mFingerprintOnly) { + alert.setPositiveButton(R.string.btn_okay, null); + + byte[] blob = (byte[]) ProviderHelper.getGenericData( + getActivity(), KeyRings.buildUnifiedKeyRingUri(dataUri), + KeyRings.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); + if(blob == null) { + // TODO error handling?! + return null; + } + + String fingerprint = PgpKeyHelper.convertFingerprintToHex(blob); + mText.setText(getString(R.string.share_qr_code_dialog_fingerprint_text) + " " + fingerprint); + content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; + setQrCode(content); + } else { + mText.setText(R.string.share_qr_code_dialog_start); + + // TODO works, but + long masterKeyId = ProviderHelper.getMasterKeyId(getActivity(), dataUri); + // get public keyring as ascii armored string + ArrayList keyringArmored = ProviderHelper.getKeyRingsAsArmoredString( + getActivity(), new long[] { masterKeyId }); + + // TODO: binary? + + content = keyringArmored.get(0); + + // OnClickListener are set in onResume to prevent automatic dismissing of Dialogs + // http://bit.ly/O5vfaR + alert.setPositiveButton(R.string.btn_next, null); + alert.setNegativeButton(android.R.string.cancel, null); + + mContentList = splitString(content, 1000); + + // start with first + mCounter = 0; + updatePartsQrCode(); + } + + return alert.create(); + } + + @Override + public void onResume() { + super.onResume(); + + if (!mFingerprintOnly) { + AlertDialog alertDialog = (AlertDialog) getDialog(); + final Button backButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); + final Button nextButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + + backButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mCounter > 0) { + mCounter--; + updatePartsQrCode(); + updateDialog(backButton, nextButton); + } else { + dismiss(); + } + } + }); + nextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + if (mCounter < mContentList.size() - 1) { + mCounter++; + updatePartsQrCode(); + updateDialog(backButton, nextButton); + } else { + dismiss(); + } + } + }); + } + } + + private void updatePartsQrCode() { + // Content: ,, + setQrCode(mCounter + "," + mContentList.size() + "," + mContentList.get(mCounter)); + } + + private void setQrCode(String data) { + mImage.setImageBitmap(QrCodeUtils.getQRCodeBitmap(data, QR_CODE_SIZE)); + } + + private void updateDialog(Button backButton, Button nextButton) { + if (mCounter == 0) { + backButton.setText(android.R.string.cancel); + } else { + backButton.setText(R.string.btn_back); + } + if (mCounter == mContentList.size() - 1) { + nextButton.setText(android.R.string.ok); + } else { + nextButton.setText(R.string.btn_next); + } + + mText.setText(getResources().getString(R.string.share_qr_code_dialog_progress, + mCounter + 1, mContentList.size())); + } + + /** + * Split String by number of characters + * + * @param text + * @param size + * @return + */ + private ArrayList splitString(String text, int size) { + ArrayList strings = new ArrayList(); + int index = 0; + while (index < text.length()) { + strings.add(text.substring(index, Math.min(index + size, text.length()))); + index += size; + } + + return strings; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java new file mode 100644 index 000000000..7b21c189d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/Editor.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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, boolean wasNewItem); + public void onEdited(); + } + + public void setEditorListener(EditorListener listener); + public boolean needsSaving(); +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FixedListView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FixedListView.java new file mode 100644 index 000000000..da29f808a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FixedListView.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ListView; + +/** + * Automatically calculate height of ListView based on contained items. This enables to put this + * ListView into a ScrollView without messing up. + *

+ * from + * http://stackoverflow.com/questions/2419246/how-do-i-create-a-listview-thats-not-in-a-scrollview- + * or-has-the-scrollview-dis + */ +public class FixedListView extends ListView { + + public FixedListView(Context context) { + super(context); + } + + public FixedListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public FixedListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Calculate height of the entire list by providing a very large + // height hint. But do not use the highest 2 bits of this integer; + // those are reserved for the MeasureSpec mode. + int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, expandSpec); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FoldableLinearLayout.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FoldableLinearLayout.java new file mode 100644 index 000000000..6b2f3bf06 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/FoldableLinearLayout.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.beardedhen.androidbootstrap.FontAwesomeText; +import org.sufficientlysecure.keychain.R; + +/** + * Class representing a LinearLayout that can fold and hide it's content when pressed + * To use just add the following to your xml layout + + + + + + + + */ +public class FoldableLinearLayout extends LinearLayout { + + private FontAwesomeText mFoldableIcon; + private boolean mFolded; + private boolean mHasMigrated = false; + private Integer mShortAnimationDuration = null; + private TextView mFoldableTextView = null; + private LinearLayout mFoldableContainer = null; + private View mFoldableLayout = null; + + private String mFoldedIconName; + private String mUnFoldedIconName; + private String mFoldedLabel; + private String mUnFoldedLabel; + + public FoldableLinearLayout(Context context) { + super(context); + processAttributes(context, null); + } + + public FoldableLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + processAttributes(context, attrs); + } + + public FoldableLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs); + processAttributes(context, attrs); + } + + /** + * Load given attributes to inner variables, + * @param context + * @param attrs + */ + private void processAttributes(Context context, AttributeSet attrs) { + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.FoldableLinearLayout, 0, 0); + mFoldedIconName = a.getString(R.styleable.FoldableLinearLayout_foldedIcon); + mUnFoldedIconName = a.getString(R.styleable.FoldableLinearLayout_unFoldedIcon); + mFoldedLabel = a.getString(R.styleable.FoldableLinearLayout_foldedLabel); + mUnFoldedLabel = a.getString(R.styleable.FoldableLinearLayout_unFoldedLabel); + a.recycle(); + } + // If any attribute isn't found then set a default one + mFoldedIconName = (mFoldedIconName == null) ? "fa-chevron-right" : mFoldedIconName; + mUnFoldedIconName = (mUnFoldedIconName == null) ? "fa-chevron-down" : mUnFoldedIconName; + mFoldedLabel = (mFoldedLabel == null) ? context.getString(R.id.none) : mFoldedLabel; + mUnFoldedLabel = (mUnFoldedLabel == null) ? context.getString(R.id.none) : mUnFoldedLabel; + } + + @Override + protected void onFinishInflate() { + // if the migration has already happened + // there is no need to move any children + if (!mHasMigrated) { + migrateChildrenToContainer(); + mHasMigrated = true; + } + + initialiseInnerViews(); + + super.onFinishInflate(); + } + + /** + * Migrates Child views as declared in xml to the inner foldableContainer + */ + private void migrateChildrenToContainer() { + // Collect children of FoldableLinearLayout as declared in XML + int childNum = getChildCount(); + View[] children = new View[childNum]; + + for (int i = 0; i < childNum; i++) { + children[i] = getChildAt(i); + } + if (children[0].getId() == R.id.foldableControl) { + + } + + // remove all of them from FoldableLinearLayout + detachAllViewsFromParent(); + + // Inflate the inner foldable_linearlayout.xml + LayoutInflater inflator = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + mFoldableLayout = inflator.inflate(R.layout.foldable_linearlayout, this, true); + mFoldableContainer = (LinearLayout) mFoldableLayout.findViewById(R.id.foldableContainer); + + // Push previously collected children into foldableContainer. + for (int i = 0; i < childNum; i++) { + addView(children[i]); + } + } + + private void initialiseInnerViews() { + mFoldableIcon = (FontAwesomeText) mFoldableLayout.findViewById(R.id.foldableIcon); + mFoldableIcon.setIcon(mFoldedIconName); + mFoldableTextView = (TextView) mFoldableLayout.findViewById(R.id.foldableText); + mFoldableTextView.setText(mFoldedLabel); + + // retrieve and cache the system's short animation time + mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); + + LinearLayout foldableControl = (LinearLayout) mFoldableLayout.findViewById(R.id.foldableControl); + foldableControl.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mFolded = !mFolded; + if (mFolded) { + mFoldableIcon.setIcon(mUnFoldedIconName); + mFoldableContainer.setVisibility(View.VISIBLE); + AlphaAnimation animation = new AlphaAnimation(0f, 1f); + animation.setDuration(mShortAnimationDuration); + mFoldableContainer.startAnimation(animation); + mFoldableTextView.setText(mUnFoldedLabel); + + } else { + mFoldableIcon.setIcon(mFoldedIconName); + AlphaAnimation animation = new AlphaAnimation(1f, 0f); + animation.setDuration(mShortAnimationDuration); + animation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { } + + @Override + public void onAnimationEnd(Animation animation) { + // making sure that at the end the container is completely removed from view + mFoldableContainer.setVisibility(View.GONE); + } + + @Override + public void onAnimationRepeat(Animation animation) { } + }); + mFoldableContainer.startAnimation(animation); + mFoldableTextView.setText(mFoldedLabel); + } + } + }); + + } + + /** + * Adds provided child view to foldableContainer View + * @param child + */ + @Override + public void addView(View child) { + if (mFoldableContainer != null) { + mFoldableContainer.addView(child); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java new file mode 100644 index 000000000..6e1e4c678 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java @@ -0,0 +1,94 @@ +/* + * 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java new file mode 100644 index 000000000..c7bd1c987 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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.annotation.TargetApi; +import android.app.DatePickerDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.text.format.DateUtils; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.DatePicker; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; + +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.spongycastle.bcpg.sig.KeyFlags; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPSecretKey; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.util.Choice; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.Vector; + +public class KeyEditor extends LinearLayout implements Editor, OnClickListener { + private PGPSecretKey mKey; + + private EditorListener mEditorListener = null; + + private boolean mIsMasterKey; + BootstrapButton mDeleteButton; + TextView mAlgorithm; + TextView mKeyId; + TextView mCreationDate; + BootstrapButton mExpiryDateButton; + GregorianCalendar mCreatedDate; + GregorianCalendar mExpiryDate; + GregorianCalendar mOriginalExpiryDate = null; + CheckBox mChkCertify; + CheckBox mChkSign; + CheckBox mChkEncrypt; + CheckBox mChkAuthenticate; + int mUsage; + int mOriginalUsage; + boolean mIsNewKey; + + private CheckBox.OnCheckedChangeListener mCheckChanged = new CheckBox.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) + { + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + } + }; + + + private int mDatePickerResultCount = 0; + private DatePickerDialog.OnDateSetListener mExpiryDateSetListener = + new DatePickerDialog.OnDateSetListener() { + public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { + // Note: Ignore results after the first one - android sends multiples. + if (mDatePickerResultCount++ == 0) { + GregorianCalendar date = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + date.set(year, monthOfYear, dayOfMonth); + if (mOriginalExpiryDate != null) { + long numDays = (date.getTimeInMillis() / 86400000) - + (mOriginalExpiryDate.getTimeInMillis() / 86400000); + if (numDays == 0) { + setExpiryDate(mOriginalExpiryDate); + } else { + setExpiryDate(date); + } + } else { + setExpiryDate(date); + } + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + } + } + }; + + 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 = (BootstrapButton) findViewById(R.id.expiry); + + mDeleteButton = (BootstrapButton) findViewById(R.id.delete); + mDeleteButton.setOnClickListener(this); + mChkCertify = (CheckBox) findViewById(R.id.chkCertify); + mChkCertify.setOnCheckedChangeListener(mCheckChanged); + mChkSign = (CheckBox) findViewById(R.id.chkSign); + mChkSign.setOnCheckedChangeListener(mCheckChanged); + mChkEncrypt = (CheckBox) findViewById(R.id.chkEncrypt); + mChkEncrypt.setOnCheckedChangeListener(mCheckChanged); + mChkAuthenticate = (CheckBox) findViewById(R.id.chkAuthenticate); + mChkAuthenticate.setOnCheckedChangeListener(mCheckChanged); + + setExpiryDate(null); + + mExpiryDateButton.setOnClickListener(new OnClickListener() { + @TargetApi(11) + public void onClick(View v) { + GregorianCalendar date = mExpiryDate; + if (date == null) { + date = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + } + /* + * Using custom DatePickerDialog which overrides the setTitle because + * the DatePickerDialog title is buggy (unix warparound bug). + * See: https://code.google.com/p/android/issues/detail?id=49066 + */ + DatePickerDialog dialog = new ExpiryDatePickerDialog(getContext(), + mExpiryDateSetListener, date.get(Calendar.YEAR), date.get(Calendar.MONTH), + date.get(Calendar.DAY_OF_MONTH)); + mDatePickerResultCount = 0; + dialog.setCancelable(true); + dialog.setButton(Dialog.BUTTON_NEGATIVE, + getContext().getString(R.string.btn_no_date), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // Note: Ignore results after the first one - android sends multiples. + if (mDatePickerResultCount++ == 0) { + setExpiryDate(null); + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + } + } + }); + + // setCalendarViewShown() is supported from API 11 onwards. + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { + // Hide calendarView in tablets because of the unix warparound bug. + dialog.getDatePicker().setCalendarViewShown(false); + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { + if (dialog != null && mCreatedDate != null) { + dialog.getDatePicker() + .setMinDate( + mCreatedDate.getTime().getTime() + DateUtils.DAY_IN_MILLIS); + } else { + //When created date isn't available + dialog.getDatePicker().setMinDate(date.getTime().getTime() + DateUtils.DAY_IN_MILLIS); + } + } + + dialog.show(); + } + }); + + super.onFinishInflate(); + } + + public void setCanBeEdited(boolean canBeEdited) { + if (!canBeEdited) { + mDeleteButton.setVisibility(View.INVISIBLE); + mExpiryDateButton.setEnabled(false); + mChkSign.setEnabled(false); //certify is always disabled + mChkEncrypt.setEnabled(false); + mChkAuthenticate.setEnabled(false); + } + } + + public void setValue(PGPSecretKey key, boolean isMasterKey, int usage, boolean isNewKey) { + mKey = key; + + mIsMasterKey = isMasterKey; + if (mIsMasterKey) { + mDeleteButton.setVisibility(View.INVISIBLE); + } + + mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(key)); + String keyIdStr = PgpKeyHelper.convertKeyIdToHex(key.getKeyID()); + mKeyId.setText(keyIdStr); + + Vector choices = new Vector(); + boolean isElGamalKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT); + boolean isDSAKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.DSA); + if (isElGamalKey) { + mChkSign.setVisibility(View.INVISIBLE); + TableLayout table = (TableLayout) findViewById(R.id.table_keylayout); + TableRow row = (TableRow) findViewById(R.id.row_sign); + table.removeView(row); + } + if (isDSAKey) { + mChkEncrypt.setVisibility(View.INVISIBLE); + TableLayout table = (TableLayout) findViewById(R.id.table_keylayout); + TableRow row = (TableRow) findViewById(R.id.row_encrypt); + table.removeView(row); + } + if (!mIsMasterKey) { + mChkCertify.setVisibility(View.INVISIBLE); + TableLayout table = (TableLayout) findViewById(R.id.table_keylayout); + TableRow row = (TableRow) findViewById(R.id.row_certify); + table.removeView(row); + } else { + TextView mLabelUsage2 = (TextView) findViewById(R.id.label_usage2); + mLabelUsage2.setVisibility(View.INVISIBLE); + } + + int selectId = 0; + mIsNewKey = isNewKey; + if (isNewKey) { + mUsage = usage; + mChkCertify.setChecked((usage & KeyFlags.CERTIFY_OTHER) == KeyFlags.CERTIFY_OTHER); + mChkSign.setChecked((usage & KeyFlags.SIGN_DATA) == KeyFlags.SIGN_DATA); + mChkEncrypt.setChecked(((usage & KeyFlags.ENCRYPT_COMMS) == KeyFlags.ENCRYPT_COMMS) || + ((usage & KeyFlags.ENCRYPT_STORAGE) == KeyFlags.ENCRYPT_STORAGE)); + mChkAuthenticate.setChecked((usage & KeyFlags.AUTHENTICATION) == KeyFlags.AUTHENTICATION); + } else { + mUsage = PgpKeyHelper.getKeyUsage(key); + mOriginalUsage = mUsage; + if (key.isMasterKey()) { + mChkCertify.setChecked(PgpKeyHelper.isCertificationKey(key)); + } + mChkSign.setChecked(PgpKeyHelper.isSigningKey(key)); + mChkEncrypt.setChecked(PgpKeyHelper.isEncryptionKey(key)); + mChkAuthenticate.setChecked(PgpKeyHelper.isAuthenticationKey(key)); + } + + GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + cal.setTime(PgpKeyHelper.getCreationDate(key)); + setCreatedDate(cal); + cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + Date expiryDate = PgpKeyHelper.getExpiryDate(key); + if (expiryDate == null) { + setExpiryDate(null); + } else { + cal.setTime(PgpKeyHelper.getExpiryDate(key)); + setExpiryDate(cal); + mOriginalExpiryDate = 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, mIsNewKey); + } + } + } + + public void setEditorListener(EditorListener listener) { + mEditorListener = listener; + } + + private void setCreatedDate(GregorianCalendar date) { + mCreatedDate = date; + if (date == null) { + mCreationDate.setText(getContext().getString(R.string.none)); + } else { + mCreationDate.setText(DateFormat.getDateInstance().format(date.getTime())); + } + } + + private void setExpiryDate(GregorianCalendar date) { + mExpiryDate = date; + if (date == null) { + mExpiryDateButton.setText(getContext().getString(R.string.none)); + } else { + mExpiryDateButton.setText(DateFormat.getDateInstance().format(date.getTime())); + } + } + + public GregorianCalendar getExpiryDate() { + return mExpiryDate; + } + + public int getUsage() { + mUsage = (mUsage & ~KeyFlags.CERTIFY_OTHER) | + (mChkCertify.isChecked() ? KeyFlags.CERTIFY_OTHER : 0); + mUsage = (mUsage & ~KeyFlags.SIGN_DATA) | + (mChkSign.isChecked() ? KeyFlags.SIGN_DATA : 0); + mUsage = (mUsage & ~KeyFlags.ENCRYPT_COMMS) | + (mChkEncrypt.isChecked() ? KeyFlags.ENCRYPT_COMMS : 0); + mUsage = (mUsage & ~KeyFlags.ENCRYPT_STORAGE) | + (mChkEncrypt.isChecked() ? KeyFlags.ENCRYPT_STORAGE : 0); + mUsage = (mUsage & ~KeyFlags.AUTHENTICATION) | + (mChkAuthenticate.isChecked() ? KeyFlags.AUTHENTICATION : 0); + + return mUsage; + } + + public boolean needsSaving() { + if (mIsNewKey) { + return true; + } + + boolean retval = (getUsage() != mOriginalUsage); + + boolean dateChanged; + boolean mOEDNull = (mOriginalExpiryDate == null); + boolean mEDNull = (mExpiryDate == null); + if (mOEDNull != mEDNull) { + dateChanged = true; + } else { + if (mOEDNull) { + //both null, no change + dateChanged = false; + } else { + dateChanged = ((mExpiryDate.compareTo(mOriginalExpiryDate)) != 0); + } + } + retval |= dateChanged; + + return retval; + } + + public boolean getIsNewKey() { + return mIsNewKey; + } +} + +class ExpiryDatePickerDialog extends DatePickerDialog { + + public ExpiryDatePickerDialog(Context context, OnDateSetListener callBack, + int year, int monthOfYear, int dayOfMonth) { + super(context, callBack, year, monthOfYear, dayOfMonth); + } + + //Set permanent title. + public void setTitle(CharSequence title) { + super.setTitle(getContext().getString(R.string.expiry_date_dialog_title)); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java new file mode 100644 index 000000000..171763672 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.beardedhen.androidbootstrap.BootstrapButton; +import org.sufficientlysecure.keychain.R; + +public class KeyServerEditor extends LinearLayout implements Editor, OnClickListener { + private EditorListener mEditorListener = null; + + BootstrapButton 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 = (BootstrapButton) findViewById(R.id.delete); + mDeleteButton.setOnClickListener(this); + + super.onFinishInflate(); + } + + public void setValue(String value) { + mServer.setText(value); + } + + public String getValue() { + return mServer.getText().toString().trim(); + } + + public void onClick(View v) { + final ViewGroup parent = (ViewGroup) getParent(); + if (v == mDeleteButton) { + parent.removeView(this); + if (mEditorListener != null) { + mEditorListener.onDeleted(this, false); + } + } + } + + @Override + public boolean needsSaving() { + return false; + } + + public void setEditorListener(EditorListener listener) { + mEditorListener = listener; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SectionView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SectionView.java new file mode 100644 index 000000000..fb59cd3b7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SectionView.java @@ -0,0 +1,429 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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.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.support.v7.app.ActionBarActivity; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.spongycastle.openpgp.PGPKeyFlags; +import org.spongycastle.openpgp.PGPSecretKey; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.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.CreateKeyDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener; +import org.sufficientlysecure.keychain.util.Choice; + +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +public class SectionView extends LinearLayout implements OnClickListener, EditorListener, Editor { + private LayoutInflater mInflater; + private BootstrapButton mPlusButton; + private ViewGroup mEditors; + private TextView mTitle; + private int mType = 0; + private EditorListener mEditorListener = null; + + private Choice mNewKeyAlgorithmChoice; + private int mNewKeySize; + private boolean mOldItemDeleted = false; + private ArrayList mDeletedIDs = new ArrayList(); + private ArrayList mDeletedKeys = new ArrayList(); + private boolean mCanBeEdited = true; + + private ActionBarActivity mActivity; + + private ProgressDialogFragment mGeneratingDialog; + + public void setEditorListener(EditorListener listener) { + mEditorListener = listener; + } + + public SectionView(Context context) { + super(context); + mActivity = (ActionBarActivity) context; + } + + public SectionView(Context context, AttributeSet attrs) { + super(context, attrs); + mActivity = (ActionBarActivity) 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_user_ids); + break; + } + + case Id.type.key: { + mTitle.setText(R.string.section_keys); + break; + } + + default: { + break; + } + } + } + + public void setCanBeEdited(boolean canBeEdited) { + mCanBeEdited = canBeEdited; + if (!mCanBeEdited) { + mPlusButton.setVisibility(View.INVISIBLE); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void onFinishInflate() { + mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + setDrawingCacheEnabled(true); + setAlwaysDrawnWithCacheEnabled(true); + + mPlusButton = (BootstrapButton) findViewById(R.id.plusbutton); + mPlusButton.setOnClickListener(this); + + mEditors = (ViewGroup) findViewById(R.id.editors); + mTitle = (TextView) findViewById(R.id.title); + + updateEditorsVisible(); + super.onFinishInflate(); + } + + /** + * {@inheritDoc} + */ + public void onDeleted(Editor editor, boolean wasNewItem) { + mOldItemDeleted |= !wasNewItem; + if (mOldItemDeleted) { + if (mType == Id.type.user_id) { + mDeletedIDs.add(((UserIdEditor) editor).getOriginalID()); + } else if (mType == Id.type.key) { + mDeletedKeys.add(((KeyEditor) editor).getValue()); + } + } + this.updateEditorsVisible(); + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + } + + @Override + public void onEdited() { + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + } + + protected void updateEditorsVisible() { + final boolean hasChildren = mEditors.getChildCount() > 0; + mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE); + } + + public boolean needsSaving() { + //check each view for needs saving, take account of deleted items + boolean ret = mOldItemDeleted; + for (int i = 0; i < mEditors.getChildCount(); ++i) { + Editor editor = (Editor) mEditors.getChildAt(i); + ret |= editor.needsSaving(); + if (mType == Id.type.user_id) { + ret |= ((UserIdEditor) editor).primarySwapped(); + } + } + return ret; + } + + public boolean primaryChanged() { + boolean ret = false; + for (int i = 0; i < mEditors.getChildCount(); ++i) { + Editor editor = (Editor) mEditors.getChildAt(i); + if (mType == Id.type.user_id) { + ret |= ((UserIdEditor) editor).primarySwapped(); + } + } + return ret; + } + + public String getOriginalPrimaryID() { + //NB: this will have to change when we change how Primary IDs are chosen, and so we need to be + // careful about where Master key capabilities are stored... multiple primaries and + // revoked ones make this harder than the simple case we are continuing to assume here + for (int i = 0; i < mEditors.getChildCount(); ++i) { + Editor editor = (Editor) mEditors.getChildAt(i); + if (mType == Id.type.user_id) { + if (((UserIdEditor) editor).getIsOriginallyMainUserID()) { + return ((UserIdEditor) editor).getOriginalID(); + } + } + } + return null; + } + + public ArrayList getOriginalIDs() { + ArrayList orig = new ArrayList(); + if (mType == Id.type.user_id) { + for (int i = 0; i < mEditors.getChildCount(); ++i) { + UserIdEditor editor = (UserIdEditor) mEditors.getChildAt(i); + if (editor.isMainUserId()) { + orig.add(0, editor.getOriginalID()); + } else { + orig.add(editor.getOriginalID()); + } + } + return orig; + } else { + return null; + } + } + + public ArrayList getDeletedIDs() { + return mDeletedIDs; + } + + public ArrayList getDeletedKeys() { + return mDeletedKeys; + } + + public List getNeedsSavingArray() { + ArrayList mList = new ArrayList(); + for (int i = 0; i < mEditors.getChildCount(); ++i) { + Editor editor = (Editor) mEditors.getChildAt(i); + mList.add(editor.needsSaving()); + } + return mList; + } + + public List getNewIDFlags() { + ArrayList mList = new ArrayList(); + for (int i = 0; i < mEditors.getChildCount(); ++i) { + UserIdEditor editor = (UserIdEditor) mEditors.getChildAt(i); + if (editor.isMainUserId()) { + mList.add(0, editor.getIsNewID()); + } else { + mList.add(editor.getIsNewID()); + } + } + return mList; + } + + public List getNewKeysArray() { + ArrayList mList = new ArrayList(); + if (mType == Id.type.key) { + for (int i = 0; i < mEditors.getChildCount(); ++i) { + KeyEditor editor = (KeyEditor) mEditors.getChildAt(i); + mList.add(editor.getIsNewKey()); + } + } + return mList; + } + + /** + * {@inheritDoc} + */ + public void onClick(View v) { + if (mCanBeEdited) { + switch (mType) { + case Id.type.user_id: { + UserIdEditor view = (UserIdEditor) mInflater.inflate( + R.layout.edit_key_user_id_item, mEditors, false); + view.setEditorListener(this); + view.setValue("", mEditors.getChildCount() == 0, true); + mEditors.addView(view); + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + break; + } + + case Id.type.key: { + CreateKeyDialogFragment mCreateKeyDialogFragment = + CreateKeyDialogFragment.newInstance(mEditors.getChildCount()); + mCreateKeyDialogFragment + .setOnAlgorithmSelectedListener( + new CreateKeyDialogFragment.OnAlgorithmSelectedListener() { + @Override + public void onAlgorithmSelected(Choice algorithmChoice, int keySize) { + mNewKeyAlgorithmChoice = algorithmChoice; + mNewKeySize = keySize; + createKey(); + } + }); + mCreateKeyDialogFragment.show(mActivity.getSupportFragmentManager(), "createKeyDialog"); + break; + } + + default: { + break; + } + } + this.updateEditorsVisible(); + } + } + + public void setUserIds(Vector 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, mEditors.getChildCount() == 0, false); + view.setCanBeEdited(mCanBeEdited); + mEditors.addView(view); + } + + this.updateEditorsVisible(); + } + + public void setKeys(Vector list, Vector usages, boolean newKeys) { + 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), newKeys); + view.setCanBeEdited(mCanBeEdited); + mEditors.addView(view); + } + + this.updateEditorsVisible(); + } + + private void createKey() { + // Send all information needed to service to edit key in other thread + final Intent intent = new Intent(mActivity, KeychainIntentService.class); + + intent.setAction(KeychainIntentService.ACTION_GENERATE_KEY); + + // fill values for this action + Bundle data = new Bundle(); + Boolean isMasterKey; + + String passphrase; + if (mEditors.getChildCount() > 0) { + PGPSecretKey masterKey = ((KeyEditor) mEditors.getChildAt(0)).getValue(); + passphrase = PassphraseCacheService + .getCachedPassphrase(mActivity, masterKey.getKeyID()); + isMasterKey = false; + } else { + passphrase = ""; + isMasterKey = true; + } + data.putBoolean(KeychainIntentService.GENERATE_KEY_MASTER_KEY, isMasterKey); + 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( + getResources().getQuantityString(R.plurals.progress_generating, 1), + ProgressDialog.STYLE_SPINNER, + true, + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + mActivity.stopService(intent); + } + }); + + // Message is received after generating is done in KeychainIntentService + KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(mActivity, + mGeneratingDialog) { + public void handleMessage(Message message) { + // handle messages by standard KeychainIntentServiceHandler first + super.handleMessage(message); + + if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { + // get new key from data bundle returned from service + Bundle data = message.getData(); + PGPSecretKey newKey = (PGPSecretKey) PgpConversionHelper + .BytesToPGPSecretKey(data + .getByteArray(KeychainIntentService.RESULT_NEW_KEY)); + addGeneratedKeyToView(newKey); + } + } + }; + + // 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); + } + + private void addGeneratedKeyToView(PGPSecretKey newKey) { + // add view with new key + KeyEditor view = (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item, + mEditors, false); + view.setEditorListener(SectionView.this); + int usage = 0; + if (mEditors.getChildCount() == 0) { + usage = PGPKeyFlags.CAN_CERTIFY; + } + view.setValue(newKey, newKey.isMasterKey(), usage, true); + mEditors.addView(view); + SectionView.this.updateEditorsVisible(); + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UnderlineTextView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UnderlineTextView.java new file mode 100644 index 000000000..937a48e48 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UnderlineTextView.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2013 Eric Frohnhoefer + * + * 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.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.TextView; + +/** + * Copied from StickyListHeaders lib example + * + * @author Eric Frohnhoefer + */ +public class UnderlineTextView extends TextView { + private final Paint mPaint = new Paint(); + private int mUnderlineHeight = 0; + + public UnderlineTextView(Context context) { + this(context, null); + } + + public UnderlineTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public UnderlineTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + Resources r = getResources(); + mUnderlineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, + r.getDisplayMetrics()); + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + super.setPadding(left, top, right, bottom + mUnderlineHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the underline the same color as the text + mPaint.setColor(getTextColors().getDefaultColor()); + canvas.drawRect(0, getHeight() - mUnderlineHeight, getWidth(), getHeight(), mPaint); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java new file mode 100644 index 000000000..2253872d5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.Patterns; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ContactHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; + +import java.util.regex.Matcher; + +public class UserIdEditor extends LinearLayout implements Editor, OnClickListener { + private EditorListener mEditorListener = null; + + private BootstrapButton mDeleteButton; + private RadioButton mIsMainUserId; + private String mOriginalID; + private EditText mName; + private String mOriginalName; + private AutoCompleteTextView mEmail; + private String mOriginalEmail; + private EditText mComment; + private String mOriginalComment; + private boolean mOriginallyMainUserID; + private boolean mIsNewId; + + public void setCanBeEdited(boolean canBeEdited) { + if (!canBeEdited) { + mDeleteButton.setVisibility(View.INVISIBLE); + mName.setEnabled(false); + mIsMainUserId.setEnabled(false); + mEmail.setEnabled(false); + mComment.setEnabled(false); + } + } + + 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); + } + + TextWatcher mTextWatcher = new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void afterTextChanged(Editable s) { + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + } + }; + + @Override + protected void onFinishInflate() { + setDrawingCacheEnabled(true); + setAlwaysDrawnWithCacheEnabled(true); + + mDeleteButton = (BootstrapButton) findViewById(R.id.delete); + mDeleteButton.setOnClickListener(this); + mIsMainUserId = (RadioButton) findViewById(R.id.isMainUserId); + mIsMainUserId.setOnClickListener(this); + + mName = (EditText) findViewById(R.id.name); + mName.addTextChangedListener(mTextWatcher); + mEmail = (AutoCompleteTextView) findViewById(R.id.email); + mComment = (EditText) findViewById(R.id.comment); + mComment.addTextChangedListener(mTextWatcher); + + + mEmail.setThreshold(1); // Start working from first character + mEmail.setAdapter( + new ArrayAdapter + (this.getContext(), android.R.layout.simple_dropdown_item_1line, + ContactHelper.getMailAccounts(getContext()) + )); + mEmail.addTextChangedListener(new TextWatcher(){ + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { } + + @Override + public void afterTextChanged(Editable editable) { + String email = editable.toString(); + if (email.length() > 0) { + Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email); + if (emailMatcher.matches()) { + mEmail.setCompoundDrawablesWithIntrinsicBounds(0, 0, + android.R.drawable.presence_online, 0); + } else { + mEmail.setCompoundDrawablesWithIntrinsicBounds(0, 0, + android.R.drawable.presence_offline, 0); + } + } else { + // remove drawable if email is empty + mEmail.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + } + }); + + super.onFinishInflate(); + } + + public void setValue(String userId, boolean isMainID, boolean isNewId) { + + mName.setText(""); + mOriginalName = ""; + mComment.setText(""); + mOriginalComment = ""; + mEmail.setText(""); + mOriginalEmail = ""; + mIsNewId = isNewId; + mOriginalID = userId; + + String[] result = PgpKeyHelper.splitUserId(userId); + if (result[0] != null) { + mName.setText(result[0]); + mOriginalName = result[0]; + } + if (result[1] != null) { + mEmail.setText(result[1]); + mOriginalEmail = result[1]; + } + if (result[2] != null) { + mComment.setText(result[2]); + mOriginalComment = result[2]; + } + + mOriginallyMainUserID = isMainID; + setIsMainUserId(isMainID); + } + + public String getValue() { + String name = ("" + mName.getText()).trim(); + String email = ("" + mEmail.getText()).trim(); + String comment = ("" + mComment.getText()).trim(); + + String userId = name; + if (comment.length() > 0) { + userId += " (" + comment + ")"; + } + if (email.length() > 0) { + userId += " <" + email + ">"; + } + + if (userId.equals("")) { + // ok, empty one... + return userId; + } + //TODO: check gpg accepts an entirely empty ID packet. specs say this is allowed + 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, mIsNewId); + } + 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); + } + } + if (mEditorListener != null) { + mEditorListener.onEdited(); + } + } + } + + public void setIsMainUserId(boolean value) { + mIsMainUserId.setChecked(value); + } + + public boolean isMainUserId() { + return mIsMainUserId.isChecked(); + } + + public void setEditorListener(EditorListener listener) { + mEditorListener = listener; + } + + @Override + public boolean needsSaving() { + boolean retval = false; //(mOriginallyMainUserID != isMainUserId()); + retval |= !(mOriginalName.equals(("" + mName.getText()).trim())); + retval |= !(mOriginalEmail.equals(("" + mEmail.getText()).trim())); + retval |= !(mOriginalComment.equals(("" + mComment.getText()).trim())); + retval |= mIsNewId; + return retval; + } + + public boolean getIsOriginallyMainUserID() { + return mOriginallyMainUserID; + } + + public boolean primarySwapped() { + return (mOriginallyMainUserID != isMainUserId()); + } + + public String getOriginalID() { + return mOriginalID; + } + + public boolean getIsNewID() { return mIsNewId; } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/AlgorithmNames.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/AlgorithmNames.java new file mode 100644 index 000000000..d2f4cc003 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/AlgorithmNames.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.util; + +import android.annotation.SuppressLint; +import android.app.Activity; +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; + +import java.util.HashMap; + +@SuppressLint("UseSparseArrays") +public class AlgorithmNames { + Activity mActivity; + + HashMap mEncryptionNames = new HashMap(); + HashMap mHashNames = new HashMap(); + HashMap mCompressionNames = new HashMap(); + + public AlgorithmNames(Activity context) { + super(); + this.mActivity = context; + + mEncryptionNames.put(PGPEncryptedData.AES_128, "AES-128"); + mEncryptionNames.put(PGPEncryptedData.AES_192, "AES-192"); + mEncryptionNames.put(PGPEncryptedData.AES_256, "AES-256"); + mEncryptionNames.put(PGPEncryptedData.BLOWFISH, "Blowfish"); + mEncryptionNames.put(PGPEncryptedData.TWOFISH, "Twofish"); + mEncryptionNames.put(PGPEncryptedData.CAST5, "CAST5"); + mEncryptionNames.put(PGPEncryptedData.DES, "DES"); + mEncryptionNames.put(PGPEncryptedData.TRIPLE_DES, "Triple DES"); + mEncryptionNames.put(PGPEncryptedData.IDEA, "IDEA"); + + mHashNames.put(HashAlgorithmTags.MD5, "MD5"); + mHashNames.put(HashAlgorithmTags.RIPEMD160, "RIPEMD-160"); + mHashNames.put(HashAlgorithmTags.SHA1, "SHA-1"); + mHashNames.put(HashAlgorithmTags.SHA224, "SHA-224"); + mHashNames.put(HashAlgorithmTags.SHA256, "SHA-256"); + mHashNames.put(HashAlgorithmTags.SHA384, "SHA-384"); + mHashNames.put(HashAlgorithmTags.SHA512, "SHA-512"); + + mCompressionNames.put(Id.choice.compression.none, mActivity.getString(R.string.choice_none) + + " (" + mActivity.getString(R.string.compression_fast) + ")"); + mCompressionNames.put(Id.choice.compression.zip, + "ZIP (" + mActivity.getString(R.string.compression_fast) + ")"); + mCompressionNames.put(Id.choice.compression.zlib, + "ZLIB (" + mActivity.getString(R.string.compression_fast) + ")"); + mCompressionNames.put(Id.choice.compression.bzip2, + "BZIP2 (" + mActivity.getString(R.string.compression_very_slow) + ")"); + } + + public HashMap getEncryptionNames() { + return mEncryptionNames; + } + + public void setEncryptionNames(HashMap encryptionNames) { + this.mEncryptionNames = encryptionNames; + } + + public HashMap getHashNames() { + return mHashNames; + } + + public void setHashNames(HashMap hashNames) { + this.mHashNames = hashNames; + } + + public HashMap getCompressionNames() { + return mCompressionNames; + } + + public void setCompressionNames(HashMap compressionNames) { + this.mCompressionNames = compressionNames; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Choice.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Choice.java new file mode 100644 index 000000000..1a6184d9c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Choice.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/HkpKeyServer.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/HkpKeyServer.java new file mode 100644 index 000000000..5efc732e4 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/HkpKeyServer.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2011 Thialfihar + * 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 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.Constants; +import org.sufficientlysecure.keychain.pgp.PgpHelper; +import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry; + +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.URLDecoder; +import java.net.URLEncoder; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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; + + /** + * pub:%keyid%:%algo%:%keylen%:%creationdate%:%expirationdate%:%flags% + *

    + *
  • %keyid% = this is either the fingerprint or the key ID of the key. + * Either the 16-digit or 8-digit key IDs are acceptable, but obviously the fingerprint is best. + *
  • + *
  • %algo% = the algorithm number, (i.e. 1==RSA, 17==DSA, etc). + * See RFC-2440
  • + *
  • %keylen% = the key length (i.e. 1024, 2048, 4096, etc.)
  • + *
  • %creationdate% = creation date of the key in standard + * RFC-2440 form (i.e. number of + * seconds since 1/1/1970 UTC time)
  • + *
  • %expirationdate% = expiration date of the key in standard + * RFC-2440 form (i.e. number of + * seconds since 1/1/1970 UTC time)
  • + *
  • %flags% = letter codes to indicate details of the key, if any. Flags may be in any + * order. The meaning of "disabled" is implementation-specific. Note that individual flags may + * be unimplemented, so the absence of a given flag does not necessarily mean the absence of the + * detail. + *
      + *
    • r == revoked
    • + *
    • d == disabled
    • + *
    • e == expired
    • + *
    + *
  • + *
+ * + * @see + * 5.2. Machine Readable Indexes + * in Internet-Draft OpenPGP HTTP Keyserver Protocol Document + */ + public static final Pattern PUB_KEY_LINE = Pattern + .compile("pub:([0-9a-fA-F]+):([0-9]+):([0-9]+):([0-9]+):([0-9]*):([rde]*)[ \n\r]*" // pub line + + "(uid:(.*):([0-9]+):([0-9]*):([rde]*))+", // one or more uid lines + Pattern.CASE_INSENSITIVE); + + /** + * uid:%escaped uid string%:%creationdate%:%expirationdate%:%flags% + *
    + *
  • %escaped uid string% = the user ID string, with HTTP %-escaping for anything that + * isn't 7-bit safe as well as for the ":" character. Any other characters may be escaped, as + * desired.
  • + *
  • %creationdate% = creation date of the key in standard + * RFC-2440 form (i.e. number of + * seconds since 1/1/1970 UTC time)
  • + *
  • %expirationdate% = expiration date of the key in standard + * RFC-2440 form (i.e. number of + * seconds since 1/1/1970 UTC time)
  • + *
  • %flags% = letter codes to indicate details of the key, if any. Flags may be in any + * order. The meaning of "disabled" is implementation-specific. Note that individual flags may + * be unimplemented, so the absence of a given flag does not necessarily mean the absence of + * the detail. + *
      + *
    • r == revoked
    • + *
    • d == disabled
    • + *
    • e == expired
    • + *
    + *
  • + *
+ */ + public static final Pattern UID_LINE = Pattern + .compile("uid:(.*):([0-9]+):([0-9]*):([rde]*)", + Pattern.CASE_INSENSITIVE); + + private static final short PORT_DEFAULT = 11371; + + /** + * @param hostAndPort may be just + * "hostname" (eg. "pool.sks-keyservers.net"), then it will + * connect using {@link #PORT_DEFAULT}. However, port may be specified after colon + * ("hostname:port", eg. "p80.pool.sks-keyservers.net:80"). + */ + public HkpKeyServer(String hostAndPort) { + String host = hostAndPort; + short port = PORT_DEFAULT; + final int colonPosition = hostAndPort.lastIndexOf(':'); + if (colonPosition > 0) { + host = hostAndPort.substring(0, colonPosition); + final String portStr = hostAndPort.substring(colonPosition + 1); + port = Short.decode(portStr); + } + mHost = host; + mPort = port; + } + + public HkpKeyServer(String host, short port) { + mHost = host; + mPort = port; + } + + private static 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; + Log.d(Constants.TAG, "hkp keyserver query: " + url); + 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 search(String query) throws QueryException, TooManyResponses, + InsufficientQuery { + ArrayList results = new ArrayList(); + + 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&options=mr&search=" + encodedQuery; + + String data; + try { + data = query(request); + } catch (HttpError e) { + if (e.getCode() == 404) { + return results; + } else { + if (e.getData().toLowerCase(Locale.US).contains("no keys found")) { + return results; + } else if (e.getData().toLowerCase(Locale.US).contains("too many")) { + throw new TooManyResponses(); + } else if (e.getData().toLowerCase(Locale.US).contains("insufficient")) { + throw new InsufficientQuery(); + } + } + throw new QueryException("querying server(s) for '" + mHost + "' failed"); + } + + final Matcher matcher = PUB_KEY_LINE.matcher(data); + while (matcher.find()) { + final ImportKeysListEntry entry = new ImportKeysListEntry(); + + entry.setBitStrength(Integer.parseInt(matcher.group(3))); + + final int algorithmId = Integer.decode(matcher.group(2)); + entry.setAlgorithm(ImportKeysListEntry.getAlgorithmFromId(algorithmId)); + + // group 1 contains the full fingerprint (v4) or the long key id if available + // see http://bit.ly/1d4bxbk and http://bit.ly/1gD1wwr + String fingerprintOrKeyId = matcher.group(1); + if (fingerprintOrKeyId.length() > 16) { + entry.setFingerPrintHex(fingerprintOrKeyId.toLowerCase(Locale.US)); + entry.setKeyIdHex("0x" + fingerprintOrKeyId.substring(fingerprintOrKeyId.length() + - 16, fingerprintOrKeyId.length())); + } else { + // set key id only + entry.setKeyIdHex("0x" + fingerprintOrKeyId); + } + + final long creationDate = Long.parseLong(matcher.group(4)); + final GregorianCalendar tmpGreg = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + tmpGreg.setTimeInMillis(creationDate * 1000); + entry.setDate(tmpGreg.getTime()); + + entry.setRevoked(matcher.group(6).contains("r")); + + ArrayList userIds = new ArrayList(); + final String uidLines = matcher.group(7); + final Matcher uidMatcher = UID_LINE.matcher(uidLines); + while (uidMatcher.find()) { + String tmp = uidMatcher.group(1).trim(); + if (tmp.contains("%")) { + try { + // converts Strings like "Universit%C3%A4t" to a proper encoding form "Universität". + tmp = (URLDecoder.decode(tmp, "UTF8")); + } catch (UnsupportedEncodingException ignored) { + // will never happen, because "UTF8" is supported + } + } + userIds.add(tmp); + } + entry.setUserIds(userIds); + + results.add(entry); + } + return results; + } + + @Override + public String get(String keyIdHex) throws QueryException { + HttpClient client = new DefaultHttpClient(); + try { + String query = "http://" + mHost + ":" + mPort + + "/pks/lookup?op=get&options=mr&search=" + keyIdHex; + Log.d(Constants.TAG, "hkp keyserver get: " + query); + HttpGet get = new HttpGet(query); + 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 = PgpHelper.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 armoredKey) throws AddKeyException { + HttpClient client = new DefaultHttpClient(); + try { + String query = "http://" + mHost + ":" + mPort + "/pks/add"; + HttpPost post = new HttpPost(query); + Log.d(Constants.TAG, "hkp keyserver add: " + query); + List nameValuePairs = new ArrayList(2); + nameValuePairs.add(new BasicNameValuePair("keytext", armoredKey)); + 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/InputData.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/InputData.java new file mode 100644 index 000000000..28cfa11f2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/InputData.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010-2014 Thialfihar + * + * 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IntentIntegratorSupportV4.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IntentIntegratorSupportV4.java new file mode 100644 index 000000000..ae87deb31 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IntentIntegratorSupportV4.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012 ZXing authors + * + * 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 com.google.zxing.integration.android.IntentIntegrator; + +import android.content.Intent; +import android.support.v4.app.Fragment; + +/** + * IntentIntegrator for the V4 Android compatibility package. + * + * @author Lachezar Dobrev + */ +public final class IntentIntegratorSupportV4 extends IntentIntegrator { + + private final Fragment mFragment; + + /** + * @param fragment Fragment to handle activity response. + */ + public IntentIntegratorSupportV4(Fragment fragment) { + super(fragment.getActivity()); + this.mFragment = fragment; + } + + @Override + protected void startActivityForResult(Intent intent, int code) { + mFragment.startActivityForResult(intent, code); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IterableIterator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IterableIterator.java new file mode 100644 index 000000000..3af674526 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/IterableIterator.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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.ArrayList; +import java.util.Iterator; + +public class IterableIterator implements Iterable { + private Iterator mIter; + + public IterableIterator(Iterator iter, boolean failsafe) { + mIter = iter; + if (failsafe && mIter == null) { + // is there a better way? + mIter = new ArrayList().iterator(); + } + } + public IterableIterator(Iterator iter) { + this(iter, false); + } + + public Iterator iterator() { + return mIter; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyServer.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyServer.java new file mode 100644 index 000000000..7f70867a5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeyServer.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2011 Thialfihar + * 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 org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry; + +import java.util.List; + +public abstract class KeyServer { + public static class QueryException extends Exception { + private static final long serialVersionUID = 2703768928624654512L; + + public QueryException(String message) { + super(message); + } + } + + public static class TooManyResponses extends Exception { + private static final long serialVersionUID = 2703768928624654513L; + } + + public static class InsufficientQuery extends Exception { + private static final long serialVersionUID = 2703768928624654514L; + } + + public static class AddKeyException extends Exception { + private static final long serialVersionUID = -507574859137295530L; + } + + abstract List search(String query) throws QueryException, TooManyResponses, + InsufficientQuery; + + abstract String get(String keyIdHex) throws QueryException; + + abstract void add(String armoredKey) throws AddKeyException; +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeychainServiceListener.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeychainServiceListener.java new file mode 100644 index 000000000..b205bd556 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeychainServiceListener.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.util; + +public interface KeychainServiceListener { + boolean hasServiceStopped(); +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Log.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Log.java new file mode 100644 index 000000000..f58f1757a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Log.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012-2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PRNGFixes.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PRNGFixes.java new file mode 100644 index 000000000..2d8fbcd81 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PRNGFixes.java @@ -0,0 +1,352 @@ +/* + * This software is provided 'as-is', without any express or implied + * warranty. In no event will Google be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, as long as the origin is not misrepresented. + */ + +package org.sufficientlysecure.keychain.util; + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import java.io.*; +import java.security.*; + +/** + * Fixes for the output of the default PRNG having low entropy. + *

+ * The fixes need to be applied via {@link #apply()} before any use of Java Cryptography + * Architecture primitives. A good place to invoke them is in the application's {@code onCreate}. + *

+ * copied from http://android-developers.blogspot.de/2013/08/some-securerandom-thoughts.html + *

+ *

+ * More information on these Android bugs: + * http://blog.k3170makan.com/2013/08/more-details-on-android-jca-prng-flaw.html + * Paper: "Randomly failed! Weaknesses in Java Pseudo Random Number Generators (PRNGs)" + *

+ *

+ * Sep 15, 2013: + * On some devices /dev/urandom is non-writable! + * No need to seed /dev/urandom. urandom should have enough seeds from the OS and kernel. + * Only OpenSSL seeds are broken. See http://emboss.github.io/blog/2013/08/21/openssl-prng-is-not-really-fork-safe + *

+ * see also: + * https://github.com/k9mail/k-9/commit/dda8f64276d4d29c43f86237cd77819c28f22f21 + * In addition to a couple of custom ROMs linking /dev/urandom to a non-writable + * random version, now Samsung's SELinux policy also prevents apps from opening + * /dev/urandom for writing. Since we shouldn't need to write to /dev/urandom anyway + * we now simply don't. + *

+ *

+ * Sep 17, 2013: + * Updated from official blogpost: + * Update: the original code sample below crashed on a small fraction of Android + * devices due to /dev/urandom not being writable. We have now updated the code sample to handle this case gracefully. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = + getBuildFingerprintAndDeviceSerial(); + + /** + * Hidden constructor to prevent instantiation. + */ + private PRNGFixes() { + } + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = + Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals( + secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng1.getProvider().getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", + 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PausableThreadPoolExecutor.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PausableThreadPoolExecutor.java new file mode 100644 index 000000000..5ec915810 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PausableThreadPoolExecutor.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.util; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Example from + * http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/ThreadPoolExecutor.html + */ +public class PausableThreadPoolExecutor extends ThreadPoolExecutor { + + public PausableThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue, + RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); + } + + public PausableThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + } + + public PausableThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue, + ThreadFactory threadFactory) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); + } + + public PausableThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + } + + private boolean mIsPaused; + private ReentrantLock mPauseLock = new ReentrantLock(); + private Condition mUnPaused = mPauseLock.newCondition(); + + protected void beforeExecute(Thread t, Runnable r) { + super.beforeExecute(t, r); + mPauseLock.lock(); + try { + while (mIsPaused) { + mUnPaused.await(); + } + } catch (InterruptedException ie) { + t.interrupt(); + } finally { + mPauseLock.unlock(); + } + } + + public void pause() { + mPauseLock.lock(); + try { + mIsPaused = true; + } finally { + mPauseLock.unlock(); + } + } + + public void resume() { + mPauseLock.lock(); + try { + mIsPaused = false; + mUnPaused.signalAll(); + } finally { + mPauseLock.unlock(); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java new file mode 100644 index 000000000..4fcd3047f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2010-2014 Thialfihar + * + * 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Primes.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Primes.java new file mode 100644 index 000000000..28a12bf37 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Primes.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java new file mode 100644 index 000000000..26c05ad0a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2010 Thialfihar + * + * 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); +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressScaler.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressScaler.java new file mode 100644 index 000000000..23961c05f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressScaler.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.util; + +/** This is a simple class that wraps a ProgressDialogUpdater, scaling the progress + * values into a specified range. + */ +public class ProgressScaler implements ProgressDialogUpdater { + + final ProgressDialogUpdater mWrapped; + final int mFrom, mTo, mMax; + + public ProgressScaler(ProgressDialogUpdater wrapped, int from, int to, int max) { + this.mWrapped = wrapped; + this.mFrom = from; + this.mTo = to; + this.mMax = max; + } + + /** + * Set progressDialogUpdater of ProgressDialog by sending message to handler on UI thread + */ + public void setProgress(String message, int progress, int max) { + mWrapped.setProgress(message, mFrom + progress * (mTo - mFrom) / max, mMax); + } + + public void setProgress(int resourceId, int progress, int max) { + mWrapped.setProgress(resourceId, progress, mMax); + } + + public void setProgress(int progress, int max) { + mWrapped.setProgress(progress, max); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/QrCodeUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/QrCodeUtils.java new file mode 100644 index 000000000..af9034aa7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/QrCodeUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * 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 . + */ + +package org.sufficientlysecure.keychain.util; + +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 android.graphics.Bitmap; +import android.graphics.Color; + +import org.sufficientlysecure.keychain.Constants; + +import java.util.Hashtable; + +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 hints = new Hashtable(); + 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; + } + } + +} -- cgit v1.2.3