aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDominik Schürmann <dominik@dominikschuermann.de>2016-05-07 12:01:16 +0300
committerDominik Schürmann <dominik@dominikschuermann.de>2016-05-07 12:01:16 +0300
commit7dd5e2235339401b44eda13b124f3482472539d4 (patch)
treed7f1e6ad18a258e6467a75731ab44968fe005c9a
parenta2dcb579ff5d3565e7e6c6afe37878855361595b (diff)
parentd4612b5e173455a24adbae2bfd4654ae065556cc (diff)
downloadopen-keychain-7dd5e2235339401b44eda13b124f3482472539d4.zip
open-keychain-7dd5e2235339401b44eda13b124f3482472539d4.tar.gz
open-keychain-7dd5e2235339401b44eda13b124f3482472539d4.tar.bz2
Merge branch 'master' into backup-api
Conflicts: OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java extern/openpgp-api-lib
-rw-r--r--.gitignore21
-rw-r--r--.travis.yml2
-rw-r--r--.tx/config2
-rw-r--r--OpenKeychain/build.gradle181
-rw-r--r--OpenKeychain/proguard-rules.pro41
-rw-r--r--OpenKeychain/src/main/AndroidManifest.xml85
-rw-r--r--OpenKeychain/src/main/java/org/bouncycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java11
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java1
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/FacebookKeyserver.java25
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java55
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java63
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java10
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java34
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java58
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ChangeUnlockOperation.java108
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java3
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java33
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java22
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpCertifyOperation.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java80
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java178
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java12
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java38
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java42
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java261
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java31
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java52
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java21
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java220
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/SimpleContentResolverInterface.java39
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java13
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/receiver/NetworkReceiver.java52
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java10
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java75
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java284
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java194
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java4
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java10
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteCreateAccountActivity.java11
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteRegisterActivity.java8
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdListFragment.java9
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java34
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java65
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java113
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java756
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java58
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java304
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java37
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ChangeUnlockParcel.java88
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java8
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java3
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java55
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java75
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java12
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java32
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java201
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java3
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java180
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java64
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyEmailFragment.java35
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java56
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyNameFragment.java33
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java17
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java34
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java102
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java12
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java20
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java103
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java9
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java7
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MultiUserIdsFragment.java223
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java118
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/RedirectImportKeysActivity.java59
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java180
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java22
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java7
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java21
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UsbEventReceiverActivity.java59
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java61
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java13
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java82
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java1
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java13
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java7
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAddedAdapter.java48
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java13
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java513
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java1057
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java6
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java17
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java473
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyDialogFragment.java37
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java2
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/spinner/FocusFirstItemSpinner.java77
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EmailEditText.java19
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java24
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java5
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Choice.java9
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java27
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpClientFactory.java76
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpKeybaseClient.java51
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java55
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java34
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/UsbConnectionDispatcher.java97
-rw-r--r--OpenKeychain/src/main/res/drawable-hdpi/ic_change_grey_24dp.pngbin0 -> 923 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-mdpi/ic_change_grey_24dp.pngbin0 -> 623 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-xhdpi/ic_change_grey_24dp.pngbin0 -> 1066 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-xxhdpi/ic_change_grey_24dp.pngbin0 -> 1612 bytes
-rw-r--r--OpenKeychain/src/main/res/drawable-xxxhdpi/ic_change_grey_24dp.pngbin0 -> 2223 bytes
-rw-r--r--OpenKeychain/src/main/res/layout-land/backup_code_fragment.xml199
-rw-r--r--OpenKeychain/src/main/res/layout-w400dp/backup_code_fragment.xml459
-rw-r--r--OpenKeychain/src/main/res/layout-w400dp/passphrase_dialog_backup_code.xml201
-rw-r--r--OpenKeychain/src/main/res/layout-w600dp/backup_code_fragment.xml459
-rw-r--r--OpenKeychain/src/main/res/layout-w600dp/security_token_operation_activity.xml (renamed from OpenKeychain/src/main/res/layout-land/security_token_operation_activity.xml)0
-rw-r--r--OpenKeychain/src/main/res/layout/add_subkey_dialog.xml163
-rw-r--r--OpenKeychain/src/main/res/layout/backup_code_fragment.xml346
-rw-r--r--OpenKeychain/src/main/res/layout/certify_item.xml2
-rw-r--r--OpenKeychain/src/main/res/layout/certify_key_fragment.xml5
-rw-r--r--OpenKeychain/src/main/res/layout/create_key_final_fragment.xml6
-rw-r--r--OpenKeychain/src/main/res/layout/encrypt_asymmetric_fragment.xml10
-rw-r--r--OpenKeychain/src/main/res/layout/multi_user_ids_fragment.xml6
-rw-r--r--OpenKeychain/src/main/res/layout/passphrase_dialog_backup_code.xml175
-rw-r--r--OpenKeychain/src/main/res/layout/two_line_spinner_dropdown_item.xml32
-rw-r--r--OpenKeychain/src/main/res/layout/upload_key_activity.xml6
-rw-r--r--OpenKeychain/src/main/res/raw-cs/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-cs/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-de/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-de/help_changelog.md9
-rw-r--r--OpenKeychain/src/main/res/raw-es-rMX/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-es-rMX/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-es/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-es/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-eu/advanced.md14
-rw-r--r--OpenKeychain/src/main/res/raw-eu/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-eu/help_changelog.md13
-rw-r--r--OpenKeychain/src/main/res/raw-fa/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-fa/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-fi/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-fi/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-fr/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-fr/help_changelog.md9
-rw-r--r--OpenKeychain/src/main/res/raw-hi/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-hi/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-hu/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-hu/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-id/advanced.md12
-rw-r--r--OpenKeychain/src/main/res/raw-id/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-id/help_changelog.md77
-rw-r--r--OpenKeychain/src/main/res/raw-it/advanced.md14
-rw-r--r--OpenKeychain/src/main/res/raw-it/help_about.md6
-rw-r--r--OpenKeychain/src/main/res/raw-it/help_changelog.md31
-rw-r--r--OpenKeychain/src/main/res/raw-ja/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-ja/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-kn/advanced.md9
-rw-r--r--OpenKeychain/src/main/res/raw-kn/help_about.md72
-rw-r--r--OpenKeychain/src/main/res/raw-kn/help_certification.md28
-rw-r--r--OpenKeychain/src/main/res/raw-kn/help_changelog.md326
-rw-r--r--OpenKeychain/src/main/res/raw-kn/help_start.md16
-rw-r--r--OpenKeychain/src/main/res/raw-ko/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-ko/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-la/advanced.md9
-rw-r--r--OpenKeychain/src/main/res/raw-la/help_about.md72
-rw-r--r--OpenKeychain/src/main/res/raw-la/help_certification.md28
-rw-r--r--OpenKeychain/src/main/res/raw-la/help_changelog.md326
-rw-r--r--OpenKeychain/src/main/res/raw-la/help_start.md16
-rw-r--r--OpenKeychain/src/main/res/raw-nb/advanced.md9
-rw-r--r--OpenKeychain/src/main/res/raw-nb/help_about.md72
-rw-r--r--OpenKeychain/src/main/res/raw-nb/help_certification.md28
-rw-r--r--OpenKeychain/src/main/res/raw-nb/help_changelog.md326
-rw-r--r--OpenKeychain/src/main/res/raw-nb/help_start.md16
-rw-r--r--OpenKeychain/src/main/res/raw-nl/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-nl/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-pl/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-pl/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-pt-rBR/advanced.md9
-rw-r--r--OpenKeychain/src/main/res/raw-pt-rBR/help_about.md72
-rw-r--r--OpenKeychain/src/main/res/raw-pt-rBR/help_certification.md28
-rw-r--r--OpenKeychain/src/main/res/raw-pt-rBR/help_changelog.md326
-rw-r--r--OpenKeychain/src/main/res/raw-pt-rBR/help_start.md16
-rw-r--r--OpenKeychain/src/main/res/raw-ru/advanced.md12
-rw-r--r--OpenKeychain/src/main/res/raw-ru/help_about.md8
-rw-r--r--OpenKeychain/src/main/res/raw-ru/help_certification.md12
-rw-r--r--OpenKeychain/src/main/res/raw-ru/help_changelog.md17
-rw-r--r--OpenKeychain/src/main/res/raw-ru/help_start.md2
-rw-r--r--OpenKeychain/src/main/res/raw-sl/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-sl/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-sr/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-sr/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-sv/advanced.md2
-rw-r--r--OpenKeychain/src/main/res/raw-sv/help_about.md4
-rw-r--r--OpenKeychain/src/main/res/raw-sv/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-tr/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-tr/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-uk/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-uk/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-vi/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-vi/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-zh-rTW/advanced.md14
-rw-r--r--OpenKeychain/src/main/res/raw-zh-rTW/help_about.md2
-rw-r--r--OpenKeychain/src/main/res/raw-zh-rTW/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw-zh/advanced.md14
-rw-r--r--OpenKeychain/src/main/res/raw-zh/help_about.md10
-rw-r--r--OpenKeychain/src/main/res/raw-zh/help_certification.md32
-rw-r--r--OpenKeychain/src/main/res/raw-zh/help_changelog.md23
-rw-r--r--OpenKeychain/src/main/res/raw-zh/help_start.md18
-rw-r--r--OpenKeychain/src/main/res/raw/help_changelog.md7
-rw-r--r--OpenKeychain/src/main/res/raw/help_faq.md2
-rw-r--r--OpenKeychain/src/main/res/values-cs/strings.xml335
-rw-r--r--OpenKeychain/src/main/res/values-de/strings.xml484
-rw-r--r--OpenKeychain/src/main/res/values-es-rMX/strings.xml11
-rw-r--r--OpenKeychain/src/main/res/values-es/strings.xml24
-rw-r--r--OpenKeychain/src/main/res/values-eu/strings.xml130
-rw-r--r--OpenKeychain/src/main/res/values-fa/strings.xml6
-rw-r--r--OpenKeychain/src/main/res/values-fi/strings.xml10
-rw-r--r--OpenKeychain/src/main/res/values-fr/strings.xml91
-rw-r--r--OpenKeychain/src/main/res/values-hi/strings.xml18
-rw-r--r--OpenKeychain/src/main/res/values-hu/strings.xml1
-rw-r--r--OpenKeychain/src/main/res/values-id/strings.xml2
-rw-r--r--OpenKeychain/src/main/res/values-it/strings.xml145
-rw-r--r--OpenKeychain/src/main/res/values-ja/strings.xml72
-rw-r--r--OpenKeychain/src/main/res/values-kn/strings.xml87
-rw-r--r--OpenKeychain/src/main/res/values-ko/strings.xml11
-rw-r--r--OpenKeychain/src/main/res/values-la/strings.xml87
-rw-r--r--OpenKeychain/src/main/res/values-nb/strings.xml203
-rw-r--r--OpenKeychain/src/main/res/values-nl/strings.xml22
-rw-r--r--OpenKeychain/src/main/res/values-pl/strings.xml17
-rw-r--r--OpenKeychain/src/main/res/values-pt-rBR/strings.xml493
-rw-r--r--OpenKeychain/src/main/res/values-ru/strings.xml782
-rw-r--r--OpenKeychain/src/main/res/values-sl/strings.xml17
-rw-r--r--OpenKeychain/src/main/res/values-sr/strings.xml53
-rw-r--r--OpenKeychain/src/main/res/values-sv/strings.xml59
-rw-r--r--OpenKeychain/src/main/res/values-sw360dp/styles.xml9
-rw-r--r--OpenKeychain/src/main/res/values-sw400dp/styles.xml9
-rw-r--r--OpenKeychain/src/main/res/values-tr/strings.xml12
-rw-r--r--OpenKeychain/src/main/res/values-uk/strings.xml16
-rw-r--r--OpenKeychain/src/main/res/values-vi/strings.xml1
-rw-r--r--OpenKeychain/src/main/res/values-zh-rTW/strings.xml15
-rw-r--r--OpenKeychain/src/main/res/values-zh/strings.xml189
-rw-r--r--OpenKeychain/src/main/res/values/colors.xml1
-rw-r--r--OpenKeychain/src/main/res/values/strings.xml79
-rw-r--r--OpenKeychain/src/main/res/values/styles.xml7
-rw-r--r--OpenKeychain/src/main/res/xml/sync_preferences.xml6
-rw-r--r--OpenKeychain/src/main/res/xml/usb_device_filter.xml27
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/BenchmarkOperationTest.java1
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java6
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java11
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java4
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java6
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java77
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/SplitUserIdTest.java (renamed from OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/KeyRingTest.java)37
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java4
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java6
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringTest.java4
-rw-r--r--README.md7
-rw-r--r--build.gradle15
m---------extern/KeybaseLib0
m---------extern/openpgp-api-lib0
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin53323 -> 53639 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew10
-rw-r--r--gradlew.bat2
-rw-r--r--settings.gradle18
272 files changed, 12274 insertions, 4349 deletions
diff --git a/.gitignore b/.gitignore
index 68f5b5a..c7c85bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,37 +1,28 @@
-#Android specific
-bin
-gen
-obj
-libs/armeabi
+# Android specific
lint.xml
local.properties
release.properties
-ant.properties
*.class
*.apk
-#Gradle
+# Gradle
.gradle
build
-gradle.properties
-# this is in here because the prepare-tests thing modifies it, and we DON'T
-# want this to be commited. use git add -f to work on this file.
-settings.gradle
-#Maven
+# Maven
target
pom.xml.*
-#Eclipse
+# Eclipse
.project
.classpath
.settings
.metadata
-#IntelliJ IDEA
+# IntelliJ IDEA
.idea
*.iml
-#OS Specific
+# OS Specific
[Tt]humbs.db
.DS_Store
diff --git a/.travis.yml b/.travis.yml
index 402bfa4..9d87e5e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,6 +14,8 @@ addons:
hostname: short-hostname
android:
components:
+ - tools
+ - build-tools-23.0.2
- build-tools-23.0.1
- android-23
- android-22
diff --git a/.tx/config b/.tx/config
index 4ea2ec2..de1ec1e 100644
--- a/.tx/config
+++ b/.tx/config
@@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
-lang_map = he: iw, zh_TW: zh-rTW, es_MX: es-rMX
+lang_map = he: iw, zh_TW: zh-rTW, es_MX: es-rMX, pt_BR: pt-rBR
[open-keychain.strings]
file_filter = OpenKeychain/src/main/res/values-<lang>/strings.xml
diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle
index 01e221a..4d1b572 100644
--- a/OpenKeychain/build.gradle
+++ b/OpenKeychain/build.gradle
@@ -8,128 +8,127 @@ dependencies {
// NOTE: libraries are pinned to a specific build, see below
// from local Android SDK
- compile 'com.android.support:support-v4:23.2.0'
- compile 'com.android.support:appcompat-v7:23.2.0'
- compile 'com.android.support:design:23.2.0'
- compile 'com.android.support:recyclerview-v7:23.2.0'
- compile 'com.android.support:cardview-v7:23.2.0'
-
- // Unit tests in the local JVM with Robolectric
- // https://developer.android.com/training/testing/unit-testing/local-unit-tests.html
- // http://robolectric.org/getting-started/
- // http://www.vogella.com/tutorials/Robolectric/article.html
- testCompile 'junit:junit:4.12'
- testCompile ('org.robolectric:robolectric:3.0') {
- exclude group: 'org.bouncycastle', module: 'bcprov-jdk16'
- }
- testCompile 'org.mockito:mockito-core:1.10.19'
-
- // UI testing with Espresso
- androidTestCompile 'com.android.support.test:runner:0.4.1'
- androidTestCompile 'com.android.support.test:rules:0.4.1'
- androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
- androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1'
- androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.2.1') {
- exclude group: 'com.android.support', module: 'appcompat'
- exclude group: 'com.android.support', module: 'support-v4'
- exclude module: 'recyclerview-v7'
- }
-
- // Temporary workaround for bug: https://code.google.com/p/android-test-kit/issues/detail?id=136
- // from https://github.com/googlesamples/android-testing/blob/master/build.gradle#L21
- configurations.all {
- resolutionStrategy.force 'com.android.support:support-annotations:23.2.0'
- }
+ compile 'com.android.support:support-v4:23.3.0'
+ compile 'com.android.support:appcompat-v7:23.3.0'
+ compile 'com.android.support:design:23.3.0'
+ compile 'com.android.support:recyclerview-v7:23.3.0'
+ compile 'com.android.support:cardview-v7:23.3.0'
+ compile 'com.android.support:support-annotations:23.3.0'
// JCenter etc.
- compile 'com.eftimoff:android-patternview:1.0.5@aar'
- compile 'com.journeyapps:zxing-android-embedded:3.1.0@aar'
+ compile 'com.journeyapps:zxing-android-embedded:3.2.0@aar'
compile 'com.google.zxing:core:3.2.1'
compile 'com.jpardogo.materialtabstrip:library:1.1.0'
compile 'com.getbase:floatingactionbutton:1.10.1'
compile 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.0'
compile 'org.ocpsoft.prettytime:prettytime:4.0.1.Final'
- compile 'com.splitwise:tokenautocomplete:2.0.2@aar'
- compile 'com.github.pinball83:masked-edittext:1.0.3'
+ compile 'com.splitwise:tokenautocomplete:2.0.7@aar'
compile 'se.emilsjolander:stickylistheaders:2.7.0'
- compile 'org.sufficientlysecure:html-textview:1.3'
+ compile 'org.sufficientlysecure:html-textview:1.5'
compile 'org.sufficientlysecure:donations:2.4'
compile 'com.nispok:snackbar:2.11.0'
- compile 'com.squareup.okhttp:okhttp:2.7.1'
- compile 'com.squareup.okhttp:okhttp-urlconnection:2.7.1'
+ compile 'com.squareup.okhttp3:okhttp:3.2.0'
+ compile 'com.squareup.okhttp3:okhttp-urlconnection:3.2.0'
compile 'org.apache.james:apache-mime4j-core:0.7.2'
compile 'org.apache.james:apache-mime4j-dom:0.7.2'
compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
compile 'com.cocosw:bottomsheet:1.3.0@aar'
// Material Drawer
- compile 'com.mikepenz:materialdrawer:4.6.3@aar'
- compile 'com.mikepenz:materialize:0.5.1'
- compile 'com.mikepenz:iconics-core:2.5.3@aar'
- compile 'com.mikepenz:google-material-typeface:2.2.0.1@aar'
- compile 'com.mikepenz:fontawesome-typeface:4.5.0.1@aar'
- compile 'com.mikepenz:community-material-typeface:1.3.41.1@aar'
+ compile 'com.mikepenz:materialdrawer:5.2.2@aar'
+ compile 'com.mikepenz:fastadapter:1.4.4'
+ compile 'com.mikepenz:materialize:0.8.8'
+ compile 'com.mikepenz:iconics-core:2.5.11@aar'
+ compile 'com.mikepenz:google-material-typeface:2.2.0.1.original@aar'
+ compile 'com.mikepenz:fontawesome-typeface:4.6.0.1@aar'
+ compile 'com.mikepenz:community-material-typeface:1.5.54.1@aar'
// Nordpol
compile 'com.fidesmo:nordpol-android:0.1.18'
// libs as submodules
- compile project(':extern:openpgp-api-lib:openpgp-api')
- compile project(':extern:openkeychain-api-lib:openkeychain-intents')
+ compile project(':openpgp-api-lib')
+ compile project(':openkeychain-api-lib')
compile project(':extern:bouncycastle:core')
compile project(':extern:bouncycastle:pg')
compile project(':extern:bouncycastle:prov')
compile project(':extern:minidns')
- compile project(':extern:KeybaseLib:Lib')
- compile project(':extern:safeslinger-exchange:safeslinger-exchange')
+ compile project(':KeybaseLib')
+ compile project(':safeslinger-exchange')
+
+ // Unit tests in the local JVM with Robolectric
+ // https://developer.android.com/training/testing/unit-testing/local-unit-tests.html
+ // http://robolectric.org/getting-started/
+ // http://www.vogella.com/tutorials/Robolectric/article.html
+ testCompile 'junit:junit:4.12'
+ testCompile ('org.robolectric:robolectric:3.0') {
+ exclude group: 'org.bouncycastle', module: 'bcprov-jdk16'
+ }
+ testCompile 'org.mockito:mockito-core:1.10.19'
+
+ // UI testing with Espresso
+ // Force usage of support libs in the test app, since they are internally used by the runner module.
+ // https://github.com/googlesamples/android-testing/blob/master/ui/espresso/BasicSample/app/build.gradle#L28
+ androidTestCompile 'com.android.support:support-annotations:23.3.0'
+ androidTestCompile 'com.android.support:appcompat-v7:23.3.0'
+ androidTestCompile 'com.android.support:design:23.3.0'
+ androidTestCompile 'com.android.support.test:runner:0.5'
+ androidTestCompile 'com.android.support.test:rules:0.5'
+ androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
+ androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.2'
+ androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.2.2') {
+ exclude group: 'com.android.support', module: 'appcompat'
+ exclude group: 'com.android.support', module: 'support-v4'
+ exclude module: 'recyclerview-v7'
+ }
+
}
// Output of ./gradlew -q calculateChecksums
// Comment out the libs referenced as git submodules!
dependencyVerification {
verify = [
- 'com.android.support:support-v4:992666398b80724a2f95ea3d7bf7c3d94fb810dcba7ae1aa6e38c0d6120d2603',
- 'com.android.support:appcompat-v7:14ab04eb2e3f302a082b79c308f6283d21909d1feb831a4e117cdacdad70adb7',
- 'com.android.support:design:e7f2a383ba675cf65d834f27079630a1e373f2f3a08330bec6a7c798ef50b3b2',
- 'com.android.support:recyclerview-v7:81aad9ff4104a20d8d5cda49dbbe0f2925e7dcd607a689374db843c6f2eca20a',
- 'com.android.support:cardview-v7:b2c2c070a78fbf7683ab4d84b23f9eecf2af3848f126775d048ae62238b55aed',
- 'com.eftimoff:android-patternview:594dde382fb9a445ef0c92d614f6f127727ce699f124de8167929e10f298bf8b',
- 'com.journeyapps:zxing-android-embedded:90840a4457e68962fdfb74f691c6a736be7596291001045241901f1f0e6db2ac',
+ 'com.android.support:support-v4:1e8b7cc1cb3d6f6a2fd913791a6313df6bbaa470be450384474e906bc234bd49',
+ 'com.android.support:appcompat-v7:dce81c41f76d83fa315617f4bc8ef2f84c5aa54b686f37b559422b939f622490',
+ 'com.android.support:design:1023c9d1ca3ecae3e0c54b7678cd275032fe5720bbb4f58e3c8b2f2a595d0051',
+ 'com.android.support:recyclerview-v7:32b98ca177d9352b19a92af80ff9a3c3589b50232b6722d2bce5514abe90be51',
+ 'com.android.support:cardview-v7:2f592da4dd1db85dab99653474dd139135aa3ca7596dfdbcfef714e0339b603a',
+ 'com.android.support:support-annotations:e9e076f3ea4fb144387c6054a6f69a2f6150ad4b1907897aaf55d6e8f4b8b91e',
+ 'com.journeyapps:zxing-android-embedded:afe4cd51d95ba0fd3a4bfe08c5a160bd32602aa174d511600ac824b6de4c79f1',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'com.jpardogo.materialtabstrip:library:24d19232b319f8c73e25793432357919a7ed972186f57a3b2c9093ea74ad8311',
'com.getbase:floatingactionbutton:3edefa511aac4d90794c7b0496aca59cff2eee1e32679247b4f85acbeee05240',
'org.commonjava.googlecode.markdown4j:markdown4j:e952e825d29e1317d96f79f346bfb6786c7c5eef50bd26e54a80823704b62e13',
'org.ocpsoft.prettytime:prettytime:ef7098d973ae78b57d1a22dc37d3b8a771bf030301300e24055d676b6cdc5e75',
- 'com.splitwise:tokenautocomplete:2fc238424130b42155b5f2e39799a90bbbd13b148850afbe534ab08bb913c7f7',
- 'com.github.pinball83:masked-edittext:b1913d86482c7066ebb7831696773ac131865dc441cf8a3fc41d3b7d5691724e',
+ 'com.splitwise:tokenautocomplete:f56239588390f103b270b7c12361d99b06313a5a0410dc7f66e241ac4baf9baa',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
+ 'org.sufficientlysecure:html-textview:b3799eea5f440b32bdec18360e4ce66f6f77814c8df9d382718506a7da0de961',
'org.sufficientlysecure:donations:96f8197bab26dfe41900d824f10f8f1914519cd62eedb77bdac5b223eccdf0a6',
- 'org.sufficientlysecure:html-textview:39048e35894e582adada388e6c00631803283f8defed8e07ad58a5f284f272ee',
- 'com.squareup.okhttp:okhttp:8df336e3e93b22ba8c05da5d94caf968950db845869c7ca16ed682e065135aa8',
'com.nispok:snackbar:46b5eb9d630d329e13c2ce00ee9fb115ffb66c23c72cff32ee97eedd76824c6f',
+ 'com.squareup.okhttp3:okhttp:a41cdb7b024c56436a21e38f00b4d12e3b7e01451ffe6c4f545acba805bba03b',
+ 'com.squareup.okhttp3:okhttp-urlconnection:7d6598a6665c166e2d4b78956a96056b9be7de192b3c923ccf4695d08e580833',
'org.apache.james:apache-mime4j-core:4d7434c68f94b81a253c12f28e6bbb4d6239c361d6086a46e22e594bb43ac660',
- 'com.squareup.okhttp:okhttp-urlconnection:8dce03792fd7b5f089dc4fc0fdcecbbe50ae6cca21cb08a787a3b902a9914111',
- 'org.thoughtcrime.ssl.pinning:AndroidPinning:afa1d74e699257fa75cb109ff29bac50726ef269c6e306bdeffe8223cee06ef4',
'org.apache.james:apache-mime4j-dom:7e6b06ee164a1c21b7e477249ea0b74a18fddce44764e5764085f58dd8c34633',
- 'com.mikepenz:materialdrawer:4e2644f454cc2ce48b956536d3339957c3f592adb2e0b6dad72d477da29f7677',
+ 'org.thoughtcrime.ssl.pinning:AndroidPinning:afa1d74e699257fa75cb109ff29bac50726ef269c6e306bdeffe8223cee06ef4',
'com.cocosw:bottomsheet:4af6112a7f4cad4e2b70e5fdf1edc39f51275523a0f53011a012837dc103e597',
- 'com.mikepenz:iconics-core:d2495547db9d881168b1b502b1934f6a000ed5086c6c6a7114f3bbcbbb7ec306',
- 'com.mikepenz:materialize:2457dbe0b874a422c0a21bc6716cf5af1d5a8d39387857ff7c20855ab5543bf8',
- 'com.mikepenz:fontawesome-typeface:69cb09934a83bac607e78a29459868d537f766224b4a65a042d1f84c98c7b05d',
- 'com.mikepenz:google-material-typeface:48b2712de87d542e9b050846e9f602238a367f38e2d5e8ea4557c5b12adfcbec',
+ 'com.mikepenz:materialdrawer:4169462fdde042e2bb53a7c2b4e2334d569d16b2020781ee05741b50e1a2967d',
+ 'com.mikepenz:fastadapter:1bfc00216d71dfdfe0d8e7a9d92bb97bfaa1794543930e34b1f79d5d7adbddf6',
+ 'com.mikepenz:materialize:575195b2fa5b2414fb14a59470ee21d8a8cd8355b651e0cf52e477e3ff1cd96c',
+ 'com.mikepenz:iconics-core:d57c6b0ecb33d9ed9708da62e07ad8993d80a16f374f7a2018be7837a60b7ed7',
+ 'com.mikepenz:google-material-typeface:47eabb0aadcc0f56530c3ca462b671a5cf5251101cef8f581aedf90ca511914a',
+ 'com.mikepenz:fontawesome-typeface:033cf3460d8074bd37a1fefc2ff4eac8f2e3db835ec78bf386d46710e4d0827c',
+ 'com.mikepenz:community-material-typeface:382e8446fc08fe03cb1e0f91ee329ffd514c113ad22f8389b88424ac71ed5fbb',
'com.fidesmo:nordpol-android:56f43fe2b1676817bcb4085926de14a08282ef6729c855c198d81aec62b20d65',
- 'com.mikepenz:community-material-typeface:990acfcfb892a733d36748fe29176bd61dd5ab34bc8ca1c591200e639d955b99',
-// 'OpenKeychain.extern.bouncycastle:core:b22dfb37e09fb520683dd0ba089351787560a75b59b60822143f633ec984cab5',
-// 'OpenKeychain.extern.openpgp-api-lib:openpgp-api:fbd9a53022747bdc3c5b0926c6f9cbcbee19e766dd96f090a4310dc65026b393',
-// 'OpenKeychain.extern.openkeychain-api-lib:openkeychain-intents:9263330c00497b7bb70502160f50c8396228129376f48f4f5656d28360a2edac',
-// 'OpenKeychain.extern.bouncycastle:prov:2d93a52e1b519995b18c0a92a1e59a2773d67d9b466a9cce6af5202a66502577',
-// 'OpenKeychain.extern.bouncycastle:pg:1397025acf36be36d329c0345b136af776be82fe5d6dad70cc06db09d2f02201',
-// 'OpenKeychain.extern.safeslinger-exchange:safeslinger-exchange:989fcc0eba663489a41aa166f6bb39f21271d980faddea5f06ab75339e792d10',
- 'com.android.support:support-annotations:7f21659b084da073b77b6f7fe7ab250c4f23346238d4efdbbbb937e017ae4693',
-// 'OpenKeychain.extern:minidns:109d5851ab351d7628ed62a0ed96b40598952424e56657c17debbeb4a704f0ce',
-// 'OpenKeychain.extern.KeybaseLib:Lib:c5b1567ff781c311240e83f865c4ba76ae435eb00994529b8364371abf0d76de',
- 'com.android.support:animated-vector-drawable:4d8366192dedc8ca9e6ff62e9be9ba6145166554d61befc9df312e8328836f55',
- 'com.android.support:support-vector-drawable:0f43fc47b0d2797c4e1851aba61ab87a4fd33323348c2bd022821aaac052a266',
+// 'OpenKeychain:openpgp-api-lib:e4f456b77f80886eb2094bd643c8c8c9cf0eb3e8ef919706d7e92da6fc3c5517',
+// 'OpenKeychain:openkeychain-api-lib:cddb2953fc3ec2876923f01acbe91e136eaad1c327450a4fab21274b0edf73d9',
+// 'OpenKeychain.extern.bouncycastle:core:d4574e14e78d78b8b0b30b37089f82901a8fb46679915e034fe5d15cc5431c6a',
+// 'OpenKeychain.extern.bouncycastle:pg:064250718891c89d3bfa1e9bdef68bbf1cd2adb83532707754390b42643f2156',
+// 'OpenKeychain.extern.bouncycastle:prov:6933802a03ffd53c084415550db834a59540327434a061af39442e00002611f8',
+// 'OpenKeychain.extern:minidns:357901cf1b93f74f9729f6e3a1dfa5f148d627bf23423c8597ab82be84b3dc9d',
+// 'OpenKeychain:KeybaseLib:cbba456bf50083cde99f9c460fcbd812dbcd7d9e1a5c1df0ee9e080a85802aa8',
+// 'OpenKeychain:safeslinger-exchange:422efcad6868e32ce1446d18f08d728d266a30725fdc09a71f1b78c133bfc051',
+ 'com.android.support:support-vector-drawable:a4feae56880e385e147dfed3f73593ce61164ec49bb401f34de9a77a17c68a44',
+ 'com.android.support:animated-vector-drawable:ddf1f9dc18d38b21b01c21842d4bdf57613ea86398ede8f914f29b5770f4c32b',
'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266',
'com.fidesmo:nordpol-core:3de58e850a00bba5b4d3a604d1399bcd89f695ea191ec0b03a57222e18062d15',
]
@@ -141,14 +140,12 @@ android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
- // TODO: remove org.apache dependencies in LinkedTokenResource etc.
- useLibrary 'org.apache.http.legacy'
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
- versionCode 38200
- versionName "3.8.2"
+ versionCode 39600
+ versionName "3.9.6 beta"
applicationId "org.sufficientlysecure.keychain"
// the androidjunitrunner is broken regarding coverage, see here:
// https://code.google.com/p/android/issues/detail?id=170607
@@ -190,6 +187,12 @@ android {
}
debug {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+
+ // Enable code coverage (Jacoco)
+ testCoverageEnabled true
+
applicationIdSuffix ".debug"
// Reference them in the java files with e.g. BuildConfig.ACCOUNT_TYPE.
@@ -203,9 +206,6 @@ android {
// Github API
buildConfigField "String", "GITHUB_CLIENT_ID", "\"c942cd81844d94e7e41b\""
buildConfigField "String", "GITHUB_CLIENT_SECRET", "\"f1dd17e70a0614abbd9310b00a310e23c6c8edff\""
-
- // Enable code coverage (Jacoco)
- testCoverageEnabled true
}
}
@@ -267,10 +267,11 @@ android {
dexOptions {
incremental = true
- // Disable preDexing, causes com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000) on some systems
- preDexLibraries = false
+ preDexLibraries = true
+ // dexInProcess requires much RAM, which is not available on all dev systems
+ dexInProcess = false
jumboMode = true
- javaMaxHeapSize "2g"
+ javaMaxHeapSize "4g"
}
packagingOptions {
@@ -284,8 +285,6 @@ android {
}
}
-// apply plugin: 'spoon'
-
task jacocoTestReport(type:JacocoReport, dependsOn: "testFdroidDebugUnitTest") {
group = "Reporting"
description = "Generate Jacoco coverage reports"
diff --git a/OpenKeychain/proguard-rules.pro b/OpenKeychain/proguard-rules.pro
index e37fe5a..59e1384 100644
--- a/OpenKeychain/proguard-rules.pro
+++ b/OpenKeychain/proguard-rules.pro
@@ -1,28 +1,21 @@
-# Add project specific ProGuard rules here.
-# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.
-#
-# For more details, see
+# Documentation for ProGuard:
# http://developer.android.com/guide/developing/tools/proguard.html
+# http://proguard.sourceforge.net/
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-
-# Workaround for Samsung Android 4.2 bug
-# https://code.google.com/p/android/issues/detail?id=78377
-# https://code.google.com/p/android/issues/detail?id=78377#c188
-# https://code.google.com/p/android/issues/detail?id=78377#c302
--keepattributes **
--keep class !android.support.v7.view.menu.**,** {*;}
--dontpreverify
+#-dontshrink # shrinking enabled, see below
+#-dontobfuscate # obfuscation enabled for one class (see below)
-dontoptimize
--dontshrink
+-dontpreverify
+-keepattributes **
-dontwarn **
--dontnote ** \ No newline at end of file
+-dontnote **
+
+# Rules are defined as negation filters!
+# (! = negation filter, ** = all subpackages)
+# Keep everything (** {*;}) except...
+
+# * Obfuscate android.support.v7.view.menu.** to fix Samsung Android 4.2 bug
+# https://code.google.com/p/android/issues/detail?id=78377
+# * Remove unneeded Bouncy Castle packages to be under 64K limit
+# http://developer.android.com/tools/building/multidex.html
+-keep class !android.support.v7.view.menu.**,!org.bouncycastle.crypto.tls.**,!org.bouncycastle.pqc.**,!org.bouncycastle.x509.**,** {*;}
diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml
index e1cccef..bead69c 100644
--- a/OpenKeychain/src/main/AndroidManifest.xml
+++ b/OpenKeychain/src/main/AndroidManifest.xml
@@ -5,11 +5,6 @@
android:installLocation="auto">
<!--
- General remarks
- ===============
- - Last APG 1 version was 10900 (1.0.9 beta 00)
- - Keychain starting with versionCode 20000!
-
Association of file types to Keychain
=====================================
General remarks about file ending conventions:
@@ -48,6 +43,11 @@
android:name="android.hardware.screen.portrait"
android:required="false" />
+ <!-- For OTG tokens -->
+ <uses-feature
+ android:name="android.hardware.usb.host"
+ android:required="false" />
+
<!-- TemporaryStorageProvider should be writable by OpenKeychain only, thus signature-level permission -->
<permission
android:name="${applicationId}.WRITE_TEMPORARY_STORAGE"
@@ -81,6 +81,7 @@
<!-- other group (for free) -->
<uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
@@ -94,6 +95,15 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Keychain.Light">
+ <!-- broadcast receiver for Wi-Fi Connection -->
+ <receiver
+ android:name=".receiver.NetworkReceiver"
+ android:enabled="false"
+ android:exported="true" >
+ <intent-filter>
+ <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
+ </intent-filter>
+ </receiver>
<!-- singleTop for NFC dispatch, see SecurityTokenOperationActivity -->
<activity
android:name=".ui.MainActivity"
@@ -503,21 +513,6 @@
android:theme="@style/Theme.Keychain.Transparent"
android:windowSoftInputMode="stateHidden">
- <!-- VIEW with fingerprint scheme:
- Handle URIs with fingerprints when scanning directly from Barcode Scanner -->
- <intent-filter android:label="@string/intent_import_key">
- <action android:name="android.intent.action.VIEW" />
-
- <category android:name="android.intent.category.BROWSABLE" />
- <category android:name="android.intent.category.DEFAULT" />
-
- <!-- Android's scheme matcher is case-sensitive, so include most likely variations -->
- <data android:scheme="openpgp4fpr" />
- <data android:scheme="OPENPGP4FPR" />
- <data android:scheme="OpenPGP4FPR" />
- <data android:scheme="OpenPGP4Fpr" />
- <data android:scheme="OpenPGP4fpr" />
- </intent-filter>
<!-- IMPORT_KEY without mimeType to allow import with extras Bundle -->
<intent-filter android:label="@string/intent_import_key">
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_QR_CODE" />
@@ -730,11 +725,36 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
+
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.MainActivity" />
</activity>
<activity
+ android:name=".ui.RedirectImportKeysActivity"
+ android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
+ android:label="@string/title_activity_redirect_key"
+ android:theme="@style/Theme.Keychain.Transparent">
+
+ <!-- VIEW with fingerprint scheme:
+ Handle URIs with fingerprints when scanning directly from Barcode Scanner -->
+ <intent-filter android:label="@string/intent_import_key">
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.BROWSABLE" />
+ <category android:name="android.intent.category.DEFAULT" />
+
+ <!-- Android's scheme matcher is case-sensitive, so include most likely variations -->
+ <data android:scheme="openpgp4fpr" />
+ <data android:scheme="OPENPGP4FPR" />
+ <data android:scheme="OpenPGP4FPR" />
+ <data android:scheme="OpenPGP4Fpr" />
+ <data android:scheme="OpenPGP4fpr" />
+ </intent-filter>
+ </activity>
+
+
+ <activity
android:name=".ui.LogDisplayActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_log_display" />
@@ -806,7 +826,12 @@
android:exported="false"
android:label="@string/keyserver_sync_settings_title" />
- <!-- Internal classes of the remote APIs (not exported!) -->
+ <provider
+ android:name=".remote.KeychainExternalProvider"
+ android:authorities="${applicationId}.provider.exported"
+ android:exported="true" />
+
+ <!-- Internal classes of the remote APIs (not exported) -->
<activity
android:name=".remote.ui.RemoteCreateAccountActivity"
android:exported="false"
@@ -864,6 +889,24 @@
android:configChanges="keyboardHidden|keyboard"
android:label="@string/title_backup" />
+ <!-- Usb interceptor activity -->
+ <activity
+ android:name=".ui.UsbEventReceiverActivity"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.Keychain.Transparent"
+ android:noHistory="true"
+ android:excludeFromRecents="true"
+ android:taskAffinity="com.example.taskAffinityUsbEventReceiver"
+ android:process=":UsbEventReceiverActivityProcess"
+ android:exported="false">
+
+ <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
+ android:resource="@xml/usb_device_filter" />
+ <intent-filter>
+ <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
+ </intent-filter>
+ </activity>
+
<!-- DEPRECATED service,
using this service may lead to truncated data being returned to the caller -->
<service
diff --git a/OpenKeychain/src/main/java/org/bouncycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java b/OpenKeychain/src/main/java/org/bouncycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java
index 703af94..7679f84 100644
--- a/OpenKeychain/src/main/java/org/bouncycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java
+++ b/OpenKeychain/src/main/java/org/bouncycastle/openpgp/operator/jcajce/CachingDataDecryptorFactory.java
@@ -6,15 +6,16 @@
package org.bouncycastle.openpgp.operator.jcajce;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+
import org.bouncycastle.jcajce.util.NamedJcaJceHelper;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.operator.PGPDataDecryptor;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
-import java.nio.ByteBuffer;
-import java.util.Map;
-
public class CachingDataDecryptorFactory implements PublicKeyDataDecryptorFactory
{
private final PublicKeyDataDecryptorFactory mWrappedDecryptor;
@@ -59,6 +60,10 @@ public class CachingDataDecryptorFactory implements PublicKeyDataDecryptorFactor
return mSessionKeyCache.get(bi);
}
+ if (mWrappedDecryptor == null) {
+ throw new IllegalStateException("tried to decrypt without wrapped decryptor, this is a bug!");
+ }
+
byte[] sessionData = mWrappedDecryptor.recoverSessionData(keyAlgorithm, secKeyData);
mSessionKeyCache.put(bi, sessionData);
return sessionData;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
index 53fb5af..fd6e903 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java
@@ -118,6 +118,7 @@ public final class Constants {
// keyserver sync settings
public static final String SYNC_CONTACTS = "syncContacts";
public static final String SYNC_KEYSERVER = "syncKeyserver";
+ public static final String ENABLE_WIFI_SYNC_ONLY = "enableWifiSyncOnly";
// other settings
public static final String EXPERIMENTAL_ENABLE_WORD_CONFIRM = "experimentalEnableWordConfirm";
public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities";
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java
index e1f61a5..2f0ebe9 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java
@@ -100,6 +100,11 @@ public class KeychainApplication extends Application {
// Add OpenKeychain account to Android to link contacts with keys and keyserver sync
createAccountIfNecessary(this);
+ if (Preferences.getKeyserverSyncEnabled(this)) {
+ // will update a keyserver sync if the interval has changed
+ KeyserverSyncAdapterService.enableKeyserverSync(this);
+ }
+
// if first time, enable keyserver and contact sync
if (Preferences.getPreferences(this).isFirstTime()) {
KeyserverSyncAdapterService.enableKeyserverSync(this);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/FacebookKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/FacebookKeyserver.java
index d87a82a..6217d1a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/FacebookKeyserver.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/FacebookKeyserver.java
@@ -21,11 +21,12 @@ package org.sufficientlysecure.keychain.keyimport;
import android.net.Uri;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
@@ -33,11 +34,12 @@ import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
+import org.sufficientlysecure.keychain.util.TlsHelper;
import java.io.IOException;
import java.net.Proxy;
-import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@@ -104,11 +106,10 @@ public class FacebookKeyserver extends Keyserver {
String request = String.format(FB_KEY_URL_FORMAT, fbUsername);
Log.d(Constants.TAG, "fetching from Facebook with: " + request + " proxy: " + mProxy);
- OkHttpClient client = new OkHttpClient();
- client.setProxy(mProxy);
-
URL url = new URL(request);
+ OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(url, mProxy);
+
Response response = client.newCall(new Request.Builder().url(url).build()).execute();
// contains body both in case of success or failure
@@ -126,6 +127,9 @@ public class FacebookKeyserver extends Keyserver {
throw new QueryFailedException("Cannot connect to Facebook. "
+ "Check your Internet connection!"
+ (mProxy == Proxy.NO_PROXY ? "" : " Using proxy " + mProxy));
+ } catch (TlsHelper.TlsHelperException e) {
+ Log.e(Constants.TAG, "Exception in cert pinning", e);
+ throw new QueryFailedException("Exception in cert pinning. ");
}
}
@@ -190,8 +194,11 @@ public class FacebookKeyserver extends Keyserver {
return uri.getPathSegments().get(0);
}
- public static boolean isFacebookHost(Uri uri) {
+ public static boolean isFacebookHost(@Nullable Uri uri) {
+ if (uri == null) {
+ return false;
+ }
String host = uri.getHost();
- return host.equalsIgnoreCase(FB_HOST) || host.equalsIgnoreCase(FB_HOST_WWW);
+ return FB_HOST.equalsIgnoreCase(host) || FB_HOST_WWW.equalsIgnoreCase(host);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java
index c219031..5e3d2eb 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java
@@ -18,16 +18,18 @@
package org.sufficientlysecure.keychain.keyimport;
-import com.squareup.okhttp.MediaType;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.RequestBody;
-import com.squareup.okhttp.Response;
+
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
import org.sufficientlysecure.keychain.util.TlsHelper;
import java.io.IOException;
@@ -42,7 +44,6 @@ import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -199,43 +200,12 @@ public class HkpKeyserver extends Keyserver {
return mSecure ? "https://" : "http://";
}
- /**
- * returns a client with pinned certificate if necessary
- *
- * @param url url to be queried by client
- * @param proxy proxy to be used by client
- * @return client with a pinned certificate if necessary
- */
- public static OkHttpClient getClient(URL url, Proxy proxy) throws IOException {
- OkHttpClient client = new OkHttpClient();
-
- try {
- TlsHelper.usePinnedCertificateIfAvailable(client, url);
- } catch (TlsHelper.TlsHelperException e) {
- Log.w(Constants.TAG, e);
- }
-
- // don't follow any redirects
- client.setFollowRedirects(false);
- client.setFollowSslRedirects(false);
-
- if (proxy != null) {
- client.setProxy(proxy);
- client.setConnectTimeout(30000, TimeUnit.MILLISECONDS);
- } else {
- client.setProxy(Proxy.NO_PROXY);
- client.setConnectTimeout(5000, TimeUnit.MILLISECONDS);
- }
- client.setReadTimeout(45000, TimeUnit.MILLISECONDS);
-
- return client;
- }
private String query(String request, @NonNull Proxy proxy) throws QueryFailedException, HttpError {
try {
URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + request);
Log.d(Constants.TAG, "hkp keyserver query: " + url + " Proxy: " + proxy);
- OkHttpClient client = getClient(url, proxy);
+ OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(url, proxy);
Response response = client.newCall(new Request.Builder().url(url).build()).execute();
String responseBody = response.body().string(); // contains body both in case of success or failure
@@ -249,6 +219,9 @@ public class HkpKeyserver extends Keyserver {
Log.e(Constants.TAG, "IOException at HkpKeyserver", e);
throw new QueryFailedException("Keyserver '" + mHost + "' is unavailable. Check your Internet connection!" +
(proxy == Proxy.NO_PROXY ? "" : " Using proxy " + proxy));
+ } catch (TlsHelper.TlsHelperException e) {
+ Log.e(Constants.TAG, "Exception in pinning certs", e);
+ throw new QueryFailedException("Exception in pinning certs");
}
}
@@ -413,6 +386,7 @@ public class HkpKeyserver extends Keyserver {
Log.d(Constants.TAG, "hkp keyserver add: " + url);
Log.d(Constants.TAG, "params: " + params);
+
RequestBody body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), params);
Request request = new Request.Builder()
@@ -422,7 +396,7 @@ public class HkpKeyserver extends Keyserver {
.post(body)
.build();
- Response response = getClient(url, mProxy).newCall(request).execute();
+ Response response = OkHttpClientFactory.getClientPinnedIfAvailable(url, mProxy).newCall(request).execute();
Log.d(Constants.TAG, "response code: " + response.code());
Log.d(Constants.TAG, "answer: " + response.body().string());
@@ -434,6 +408,9 @@ public class HkpKeyserver extends Keyserver {
} catch (IOException e) {
Log.e(Constants.TAG, "IOException", e);
throw new AddKeyException();
+ } catch (TlsHelper.TlsHelperException e) {
+ Log.e(Constants.TAG, "Exception in pinning certs", e);
+ throw new AddKeyException();
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java
index e5a128e..a5f882d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java
@@ -2,12 +2,10 @@ package org.sufficientlysecure.keychain.linked;
import android.content.Context;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.params.BasicHttpParams;
+import okhttp3.CertificatePinner;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
import org.json.JSONException;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
@@ -18,12 +16,9 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
-import org.thoughtcrime.ssl.pinning.util.PinningHelper;
+import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.HashMap;
@@ -233,46 +228,38 @@ public abstract class LinkedTokenResource extends LinkedResource {
}
- @SuppressWarnings("deprecation") // HttpRequestBase is deprecated
- public static String getResponseBody(Context context, HttpRequestBase request)
- throws IOException, HttpStatusException {
- return getResponseBody(context, request, null);
- }
- @SuppressWarnings("deprecation") // HttpRequestBase is deprecated
- public static String getResponseBody(Context context, HttpRequestBase request, String[] pins)
- throws IOException, HttpStatusException {
- StringBuilder sb = new StringBuilder();
+ private static CertificatePinner getCertificatePinner(String hostname, String[] pins){
+ CertificatePinner.Builder builder = new CertificatePinner.Builder();
+ for(String pin : pins){
+ builder.add(hostname,pin);
+ }
+ return builder.build();
+ }
- request.setHeader("User-Agent", "Open Keychain");
+ public static String getResponseBody(Request request, String... pins)
+ throws IOException, HttpStatusException {
- HttpClient httpClient;
- if (pins == null) {
- httpClient = new DefaultHttpClient(new BasicHttpParams());
+ Log.d("Connection to: " + request.url().url().getHost(), "");
+ OkHttpClient client;
+ if (pins != null) {
+ client = OkHttpClientFactory.getSimpleClientPinned(getCertificatePinner(request.url().url().getHost(), pins));
} else {
- httpClient = PinningHelper.getPinnedHttpClient(context, pins);
+ client = OkHttpClientFactory.getSimpleClient();
}
- HttpResponse response = httpClient.execute(request);
- int statusCode = response.getStatusLine().getStatusCode();
- String reason = response.getStatusLine().getReasonPhrase();
+ Response response = client.newCall(request).execute();
- if (statusCode != 200) {
- throw new HttpStatusException(statusCode, reason);
- }
- HttpEntity entity = response.getEntity();
- InputStream inputStream = entity.getContent();
+ int statusCode = response.code();
+ String reason = response.message();
- BufferedReader bReader = new BufferedReader(
- new InputStreamReader(inputStream, "UTF-8"), 8);
- String line;
- while ((line = bReader.readLine()) != null) {
- sb.append(line);
+ if (statusCode != 200) {
+ throw new HttpStatusException(statusCode, reason);
}
- return sb.toString();
+ return response.body().string();
}
public static class HttpStatusException extends Throwable {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java
index 82240c4..da531e8 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java
@@ -6,7 +6,7 @@ import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
-import org.apache.http.client.methods.HttpGet;
+import okhttp3.Request;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
@@ -32,14 +32,16 @@ public class GenericHttpsResource extends LinkedTokenResource {
token, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24));
}
- @SuppressWarnings("deprecation") // HttpGet is deprecated
@Override
protected String fetchResource (Context context, OperationLog log, int indent)
throws HttpStatusException, IOException {
log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
- HttpGet httpGet = new HttpGet(mSubUri);
- return getResponseBody(context, httpGet);
+ Request request = new Request.Builder()
+ .url(mSubUri.toURL())
+ .addHeader("User-Agent", "OpenKeychain")
+ .build();
+ return getResponseBody(request);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java
index 7a97ffd..0e87ca6 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java
@@ -6,7 +6,7 @@ import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
-import org.apache.http.client.methods.HttpGet;
+import okhttp3.Request;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -47,7 +47,7 @@ public class GithubResource extends LinkedTokenResource {
return String.format(context.getResources().getString(R.string.linked_id_github_text), token);
}
- @SuppressWarnings("deprecation") // HttpGet is deprecated
+
@Override
protected String fetchResource (Context context, OperationLog log, int indent)
throws HttpStatusException, IOException, JSONException {
@@ -55,8 +55,11 @@ public class GithubResource extends LinkedTokenResource {
log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
indent += 1;
- HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + mGistId);
- String response = getResponseBody(context, httpGet);
+ Request request = new Request.Builder()
+ .url("https://api.github.com/gists/" + mGistId)
+ .addHeader("User-Agent", "OpenKeychain")
+ .build();
+ String response = getResponseBody(request);
JSONObject obj = new JSONObject(response);
@@ -79,7 +82,7 @@ public class GithubResource extends LinkedTokenResource {
}
- @Deprecated // not used for now, but could be used to pick up earlier posted gist if already present?
+
@SuppressWarnings({ "deprecation", "unused" })
public static GithubResource searchInGithubStream(
Context context, String screenName, String needle, OperationLog log) {
@@ -94,12 +97,12 @@ public class GithubResource extends LinkedTokenResource {
try {
JSONArray array; {
- HttpGet httpGet =
- new HttpGet("https://api.github.com/users/" + screenName + "/gists");
- httpGet.setHeader("Content-Type", "application/json");
- httpGet.setHeader("User-Agent", "OpenKeychain");
-
- String response = getResponseBody(context, httpGet);
+ Request request = new Request.Builder()
+ .url("https://api.github.com/users/" + screenName + "/gists")
+ .addHeader("Content-Type", "application/json")
+ .addHeader("User-Agent", "OpenKeychain")
+ .build();
+ String response = getResponseBody(request);
array = new JSONArray(response);
}
@@ -116,10 +119,13 @@ public class GithubResource extends LinkedTokenResource {
continue;
}
String id = obj.getString("id");
- HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + id);
- httpGet.setHeader("User-Agent", "OpenKeychain");
- JSONObject gistObj = new JSONObject(getResponseBody(context, httpGet));
+ Request request = new Request.Builder()
+ .url("https://api.github.com/gists/" + id)
+ .addHeader("User-Agent", "OpenKeychain")
+ .build();
+
+ JSONObject gistObj = new JSONObject(getResponseBody(request));
JSONObject gistFiles = gistObj.getJSONObject("files");
Iterator<String> gistIt = gistFiles.keys();
if (!gistIt.hasNext()) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java
index 73e3d36..db3b642 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java
@@ -9,9 +9,9 @@ import android.util.Log;
import com.textuality.keybase.lib.JWalk;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.StringEntity;
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -84,18 +84,19 @@ public class TwitterResource extends LinkedTokenResource {
return null;
}
- HttpGet httpGet =
- new HttpGet("https://api.twitter.com/1.1/statuses/show.json"
- + "?id=" + mTweetId
- + "&include_entities=false");
-
// construct a normal HTTPS request and include an Authorization
// header with the value of Bearer <>
- httpGet.setHeader("Authorization", "Bearer " + authToken);
- httpGet.setHeader("Content-Type", "application/json");
+ Request request = new Request.Builder()
+ .url("https://api.twitter.com/1.1/statuses/show.json"
+ + "?id=" + mTweetId
+ + "&include_entities=false")
+ .addHeader("Authorization", "Bearer " + authToken)
+ .addHeader("Content-Type", "application/json")
+ .addHeader("User-Agent", "OpenKeychain")
+ .build();
try {
- String response = getResponseBody(context, httpGet, CERT_PINS);
+ String response = getResponseBody(request, CERT_PINS);
JSONObject obj = new JSONObject(response);
JSONObject user = obj.getJSONObject("user");
if (!mHandle.equalsIgnoreCase(user.getString("screen_name"))) {
@@ -157,21 +158,20 @@ public class TwitterResource extends LinkedTokenResource {
return null;
}
- HttpGet httpGet =
- new HttpGet("https://api.twitter.com/1.1/statuses/user_timeline.json"
+ Request request = new Request.Builder()
+ .url("https://api.twitter.com/1.1/statuses/user_timeline.json"
+ "?screen_name=" + screenName
+ "&count=15"
+ "&include_rts=false"
+ "&trim_user=true"
- + "&exclude_replies=true");
-
- // construct a normal HTTPS request and include an Authorization
- // header with the value of Bearer <>
- httpGet.setHeader("Authorization", "Bearer " + authToken);
- httpGet.setHeader("Content-Type", "application/json");
+ + "&exclude_replies=true")
+ .addHeader("Authorization", "Bearer " + authToken)
+ .addHeader("Content-Type", "application/json")
+ .addHeader("User-Agent", "OpenKeychain")
+ .build();
try {
- String response = getResponseBody(context, httpGet, CERT_PINS);
+ String response = getResponseBody(request, CERT_PINS);
JSONArray array = new JSONArray(response);
for (int i = 0; i < array.length(); i++) {
@@ -216,12 +216,20 @@ public class TwitterResource extends LinkedTokenResource {
String base64Encoded = rot13("D293FQqanH0jH29KIaWJER5DomqSGRE2Ewc1LJACn3cbD1c"
+ "Fq1bmqSAQAz5MI2cIHKOuo3cPoRAQI1OyqmIVFJS6LHMXq2g6MRLkIj") + "==";
+ RequestBody requestBody = RequestBody.create(
+ MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8"),
+ "grant_type=client_credentials");
+
// Step 2: Obtain a bearer token
- HttpPost httpPost = new HttpPost("https://api.twitter.com/oauth2/token");
- httpPost.setHeader("Authorization", "Basic " + base64Encoded);
- httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
- httpPost.setEntity(new StringEntity("grant_type=client_credentials"));
- JSONObject rawAuthorization = new JSONObject(getResponseBody(context, httpPost, CERT_PINS));
+ Request request = new Request.Builder()
+ .url("https://api.twitter.com/oauth2/token")
+ .addHeader("Authorization", "Basic " + base64Encoded)
+ .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
+ .addHeader("User-Agent", "OpenKeychain")
+ .post(requestBody)
+ .build();
+
+ JSONObject rawAuthorization = new JSONObject(getResponseBody(request, CERT_PINS));
// Applications should verify that the value associated with the
// token_type key of the returned object is bearer
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java
index 79b42ec..b4b27f7 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java
@@ -47,7 +47,7 @@ import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
-import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Passphrase;
@@ -144,7 +144,7 @@ public class CertifyOperation extends BaseOperation<CertifyActionsParcel> {
int certifyOk = 0, certifyError = 0, uploadOk = 0, uploadError = 0;
- NfcSignOperationsBuilder allRequiredInput = new NfcSignOperationsBuilder(
+ SecurityTokenSignOperationsBuilder allRequiredInput = new SecurityTokenSignOperationsBuilder(
cryptoInput.getSignatureTime(), masterKeyId, masterKeyId);
// Work through all requested certifications
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ChangeUnlockOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ChangeUnlockOperation.java
new file mode 100644
index 0000000..f9ae13b
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ChangeUnlockOperation.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 Alex Fong Jie Wen <alexfongg@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.operations;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.operations.results.EditKeyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
+import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
+import org.sufficientlysecure.keychain.pgp.Progressable;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.ProgressScaler;
+
+
+public class ChangeUnlockOperation extends BaseOperation<ChangeUnlockParcel> {
+
+ public ChangeUnlockOperation(Context context, ProviderHelper providerHelper, Progressable progressable) {
+ super(context, providerHelper, progressable);
+ }
+
+ @NonNull
+ public OperationResult execute(ChangeUnlockParcel unlockParcel, CryptoInputParcel cryptoInput) {
+ OperationResult.OperationLog log = new OperationResult.OperationLog();
+ log.add(OperationResult.LogType.MSG_ED, 0);
+
+ if (unlockParcel == null || unlockParcel.mMasterKeyId == null) {
+ log.add(OperationResult.LogType.MSG_ED_ERROR_NO_PARCEL, 1);
+ return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
+ }
+
+ // Perform actual modification
+ PgpEditKeyResult modifyResult;
+ {
+ PgpKeyOperation keyOperations =
+ new PgpKeyOperation(new ProgressScaler(mProgressable, 0, 70, 100));
+
+ try {
+ log.add(OperationResult.LogType.MSG_ED_FETCHING, 1,
+ KeyFormattingUtils.convertKeyIdToHex(unlockParcel.mMasterKeyId));
+
+ CanonicalizedSecretKeyRing secRing =
+ mProviderHelper.getCanonicalizedSecretKeyRing(unlockParcel.mMasterKeyId);
+ modifyResult = keyOperations.modifyKeyRingPassphrase(secRing, cryptoInput, unlockParcel);
+
+ if (modifyResult.isPending()) {
+ // obtain original passphrase from user
+ log.add(modifyResult, 1);
+ return new EditKeyResult(log, modifyResult);
+ }
+ } catch (ProviderHelper.NotFoundException e) {
+ log.add(OperationResult.LogType.MSG_ED_ERROR_KEY_NOT_FOUND, 2);
+ return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
+ }
+ }
+
+ log.add(modifyResult, 1);
+
+ if (!modifyResult.success()) {
+ // error is already logged by modification
+ return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
+ }
+
+ // Cannot cancel from here on out!
+ mProgressable.setPreventCancel();
+
+ // It's a success, so this must be non-null now
+ UncachedKeyRing ring = modifyResult.getRing();
+
+ SaveKeyringResult saveResult = mProviderHelper
+ .saveSecretKeyRing(ring, new ProgressScaler(mProgressable, 70, 95, 100));
+ log.add(saveResult, 1);
+
+ // If the save operation didn't succeed, exit here
+ if (!saveResult.success()) {
+ return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);
+ }
+
+ updateProgress(R.string.progress_done, 100, 100);
+ log.add(OperationResult.LogType.MSG_ED_SUCCESS, 0);
+ return new EditKeyResult(EditKeyResult.RESULT_OK, log, ring.getMasterKeyId());
+
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java
index 43fc11b..6682cc6 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java
@@ -119,8 +119,9 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
// inform the storage provider about the mime type for this uri
if (decryptResult.getDecryptionMetadata() != null) {
- TemporaryFileProvider.setMimeType(mContext, currentInputUri,
- decryptResult.getDecryptionMetadata().getMimeType());
+ OpenPgpMetadata meta = decryptResult.getDecryptionMetadata();
+ TemporaryFileProvider.setName(mContext, currentInputUri, meta.getFilename());
+ TemporaryFileProvider.setMimeType(mContext, currentInputUri, meta.getMimeType());
}
} else {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java
index 2ca7406..5bca372 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java
@@ -43,7 +43,7 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
-import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequiredInputType;
import org.sufficientlysecure.keychain.util.FileHelper;
import org.sufficientlysecure.keychain.util.InputData;
@@ -80,7 +80,7 @@ public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> {
int total = inputBytes != null ? 1 : inputUris.size(), count = 0;
ArrayList<PgpSignEncryptResult> results = new ArrayList<>();
- NfcSignOperationsBuilder pendingInputBuilder = null;
+ SecurityTokenSignOperationsBuilder pendingInputBuilder = null;
// if signing subkey has not explicitly been set, get first usable subkey capable of signing
if (input.getSignatureMasterKeyId() != Constants.key.none
@@ -161,7 +161,7 @@ public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> {
return new SignEncryptResult(log, requiredInput, results, cryptoInput);
}
if (pendingInputBuilder == null) {
- pendingInputBuilder = new NfcSignOperationsBuilder(requiredInput.mSignatureTime,
+ pendingInputBuilder = new SecurityTokenSignOperationsBuilder(requiredInput.mSignatureTime,
input.getSignatureMasterKeyId(), input.getSignatureSubKeyId());
}
pendingInputBuilder.addAll(requiredInput);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java
index 1a8f10d..7c394fc 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DeleteResult.java
@@ -131,7 +131,8 @@ public class DeleteResult extends InputPendingResult {
else if (mFail == 0) {
str = activity.getString(R.string.delete_nothing);
} else {
- str = activity.getResources().getQuantityString(R.plurals.delete_fail, mFail);
+ str = activity.getResources().getQuantityString(
+ R.plurals.delete_fail, mFail, mFail);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
index ec2fddb..d3d9628 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java
@@ -539,6 +539,7 @@ public abstract class OperationResult implements Parcelable {
// secret key modify
MSG_MF (LogLevel.START, R.string.msg_mr),
MSG_MF_DIVERT (LogLevel.DEBUG, R.string.msg_mf_divert),
+ MSG_MF_ERROR_ALL_KEYS_STRIPPED (LogLevel.ERROR, R.string.msg_mf_error_all_keys_stripped),
MSG_MF_ERROR_DIVERT_NEWSUB (LogLevel.ERROR, R.string.msg_mf_error_divert_newsub),
MSG_MF_ERROR_DIVERT_SERIAL (LogLevel.ERROR, R.string.msg_mf_error_divert_serial),
MSG_MF_ERROR_ENCODE (LogLevel.ERROR, R.string.msg_mf_error_encode),
@@ -552,6 +553,7 @@ public abstract class OperationResult implements Parcelable {
MSG_MF_ERROR_NOOP (LogLevel.ERROR, R.string.msg_mf_error_noop),
MSG_MF_ERROR_NULL_EXPIRY (LogLevel.ERROR, R.string.msg_mf_error_null_expiry),
MSG_MF_ERROR_PASSPHRASE_MASTER(LogLevel.ERROR, R.string.msg_mf_error_passphrase_master),
+ MSG_MF_ERROR_PASSPHRASES_UNCHANGED(LogLevel.ERROR, R.string.msg_mf_error_passphrases_unchanged),
MSG_MF_ERROR_PAST_EXPIRY(LogLevel.ERROR, R.string.msg_mf_error_past_expiry),
MSG_MF_ERROR_PGP (LogLevel.ERROR, R.string.msg_mf_error_pgp),
MSG_MF_ERROR_RESTRICTED(LogLevel.ERROR, R.string.msg_mf_error_restricted),
@@ -566,8 +568,6 @@ public abstract class OperationResult implements Parcelable {
MSG_MF_ERROR_BAD_SECURITY_TOKEN_SIZE(LogLevel.ERROR, R.string.edit_key_error_bad_security_token_size),
MSG_MF_ERROR_BAD_SECURITY_TOKEN_STRIPPED(LogLevel.ERROR, R.string.edit_key_error_bad_security_token_stripped),
MSG_MF_MASTER (LogLevel.DEBUG, R.string.msg_mf_master),
- MSG_MF_NOTATION_PIN (LogLevel.DEBUG, R.string.msg_mf_notation_pin),
- MSG_MF_NOTATION_EMPTY (LogLevel.DEBUG, R.string.msg_mf_notation_empty),
MSG_MF_PASSPHRASE (LogLevel.INFO, R.string.msg_mf_passphrase),
MSG_MF_PIN (LogLevel.INFO, R.string.msg_mf_pin),
MSG_MF_ADMIN_PIN (LogLevel.INFO, R.string.msg_mf_admin_pin),
@@ -730,6 +730,7 @@ public abstract class OperationResult implements Parcelable {
MSG_PSE_ERROR_PGP (LogLevel.ERROR, R.string.msg_pse_error_pgp),
MSG_PSE_ERROR_SIG (LogLevel.ERROR, R.string.msg_pse_error_sig),
MSG_PSE_ERROR_UNLOCK (LogLevel.ERROR, R.string.msg_pse_error_unlock),
+ MSG_PSE_ERROR_REVOKED_OR_EXPIRED (LogLevel.ERROR, R.string.msg_pse_error_revoked_or_expired),
MSG_PSE_KEY_OK (LogLevel.OK, R.string.msg_pse_key_ok),
MSG_PSE_KEY_UNKNOWN (LogLevel.DEBUG, R.string.msg_pse_key_unknown),
MSG_PSE_KEY_WARN (LogLevel.WARN, R.string.msg_pse_key_warn),
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java
index 77977b6..1ebab78 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java
@@ -61,6 +61,8 @@ public abstract class KeyRing {
private static final Pattern USER_ID_PATTERN = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$");
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("^.*@.*\\..*$");
+
/**
* Splits userId string into naming part, email part, and comment part
* <p/>
@@ -71,25 +73,38 @@ public abstract class KeyRing {
if (!TextUtils.isEmpty(userId)) {
final Matcher matcher = USER_ID_PATTERN.matcher(userId);
if (matcher.matches()) {
- return new UserId(matcher.group(1), matcher.group(3), matcher.group(2));
+ String name = matcher.group(1).isEmpty() ? null : matcher.group(1);
+ String comment = matcher.group(2);
+ String email = matcher.group(3);
+ if (comment == null && email == null && name != null && EMAIL_PATTERN.matcher(name).matches()) {
+ email = name;
+ name = null;
+ }
+ return new UserId(name, email, comment);
}
}
return new UserId(null, null, null);
}
/**
- * Returns a composed user id. Returns null if name is null!
+ * Returns a composed user id. Returns null if name, email and comment are empty.
*/
public static String createUserId(UserId userId) {
- String userIdString = userId.name; // consider name a required value
- if (userIdString != null && !TextUtils.isEmpty(userId.comment)) {
- userIdString += " (" + userId.comment + ")";
+ StringBuilder userIdBuilder = new StringBuilder();
+ if (!TextUtils.isEmpty(userId.name)) {
+ userIdBuilder.append(userId.name);
}
- if (userIdString != null && !TextUtils.isEmpty(userId.email)) {
- userIdString += " <" + userId.email + ">";
+ if (!TextUtils.isEmpty(userId.comment)) {
+ userIdBuilder.append(" (");
+ userIdBuilder.append(userId.comment);
+ userIdBuilder.append(")");
}
-
- return userIdString;
+ if (!TextUtils.isEmpty(userId.email)) {
+ userIdBuilder.append(" <");
+ userIdBuilder.append(userId.email);
+ userIdBuilder.append(">");
+ }
+ return userIdBuilder.length() == 0 ? null : userIdBuilder.toString();
}
public static class UserId implements Serializable {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java
index c4525e5..31a3f91 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpDecryptionResultBuilder.java
@@ -26,6 +26,8 @@ public class OpenPgpDecryptionResultBuilder {
// builder
private boolean mInsecure = false;
private boolean mEncrypted = false;
+ private byte[] sessionKey;
+ private byte[] decryptedSessionKey;
public void setInsecure(boolean insecure) {
this.mInsecure = insecure;
@@ -36,24 +38,26 @@ public class OpenPgpDecryptionResultBuilder {
}
public OpenPgpDecryptionResult build() {
- OpenPgpDecryptionResult result = new OpenPgpDecryptionResult();
-
if (mInsecure) {
Log.d(Constants.TAG, "RESULT_INSECURE");
- result.setResult(OpenPgpDecryptionResult.RESULT_INSECURE);
- return result;
+ return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_INSECURE, sessionKey, decryptedSessionKey);
}
if (mEncrypted) {
Log.d(Constants.TAG, "RESULT_ENCRYPTED");
- result.setResult(OpenPgpDecryptionResult.RESULT_ENCRYPTED);
- } else {
- Log.d(Constants.TAG, "RESULT_NOT_ENCRYPTED");
- result.setResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED);
+ return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_ENCRYPTED, sessionKey, decryptedSessionKey);
}
- return result;
+ Log.d(Constants.TAG, "RESULT_NOT_ENCRYPTED");
+ return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED);
}
+ public void setSessionKey(byte[] sessionKey, byte[] decryptedSessionKey) {
+ if ((sessionKey == null) != (decryptedSessionKey == null)) {
+ throw new AssertionError("sessionKey must be null iff decryptedSessionKey is null!");
+ }
+ this.sessionKey = sessionKey;
+ this.decryptedSessionKey = decryptedSessionKey;
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpCertifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpCertifyOperation.java
index aa1c2e0..ae0a311 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpCertifyOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpCertifyOperation.java
@@ -37,7 +37,7 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
-import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
@@ -76,7 +76,7 @@ public class PgpCertifyOperation {
// get the master subkey (which we certify for)
PGPPublicKey publicKey = publicRing.getPublicKey().getPublicKey();
- NfcSignOperationsBuilder requiredInput = new NfcSignOperationsBuilder(creationTimestamp,
+ SecurityTokenSignOperationsBuilder requiredInput = new SecurityTokenSignOperationsBuilder(creationTimestamp,
publicKey.getKeyID(), publicKey.getKeyID());
try {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
index e15139a..a27e4a8 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java
@@ -26,9 +26,12 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.ByteBuffer;
import java.security.SignatureException;
import java.util.Date;
import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
import android.content.Context;
import android.support.annotation.NonNull;
@@ -60,7 +63,6 @@ import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Constants.key;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.BaseOperation;
-import org.sufficientlysecure.keychain.util.CharsetVerifier;
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
@@ -73,6 +75,7 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.util.CharsetVerifier;
import org.sufficientlysecure.keychain.util.FileHelper;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.Log;
@@ -197,6 +200,10 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
PGPEncryptedData encryptedData;
InputStream cleartextStream;
+ // the cached session key
+ byte[] sessionKey;
+ byte[] decryptedSessionKey;
+
int symmetricEncryptionAlgo = 0;
boolean skippedDisallowedKey = false;
@@ -304,6 +311,9 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
// if this worked out so far, the data is encrypted
decryptionResultBuilder.setEncrypted(true);
+ if (esResult.sessionKey != null && esResult.decryptedSessionKey != null) {
+ decryptionResultBuilder.setSessionKey(esResult.sessionKey, esResult.decryptedSessionKey);
+ }
if (esResult.insecureEncryptionKey) {
log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1);
@@ -545,10 +555,14 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
boolean asymmetricPacketFound = false;
boolean symmetricPacketFound = false;
boolean anyPacketFound = false;
+ boolean decryptedSessionKeyAvailable = false;
PGPPublicKeyEncryptedData encryptedDataAsymmetric = null;
PGPPBEEncryptedData encryptedDataSymmetric = null;
CanonicalizedSecretKey decryptionKey = null;
+ CachingDataDecryptorFactory cachedKeyDecryptorFactory = new CachingDataDecryptorFactory(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME, cryptoInput.getCryptoData());
+ ;
Passphrase passphrase = null;
@@ -569,6 +583,13 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
log.add(LogType.MSG_DC_ASYM, indent,
KeyFormattingUtils.convertKeyIdToHex(subKeyId));
+ decryptedSessionKeyAvailable = cachedKeyDecryptorFactory.hasCachedSessionData(encData);
+ if (decryptedSessionKeyAvailable) {
+ asymmetricPacketFound = true;
+ encryptedDataAsymmetric = encData;
+ break;
+ }
+
CachedPublicKeyRing cachedPublicKeyRing;
try {
// get actual keyring object based on master key id
@@ -746,34 +767,38 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
currentProgress += 2;
updateProgress(R.string.progress_extracting_key, currentProgress, 100);
- try {
- log.add(LogType.MSG_DC_UNLOCKING, indent + 1);
- if (!decryptionKey.unlock(passphrase)) {
- log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1);
+ CachingDataDecryptorFactory decryptorFactory;
+ if (decryptedSessionKeyAvailable) {
+ decryptorFactory = cachedKeyDecryptorFactory;
+ } else {
+ try {
+ log.add(LogType.MSG_DC_UNLOCKING, indent + 1);
+ if (!decryptionKey.unlock(passphrase)) {
+ log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1);
+ return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
+ }
+ } catch (PgpGeneralException e) {
+ log.add(LogType.MSG_DC_ERROR_EXTRACT_KEY, indent + 1);
return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
}
- } catch (PgpGeneralException e) {
- log.add(LogType.MSG_DC_ERROR_EXTRACT_KEY, indent + 1);
- return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log));
- }
-
- currentProgress += 2;
- updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
- CachingDataDecryptorFactory decryptorFactory
- = decryptionKey.getCachingDecryptorFactory(cryptoInput);
+ currentProgress += 2;
+ updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
- // special case: if the decryptor does not have a session key cached for this encrypted
- // data, and can't actually decrypt on its own, return a pending intent
- if (!decryptorFactory.canDecrypt()
- && !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) {
+ decryptorFactory = decryptionKey.getCachingDecryptorFactory(cryptoInput);
- log.add(LogType.MSG_DC_PENDING_NFC, indent + 1);
- return result.with(new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation(
- decryptionKey.getRing().getMasterKeyId(),
- decryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0]
- ), cryptoInput));
+ // special case: if the decryptor does not have a session key cached for this encrypted
+ // data, and can't actually decrypt on its own, return a pending intent
+ if (!decryptorFactory.canDecrypt()
+ && !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) {
+ log.add(LogType.MSG_DC_PENDING_NFC, indent + 1);
+ return result.with(new DecryptVerifyResult(log,
+ RequiredInputParcel.createSecurityTokenDecryptOperation(
+ decryptionKey.getRing().getMasterKeyId(),
+ decryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0]
+ ), cryptoInput));
+ }
}
try {
@@ -786,8 +811,13 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
result.symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory);
result.encryptedData = encryptedDataAsymmetric;
- cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys());
-
+ Map<ByteBuffer, byte[]> cachedSessionKeys = decryptorFactory.getCachedSessionKeys();
+ cryptoInput.addCryptoData(cachedSessionKeys);
+ if (cachedSessionKeys.size() >= 1) {
+ Entry<ByteBuffer, byte[]> entry = cachedSessionKeys.entrySet().iterator().next();
+ result.sessionKey = entry.getKey().array();
+ result.decryptedSessionKey = entry.getValue();
+ }
} else {
// there wasn't even any useful data
if (!anyPacketFound) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java
index e435481..404e072 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java
@@ -18,6 +18,23 @@
package org.sufficientlysecure.keychain.pgp;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+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.security.spec.ECGenParameterSpec;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Stack;
+import java.util.concurrent.atomic.AtomicBoolean;
+
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.bcpg.sig.Features;
@@ -55,15 +72,15 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
-import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
-import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder;
-import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcKeyToCardOperationsBuilder;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenKeyToCardOperationsBuilder;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
@@ -71,22 +88,6 @@ import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.Primes;
import org.sufficientlysecure.keychain.util.ProgressScaler;
-import java.io.IOException;
-import java.math.BigInteger;
-import java.nio.ByteBuffer;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.SecureRandom;
-import java.security.SignatureException;
-import java.security.spec.ECGenParameterSpec;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.Stack;
-import java.util.concurrent.atomic.AtomicBoolean;
-
/**
* This class is the single place where ALL operations that actually modify a PGP public or secret
* key take place.
@@ -496,10 +497,10 @@ public class PgpKeyOperation {
OperationLog log,
int indent) {
- NfcSignOperationsBuilder nfcSignOps = new NfcSignOperationsBuilder(
+ SecurityTokenSignOperationsBuilder nfcSignOps = new SecurityTokenSignOperationsBuilder(
cryptoInput.getSignatureTime(), masterSecretKey.getKeyID(),
masterSecretKey.getKeyID());
- NfcKeyToCardOperationsBuilder nfcKeyToCardOps = new NfcKeyToCardOperationsBuilder(
+ SecurityTokenKeyToCardOperationsBuilder nfcKeyToCardOps = new SecurityTokenKeyToCardOperationsBuilder(
masterSecretKey.getKeyID());
progress(R.string.progress_modify, 0);
@@ -1053,13 +1054,13 @@ public class PgpKeyOperation {
}
// 6. If requested, change passphrase
- if (saveParcel.mNewUnlock != null) {
+ if (saveParcel.getChangeUnlockParcel() != null) {
progress(R.string.progress_modify_passphrase, 90);
log.add(LogType.MSG_MF_PASSPHRASE, indent);
indent += 1;
- sKR = applyNewUnlock(sKR, masterPublicKey, masterPrivateKey,
- cryptoInput.getPassphrase(), saveParcel.mNewUnlock, log, indent);
+ sKR = applyNewPassphrase(sKR, masterPublicKey, cryptoInput.getPassphrase(),
+ saveParcel.getChangeUnlockParcel().mNewPassphrase, log, indent);
if (sKR == null) {
// The error has been logged above, just return a bad state
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
@@ -1192,73 +1193,80 @@ public class PgpKeyOperation {
}
- private static PGPSecretKeyRing applyNewUnlock(
- PGPSecretKeyRing sKR,
- PGPPublicKey masterPublicKey,
- PGPPrivateKey masterPrivateKey,
- Passphrase passphrase,
- ChangeUnlockParcel newUnlock,
- OperationLog log, int indent) throws PGPException {
+ public PgpEditKeyResult modifyKeyRingPassphrase(CanonicalizedSecretKeyRing wsKR,
+ CryptoInputParcel cryptoInput,
+ ChangeUnlockParcel changeUnlockParcel) {
- if (newUnlock.mNewPassphrase != null) {
- sKR = applyNewPassphrase(sKR, masterPublicKey, passphrase, newUnlock.mNewPassphrase, log, indent);
-
- // if there is any old packet with notation data
- if (hasNotationData(sKR)) {
+ OperationLog log = new OperationLog();
+ int indent = 0;
- log.add(LogType.MSG_MF_NOTATION_EMPTY, indent);
+ if (changeUnlockParcel.mMasterKeyId == null || changeUnlockParcel.mMasterKeyId != wsKR.getMasterKeyId()) {
+ log.add(LogType.MSG_MF_ERROR_KEYID, indent);
+ return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
+ }
- // add packet with EMPTY notation data (updates old one, but will be stripped later)
- PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
- masterPrivateKey.getPublicKeyPacket().getAlgorithm(),
- PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO)
- .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
- PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
- { // set subpackets
- PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
- hashedPacketsGen.setExportable(false, false);
- sGen.setHashedSubpackets(hashedPacketsGen.generate());
- }
- sGen.init(PGPSignature.DIRECT_KEY, masterPrivateKey);
- PGPSignature emptySig = sGen.generateCertification(masterPublicKey);
+ log.add(LogType.MSG_MF, indent,
+ KeyFormattingUtils.convertKeyIdToHex(wsKR.getMasterKeyId()));
+ indent += 1;
+ progress(R.string.progress_building_key, 0);
- masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, emptySig);
- sKR = PGPSecretKeyRing.insertSecretKey(sKR,
- PGPSecretKey.replacePublicKey(sKR.getSecretKey(), masterPublicKey));
- }
+ // We work on bouncycastle object level here
+ PGPSecretKeyRing sKR = wsKR.getRing();
+ PGPSecretKey masterSecretKey = sKR.getSecretKey();
+ PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
+ // Make sure the fingerprint matches
+ if (changeUnlockParcel.mFingerprint == null || !Arrays.equals(changeUnlockParcel.mFingerprint,
+ masterSecretKey.getPublicKey().getFingerprint())) {
+ log.add(LogType.MSG_MF_ERROR_FINGERPRINT, indent);
+ return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
+ }
- return sKR;
+ // Find the first unstripped secret key
+ PGPSecretKey nonDummy = firstNonDummySecretKeyID(sKR);
+ if(nonDummy == null) {
+ log.add(OperationResult.LogType.MSG_MF_ERROR_ALL_KEYS_STRIPPED, indent);
+ return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
}
- if (newUnlock.mNewPin != null) {
- sKR = applyNewPassphrase(sKR, masterPublicKey, passphrase, newUnlock.mNewPin, log, indent);
+ if (!cryptoInput.hasPassphrase()) {
+ log.add(LogType.MSG_MF_REQUIRE_PASSPHRASE, indent);
- log.add(LogType.MSG_MF_NOTATION_PIN, indent);
+ return new PgpEditKeyResult(log, RequiredInputParcel.createRequiredSignPassphrase(
+ masterSecretKey.getKeyID(), nonDummy.getKeyID(),
+ cryptoInput.getSignatureTime()), cryptoInput);
+ } else {
+ progress(R.string.progress_modify_passphrase, 50);
+ log.add(LogType.MSG_MF_PASSPHRASE, indent);
+ indent += 1;
- // add packet with "pin" notation data
- PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
- masterPrivateKey.getPublicKeyPacket().getAlgorithm(),
- PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO)
- .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
- PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
- { // set subpackets
- PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
- hashedPacketsGen.setExportable(false, false);
- hashedPacketsGen.setNotationData(false, true, "unlock.pin@sufficientlysecure.org", "1");
- sGen.setHashedSubpackets(hashedPacketsGen.generate());
+ try {
+ sKR = applyNewPassphrase(sKR, masterPublicKey, cryptoInput.getPassphrase(),
+ changeUnlockParcel.mNewPassphrase, log, indent);
+ if (sKR == null) {
+ // The error has been logged above, just return a bad state
+ return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
+ }
+ } catch (PGPException e) {
+ throw new UnsupportedOperationException("Failed to build encryptor/decryptor!");
}
- sGen.init(PGPSignature.DIRECT_KEY, masterPrivateKey);
- PGPSignature emptySig = sGen.generateCertification(masterPublicKey);
-
- masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, emptySig);
- sKR = PGPSecretKeyRing.insertSecretKey(sKR,
- PGPSecretKey.replacePublicKey(sKR.getSecretKey(), masterPublicKey));
- return sKR;
+ indent -= 1;
+ progress(R.string.progress_done, 100);
+ log.add(LogType.MSG_MF_SUCCESS, indent);
+ return new PgpEditKeyResult(OperationResult.RESULT_OK, log, new UncachedKeyRing(sKR));
}
+ }
- throw new UnsupportedOperationException("PIN passphrases not yet implemented!");
+ private static PGPSecretKey firstNonDummySecretKeyID(PGPSecretKeyRing secRing) {
+ Iterator<PGPSecretKey> secretKeyIterator = secRing.getSecretKeys();
+ while(secretKeyIterator.hasNext()) {
+ PGPSecretKey secretKey = secretKeyIterator.next();
+ if(!isDummy(secretKey)){
+ return secretKey;
+ }
+ }
+ return null;
}
/** This method returns true iff the provided keyring has a local direct key signature
@@ -1293,9 +1301,9 @@ public class PgpKeyOperation {
PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_SYMMETRIC_ALGO, encryptorHashCalc,
PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(newPassphrase.getCharArray());
+ boolean keysModified = false;
- // noinspection unchecked
- for (PGPSecretKey sKey : new IterableIterator<PGPSecretKey>(sKR.getSecretKeys())) {
+ for (PGPSecretKey sKey : new IterableIterator<>(sKR.getSecretKeys())) {
log.add(LogType.MSG_MF_PASSPHRASE_KEY, indent,
KeyFormattingUtils.convertKeyIdToHex(sKey.getKeyID()));
@@ -1307,8 +1315,8 @@ public class PgpKeyOperation {
ok = true;
} catch (PGPException e) {
- // if this is the master key, error!
- if (sKey.getKeyID() == masterPublicKey.getKeyID()) {
+ // if the master key failed && it's not stripped, error!
+ if (sKey.getKeyID() == masterPublicKey.getKeyID() && !isDummy(sKey)) {
log.add(LogType.MSG_MF_ERROR_PASSPHRASE_MASTER, indent+1);
return null;
}
@@ -1335,7 +1343,13 @@ public class PgpKeyOperation {
}
sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey);
+ keysModified = true;
+ }
+ if(!keysModified) {
+ // no passphrase was changed
+ log.add(LogType.MSG_MF_ERROR_PASSPHRASES_UNCHANGED, indent+1);
+ return null;
}
return sKR;
@@ -1348,7 +1362,7 @@ public class PgpKeyOperation {
PGPPublicKey masterPublicKey,
int flags, long expiry,
CryptoInputParcel cryptoInput,
- NfcSignOperationsBuilder nfcSignOps,
+ SecurityTokenSignOperationsBuilder nfcSignOps,
int indent, OperationLog log)
throws PGPException, IOException, SignatureException {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java
index 5801039..8eae92e 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java
@@ -38,7 +38,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
protected Long mSignatureSubKeyId = null;
protected int mSignatureHashAlgorithm = PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT;
protected long mAdditionalEncryptId = Constants.key.none;
- protected boolean mFailOnMissingEncryptionKeyIds = false;
protected String mCharset;
protected boolean mCleartextSignature;
protected boolean mDetachedSignature = false;
@@ -65,7 +64,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
mSignatureSubKeyId = source.readInt() == 1 ? source.readLong() : null;
mSignatureHashAlgorithm = source.readInt();
mAdditionalEncryptId = source.readLong();
- mFailOnMissingEncryptionKeyIds = source.readInt() == 1;
mCharset = source.readString();
mCleartextSignature = source.readInt() == 1;
mDetachedSignature = source.readInt() == 1;
@@ -96,7 +94,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
}
dest.writeInt(mSignatureHashAlgorithm);
dest.writeLong(mAdditionalEncryptId);
- dest.writeInt(mFailOnMissingEncryptionKeyIds ? 1 : 0);
dest.writeString(mCharset);
dest.writeInt(mCleartextSignature ? 1 : 0);
dest.writeInt(mDetachedSignature ? 1 : 0);
@@ -113,10 +110,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
this.mCharset = mCharset;
}
- public boolean isFailOnMissingEncryptionKeyIds() {
- return mFailOnMissingEncryptionKeyIds;
- }
-
public long getAdditionalEncryptId() {
return mAdditionalEncryptId;
}
@@ -207,11 +200,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
return this;
}
- public PgpSignEncryptInputParcel setFailOnMissingEncryptionKeyIds(boolean failOnMissingEncryptionKeyIds) {
- mFailOnMissingEncryptionKeyIds = failOnMissingEncryptionKeyIds;
- return this;
- }
-
public PgpSignEncryptInputParcel setCleartextSignature(boolean cleartextSignature) {
this.mCleartextSignature = cleartextSignature;
return this;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java
index 0098760..0bb6419 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java
@@ -168,10 +168,17 @@ public class PgpSignEncryptOperation extends BaseOperation {
try {
long signingMasterKeyId = input.getSignatureMasterKeyId();
long signingSubKeyId = input.getSignatureSubKeyId();
- {
- CanonicalizedSecretKeyRing signingKeyRing =
- mProviderHelper.getCanonicalizedSecretKeyRing(signingMasterKeyId);
- signingKey = signingKeyRing.getSecretKey(input.getSignatureSubKeyId());
+
+ CanonicalizedSecretKeyRing signingKeyRing =
+ mProviderHelper.getCanonicalizedSecretKeyRing(signingMasterKeyId);
+ signingKey = signingKeyRing.getSecretKey(input.getSignatureSubKeyId());
+
+
+ // Make sure key is not expired or revoked
+ if (signingKeyRing.isExpired() || signingKeyRing.isRevoked()
+ || signingKey.isExpired() || signingKey.isRevoked()) {
+ log.add(LogType.MSG_PSE_ERROR_REVOKED_OR_EXPIRED, indent);
+ return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
}
// Make sure we are allowed to sign here!
@@ -281,16 +288,17 @@ public class PgpSignEncryptOperation extends BaseOperation {
if (encryptSubKeyIds.isEmpty()) {
log.add(LogType.MSG_PSE_KEY_WARN, indent + 1,
KeyFormattingUtils.convertKeyIdToHex(id));
- if (input.isFailOnMissingEncryptionKeyIds()) {
- return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
- }
+ return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
+ }
+ // Make sure key is not expired or revoked
+ if (keyRing.isExpired() || keyRing.isRevoked()) {
+ log.add(LogType.MSG_PSE_ERROR_REVOKED_OR_EXPIRED, indent);
+ return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
}
} catch (ProviderHelper.NotFoundException e) {
log.add(LogType.MSG_PSE_KEY_UNKNOWN, indent + 1,
KeyFormattingUtils.convertKeyIdToHex(id));
- if (input.isFailOnMissingEncryptionKeyIds()) {
- return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
- }
+ return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
}
}
}
@@ -470,7 +478,13 @@ public class PgpSignEncryptOperation extends BaseOperation {
InputStream in = new BufferedInputStream(inputData.getInputStream());
if (enableCompression) {
- compressGen = new PGPCompressedDataGenerator(input.getCompressionAlgorithm());
+ // Use preferred compression algo
+ int algo = input.getCompressionAlgorithm();
+ if (algo == PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.USE_DEFAULT) {
+ algo = PgpSecurityConstants.DEFAULT_COMPRESSION_ALGORITHM;
+ }
+
+ compressGen = new PGPCompressedDataGenerator(algo);
bcpgOut = new BCPGOutputStream(compressGen.open(out));
} else {
bcpgOut = new BCPGOutputStream(out);
@@ -514,7 +528,7 @@ public class PgpSignEncryptOperation extends BaseOperation {
} catch (NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded e) {
// this secret key diverts to a OpenPGP card, throw exception with hash that will be signed
log.add(LogType.MSG_PSE_PENDING_NFC, indent);
- return new PgpSignEncryptResult(log, RequiredInputParcel.createNfcSignOperation(
+ return new PgpSignEncryptResult(log, RequiredInputParcel.createSecurityTokenSignOperation(
signingKey.getRing().getMasterKeyId(), signingKey.getKeyId(),
e.hashToSign, e.hashAlgo, cryptoInput.getSignatureTime()), cryptoInput);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
index d696b9d..b0db36b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java
@@ -35,6 +35,8 @@ import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
+import android.support.annotation.VisibleForTesting;
+
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.SignatureSubpacketTags;
@@ -42,15 +44,22 @@ import org.bouncycastle.bcpg.UserAttributeSubpacketTags;
import org.bouncycastle.bcpg.sig.KeyFlags;
import org.bouncycastle.openpgp.PGPKeyRing;
import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector;
import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
@@ -1310,4 +1319,37 @@ public class UncachedKeyRing {
|| algorithm == PGPPublicKey.ECDH;
}
+ // ONLY TO BE USED FOR TESTING!!
+ @VisibleForTesting
+ public static UncachedKeyRing forTestingOnlyAddDummyLocalSignature(
+ UncachedKeyRing uncachedKeyRing, String passphrase) throws Exception {
+ PGPSecretKeyRing sKR = (PGPSecretKeyRing) uncachedKeyRing.mRing;
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
+ PGPPrivateKey masterPrivateKey = sKR.getSecretKey().extractPrivateKey(keyDecryptor);
+ PGPPublicKey masterPublicKey = uncachedKeyRing.mRing.getPublicKey();
+
+ // add packet with "pin" notation data
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ masterPrivateKey.getPublicKeyPacket().getAlgorithm(),
+ PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+ { // set subpackets
+ PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ hashedPacketsGen.setExportable(false, false);
+ hashedPacketsGen.setNotationData(false, true, "dummynotationdata", "some data");
+ sGen.setHashedSubpackets(hashedPacketsGen.generate());
+ }
+ sGen.init(PGPSignature.DIRECT_KEY, masterPrivateKey);
+ PGPSignature emptySig = sGen.generateCertification(masterPublicKey);
+
+ masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, emptySig);
+ sKR = PGPSecretKeyRing.insertSecretKey(sKR,
+ PGPSecretKey.replacePublicKey(sKR.getSecretKey(), masterPublicKey));
+
+ return new UncachedKeyRing(sKR);
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java
new file mode 100644
index 0000000..7c8295a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ApiDataAccessObject.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2014-2016 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import org.bouncycastle.bcpg.CompressionAlgorithmTags;
+import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
+import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
+import org.sufficientlysecure.keychain.remote.AccountSettings;
+import org.sufficientlysecure.keychain.remote.AppSettings;
+
+
+public class ApiDataAccessObject {
+
+ private final SimpleContentResolverInterface mQueryInterface;
+
+ public ApiDataAccessObject(Context context) {
+ final ContentResolver contentResolver = context.getContentResolver();
+ mQueryInterface = new SimpleContentResolverInterface() {
+ @Override
+ public Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return contentResolver.query(contentUri, projection, selection, selectionArgs, sortOrder);
+ }
+
+ @Override
+ public Uri insert(Uri contentUri, ContentValues values) {
+ return contentResolver.insert(contentUri, values);
+ }
+
+ @Override
+ public int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs) {
+ return contentResolver.update(contentUri, values, where, selectionArgs);
+ }
+
+ @Override
+ public int delete(Uri contentUri, String where, String[] selectionArgs) {
+ return contentResolver.delete(contentUri, where, selectionArgs);
+ }
+ };
+ }
+
+ public ApiDataAccessObject(SimpleContentResolverInterface queryInterface) {
+ mQueryInterface = queryInterface;
+ }
+
+ public ArrayList<String> getRegisteredApiApps() {
+ Cursor cursor = mQueryInterface.query(ApiApps.CONTENT_URI, null, null, null, null);
+
+ ArrayList<String> packageNames = new ArrayList<>();
+ try {
+ if (cursor != null) {
+ int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME);
+ if (cursor.moveToFirst()) {
+ do {
+ packageNames.add(cursor.getString(packageNameCol));
+ } while (cursor.moveToNext());
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return packageNames;
+ }
+
+ private ContentValues contentValueForApiApps(AppSettings appSettings) {
+ ContentValues values = new ContentValues();
+ values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName());
+ values.put(ApiApps.PACKAGE_CERTIFICATE, appSettings.getPackageCertificate());
+ return values;
+ }
+
+ private ContentValues contentValueForApiAccounts(AccountSettings accSettings) {
+ ContentValues values = new ContentValues();
+ values.put(KeychainContract.ApiAccounts.ACCOUNT_NAME, accSettings.getAccountName());
+ values.put(KeychainContract.ApiAccounts.KEY_ID, accSettings.getKeyId());
+
+ // DEPRECATED and thus hardcoded
+ values.put(KeychainContract.ApiAccounts.COMPRESSION, CompressionAlgorithmTags.ZLIB);
+ values.put(KeychainContract.ApiAccounts.ENCRYPTION_ALGORITHM,
+ PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT);
+ values.put(KeychainContract.ApiAccounts.HASH_ALORITHM,
+ PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT);
+ return values;
+ }
+
+ public void insertApiApp(AppSettings appSettings) {
+ mQueryInterface.insert(ApiApps.CONTENT_URI,
+ contentValueForApiApps(appSettings));
+ }
+
+ public void insertApiAccount(Uri uri, AccountSettings accSettings) {
+ mQueryInterface.insert(uri, contentValueForApiAccounts(accSettings));
+ }
+
+ public void updateApiAccount(Uri uri, AccountSettings accSettings) {
+ if (mQueryInterface.update(uri, contentValueForApiAccounts(accSettings), null,
+ null) <= 0) {
+ throw new RuntimeException();
+ }
+ }
+
+ /**
+ * Must be an uri pointing to an account
+ */
+ public AppSettings getApiAppSettings(Uri uri) {
+ AppSettings settings = null;
+
+ Cursor cursor = mQueryInterface.query(uri, null, null, null, null);
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ settings = new AppSettings();
+ settings.setPackageName(cursor.getString(
+ cursor.getColumnIndex(ApiApps.PACKAGE_NAME)));
+ settings.setPackageCertificate(cursor.getBlob(
+ cursor.getColumnIndex(ApiApps.PACKAGE_CERTIFICATE)));
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return settings;
+ }
+
+ public AccountSettings getApiAccountSettings(Uri accountUri) {
+ AccountSettings settings = null;
+
+ Cursor cursor = mQueryInterface.query(accountUri, null, null, null, null);
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ settings = new AccountSettings();
+
+ settings.setAccountName(cursor.getString(
+ cursor.getColumnIndex(KeychainContract.ApiAccounts.ACCOUNT_NAME)));
+ settings.setKeyId(cursor.getLong(
+ cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID)));
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return settings;
+ }
+
+ public Set<Long> getAllKeyIdsForApp(Uri uri) {
+ Set<Long> keyIds = new HashSet<>();
+
+ Cursor cursor = mQueryInterface.query(uri, null, null, null, null);
+ try {
+ if (cursor != null) {
+ int keyIdColumn = cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID);
+ while (cursor.moveToNext()) {
+ keyIds.add(cursor.getLong(keyIdColumn));
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return keyIds;
+ }
+
+ public HashSet<Long> getAllowedKeyIdsForApp(Uri uri) {
+ HashSet<Long> keyIds = new HashSet<>();
+
+ Cursor cursor = mQueryInterface.query(uri, null, null, null, null);
+ try {
+ if (cursor != null) {
+ int keyIdColumn = cursor.getColumnIndex(ApiAllowedKeys.KEY_ID);
+ while (cursor.moveToNext()) {
+ keyIds.add(cursor.getLong(keyIdColumn));
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return keyIds;
+ }
+
+ public void saveAllowedKeyIdsForApp(Uri uri, Set<Long> allowedKeyIds)
+ throws RemoteException, OperationApplicationException {
+ // wipe whole table of allowed keys for this account
+ mQueryInterface.delete(uri, null, null);
+
+ // re-insert allowed key ids
+ for (Long keyId : allowedKeyIds) {
+ ContentValues values = new ContentValues();
+ values.put(ApiAllowedKeys.KEY_ID, keyId);
+ mQueryInterface.insert(uri, values);
+ }
+ }
+
+ public void addAllowedKeyIdForApp(Uri uri, long allowedKeyId) {
+ ContentValues values = new ContentValues();
+ values.put(ApiAllowedKeys.KEY_ID, allowedKeyId);
+ mQueryInterface.insert(uri, values);
+ }
+
+ public byte[] getApiAppCertificate(String packageName) {
+ Uri queryUri = ApiApps.buildByPackageNameUri(packageName);
+
+ String[] projection = new String[]{ApiApps.PACKAGE_CERTIFICATE};
+
+ Cursor cursor = mQueryInterface.query(queryUri, projection, null, null, null);
+ try {
+ byte[] signature = null;
+ if (cursor != null && cursor.moveToFirst()) {
+ int signatureCol = 0;
+
+ signature = cursor.getBlob(signatureCol);
+ }
+ return signature;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
index 177f073..90a6955 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java
@@ -60,6 +60,9 @@ public class KeychainContract {
String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID
String TYPE = "type"; // not a database id
String USER_ID = "user_id"; // not a database id
+ String NAME = "name";
+ String EMAIL = "email";
+ String COMMENT = "comment";
String ATTRIBUTE_DATA = "attribute_data"; // not a database id
String RANK = "rank"; // ONLY used for sorting! no key, no nothing!
String IS_PRIMARY = "is_primary";
@@ -359,6 +362,9 @@ public class KeychainContract {
public static class Certs implements CertsColumns, BaseColumns {
public static final String USER_ID = UserPacketsColumns.USER_ID;
+ public static final String NAME = UserPacketsColumns.NAME;
+ public static final String EMAIL = UserPacketsColumns.EMAIL;
+ public static final String COMMENT = UserPacketsColumns.COMMENT;
public static final String SIGNER_UID = "signer_user_id";
public static final int UNVERIFIED = 0;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
index 752c130..0eb7a0c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
@@ -54,7 +54,7 @@ import java.io.IOException;
*/
public class KeychainDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "openkeychain.db";
- private static final int DATABASE_VERSION = 14;
+ private static final int DATABASE_VERSION = 17;
static Boolean apgHack = false;
private Context mContext;
@@ -115,6 +115,9 @@ public class KeychainDatabase extends SQLiteOpenHelper {
+ UserPacketsColumns.MASTER_KEY_ID + " INTEGER, "
+ UserPacketsColumns.TYPE + " INT, "
+ UserPacketsColumns.USER_ID + " TEXT, "
+ + UserPacketsColumns.NAME + " TEXT, "
+ + UserPacketsColumns.EMAIL + " TEXT, "
+ + UserPacketsColumns.COMMENT + " TEXT, "
+ UserPacketsColumns.ATTRIBUTE_DATA + " BLOB, "
+ UserPacketsColumns.IS_PRIMARY + " INTEGER, "
@@ -276,37 +279,45 @@ public class KeychainDatabase extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE user_ids ADD COLUMN type INTEGER");
db.execSQL("ALTER TABLE user_ids ADD COLUMN attribute_data BLOB");
case 7:
- // consolidate
- case 8:
// new table for allowed key ids in API
try {
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
} catch (Exception e) {
// never mind, the column probably already existed
}
- case 9:
+ case 8:
// tbale name for user_ids changed to user_packets
db.execSQL("DROP TABLE IF EXISTS certs");
db.execSQL("DROP TABLE IF EXISTS user_ids");
db.execSQL(CREATE_USER_PACKETS);
db.execSQL(CREATE_CERTS);
- case 10:
+ case 9:
// do nothing here, just consolidate
- case 11:
+ case 10:
// fix problems in database, see #1402 for details
// https://github.com/open-keychain/open-keychain/issues/1402
db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3");
- case 12:
+ case 11:
db.execSQL(CREATE_UPDATE_KEYS);
- case 13:
+ case 12:
// do nothing here, just consolidate
- case 14:
+ case 13:
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");");
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
+ UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");");
db.execSQL("CREATE INDEX verified_certs ON certs ("
+ CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");");
-
+ case 14:
+ db.execSQL("ALTER TABLE user_packets ADD COLUMN name TEXT");
+ db.execSQL("ALTER TABLE user_packets ADD COLUMN email TEXT");
+ db.execSQL("ALTER TABLE user_packets ADD COLUMN comment TEXT");
+ case 15:
+ db.execSQL("CREATE INDEX uids_by_name ON user_packets (name COLLATE NOCASE)");
+ db.execSQL("CREATE INDEX uids_by_email ON user_packets (email COLLATE NOCASE)");
+ if (oldVersion == 14) {
+ // no consolidate necessary
+ return;
+ }
}
// always do consolidate after upgrade
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java
new file mode 100644
index 0000000..a4d35f1
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainExternalContract.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import org.sufficientlysecure.keychain.Constants;
+
+
+public class KeychainExternalContract {
+
+ // this is in KeychainExternalContract already, but we want to be double
+ // sure this isn't mixed up with the internal one!
+ public static final String CONTENT_AUTHORITY_EXTERNAL = Constants.PROVIDER_AUTHORITY + ".exported";
+
+ private static final Uri BASE_CONTENT_URI_EXTERNAL = Uri
+ .parse("content://" + CONTENT_AUTHORITY_EXTERNAL);
+
+ public static final String BASE_EMAIL_STATUS = "email_status";
+
+ public static class EmailStatus implements BaseColumns {
+ public static final String EMAIL_ADDRESS = "email_address";
+ public static final String EMAIL_STATUS = "email_status";
+
+ public static final Uri CONTENT_URI = BASE_CONTENT_URI_EXTERNAL.buildUpon()
+ .appendPath(BASE_EMAIL_STATUS).build();
+
+ public static final String CONTENT_TYPE
+ = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.email_status";
+ }
+
+ private KeychainExternalContract() {
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
index 87b8a3b..8a5d09d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java
@@ -1,7 +1,7 @@
/*
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org>
- * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ * Copyright (C) 2014-2016 Vincent Breitmoser <v.breitmoser@mugenguild.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -308,13 +308,18 @@ public class KeychainProvider extends ContentProvider {
projectionMap.put(KeyRings.ALGORITHM, Tables.KEYS + "." + Keys.ALGORITHM);
projectionMap.put(KeyRings.FINGERPRINT, Tables.KEYS + "." + Keys.FINGERPRINT);
projectionMap.put(KeyRings.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID);
+ projectionMap.put(KeyRings.NAME, Tables.USER_PACKETS + "." + UserPackets.NAME);
+ projectionMap.put(KeyRings.EMAIL, Tables.USER_PACKETS + "." + UserPackets.EMAIL);
+ projectionMap.put(KeyRings.COMMENT, Tables.USER_PACKETS + "." + UserPackets.COMMENT);
projectionMap.put(KeyRings.HAS_DUPLICATE_USER_ID,
- "(EXISTS (SELECT * FROM " + Tables.USER_PACKETS + " AS dups"
+ "(EXISTS (SELECT * FROM " + Tables.USER_PACKETS + " AS dups"
+ " WHERE dups." + UserPackets.MASTER_KEY_ID
+ " != " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
+ " AND dups." + UserPackets.RANK + " = 0"
- + " AND dups." + UserPackets.USER_ID
- + " = "+ Tables.USER_PACKETS + "." + UserPackets.USER_ID
+ + " AND dups." + UserPackets.NAME
+ + " = " + Tables.USER_PACKETS + "." + UserPackets.NAME + " COLLATE NOCASE"
+ + " AND dups." + UserPackets.EMAIL
+ + " = " + Tables.USER_PACKETS + "." + UserPackets.EMAIL + " COLLATE NOCASE"
+ ")) AS " + KeyRings.HAS_DUPLICATE_USER_ID);
projectionMap.put(KeyRings.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED);
projectionMap.put(KeyRings.PUBKEY_DATA,
@@ -451,12 +456,12 @@ public class KeychainProvider extends ContentProvider {
if (i != 0) {
emailWhere += " OR ";
}
- emailWhere += "tmp." + UserPackets.USER_ID + " LIKE ";
- // match '*<email>', so it has to be at the *end* of the user id
if (match == KEY_RINGS_FIND_BY_EMAIL) {
- emailWhere += DatabaseUtils.sqlEscapeString("%<" + chunks[i] + ">");
+ emailWhere += "tmp." + UserPackets.EMAIL + " LIKE "
+ + DatabaseUtils.sqlEscapeString(chunks[i]);
} else {
- emailWhere += DatabaseUtils.sqlEscapeString("%" + chunks[i] + "%");
+ emailWhere += "tmp." + UserPackets.USER_ID + " LIKE "
+ + DatabaseUtils.sqlEscapeString("%" + chunks[i] + "%");
}
gotCondition = true;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
index 83e2baf..a0ebc69 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
@@ -18,6 +18,7 @@
package org.sufficientlysecure.keychain.provider;
+
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -29,19 +30,12 @@ import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.v4.util.LongSparseArray;
-import org.bouncycastle.bcpg.CompressionAlgorithmTags;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
-import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
-import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
-import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
-import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
-import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
-import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.ImportOperation;
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
+import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
@@ -51,24 +45,25 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
import org.sufficientlysecure.keychain.pgp.KeyRing;
-import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
import org.sufficientlysecure.keychain.pgp.WrappedSignature;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
-import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys;
-import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
+import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
-import org.sufficientlysecure.keychain.remote.AccountSettings;
-import org.sufficientlysecure.keychain.remote.AppSettings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableFileCache;
+import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
+import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.ProgressFixedScaler;
import org.sufficientlysecure.keychain.util.ProgressScaler;
import org.sufficientlysecure.keychain.util.Utf8Util;
@@ -80,10 +75,8 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
-import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
@@ -459,11 +452,13 @@ public class ProviderHelper {
mIndent += 1;
for (byte[] rawUserId : masterKey.getUnorderedRawUserIds()) {
String userId = Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(rawUserId);
-
UserPacketItem item = new UserPacketItem();
uids.add(item);
+ KeyRing.UserId splitUserId = KeyRing.splitUserId(userId);
item.userId = userId;
-
+ item.name = splitUserId.name;
+ item.email = splitUserId.email;
+ item.comment = splitUserId.comment;
int unknownCerts = 0;
log(LogType.MSG_IP_UID_PROCESSING, userId);
@@ -753,6 +748,9 @@ public class ProviderHelper {
private static class UserPacketItem implements Comparable<UserPacketItem> {
Integer type;
String userId;
+ String name;
+ String email;
+ String comment;
byte[] attributeData;
boolean isPrimary = false;
WrappedSignature selfCert;
@@ -1444,6 +1442,9 @@ public class ProviderHelper {
values.put(UserPackets.MASTER_KEY_ID, masterKeyId);
values.put(UserPackets.TYPE, item.type);
values.put(UserPackets.USER_ID, item.userId);
+ values.put(UserPackets.NAME, item.name);
+ values.put(UserPackets.EMAIL, item.email);
+ values.put(UserPackets.COMMENT, item.comment);
values.put(UserPackets.ATTRIBUTE_DATA, item.attributeData);
values.put(UserPackets.IS_PRIMARY, item.isPrimary);
values.put(UserPackets.IS_REVOKED, item.selfRevocation != null);
@@ -1481,191 +1482,6 @@ public class ProviderHelper {
return mContentResolver.insert(UpdatedKeys.CONTENT_URI, values);
}
- public ArrayList<String> getRegisteredApiApps() {
- Cursor cursor = mContentResolver.query(ApiApps.CONTENT_URI, null, null, null, null);
-
- ArrayList<String> packageNames = new ArrayList<>();
- try {
- if (cursor != null) {
- int packageNameCol = cursor.getColumnIndex(ApiApps.PACKAGE_NAME);
- if (cursor.moveToFirst()) {
- do {
- packageNames.add(cursor.getString(packageNameCol));
- } while (cursor.moveToNext());
- }
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- return packageNames;
- }
-
- private ContentValues contentValueForApiApps(AppSettings appSettings) {
- ContentValues values = new ContentValues();
- values.put(ApiApps.PACKAGE_NAME, appSettings.getPackageName());
- values.put(ApiApps.PACKAGE_CERTIFICATE, appSettings.getPackageCertificate());
- return values;
- }
-
- private ContentValues contentValueForApiAccounts(AccountSettings accSettings) {
- ContentValues values = new ContentValues();
- values.put(KeychainContract.ApiAccounts.ACCOUNT_NAME, accSettings.getAccountName());
- values.put(KeychainContract.ApiAccounts.KEY_ID, accSettings.getKeyId());
-
- // DEPRECATED and thus hardcoded
- values.put(KeychainContract.ApiAccounts.COMPRESSION, CompressionAlgorithmTags.ZLIB);
- values.put(KeychainContract.ApiAccounts.ENCRYPTION_ALGORITHM,
- PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT);
- values.put(KeychainContract.ApiAccounts.HASH_ALORITHM,
- PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT);
- return values;
- }
-
- public void insertApiApp(AppSettings appSettings) {
- mContentResolver.insert(KeychainContract.ApiApps.CONTENT_URI,
- contentValueForApiApps(appSettings));
- }
-
- public void insertApiAccount(Uri uri, AccountSettings accSettings) {
- mContentResolver.insert(uri, contentValueForApiAccounts(accSettings));
- }
-
- public void updateApiAccount(Uri uri, AccountSettings accSettings) {
- if (mContentResolver.update(uri, contentValueForApiAccounts(accSettings), null,
- null) <= 0) {
- throw new RuntimeException();
- }
- }
-
- /**
- * Must be an uri pointing to an account
- */
- public AppSettings getApiAppSettings(Uri uri) {
- AppSettings settings = null;
-
- Cursor cursor = mContentResolver.query(uri, null, null, null, null);
- try {
- if (cursor != null && cursor.moveToFirst()) {
- settings = new AppSettings();
- settings.setPackageName(cursor.getString(
- cursor.getColumnIndex(KeychainContract.ApiApps.PACKAGE_NAME)));
- settings.setPackageCertificate(cursor.getBlob(
- cursor.getColumnIndex(KeychainContract.ApiApps.PACKAGE_CERTIFICATE)));
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- return settings;
- }
-
- public AccountSettings getApiAccountSettings(Uri accountUri) {
- AccountSettings settings = null;
-
- Cursor cursor = mContentResolver.query(accountUri, null, null, null, null);
- try {
- if (cursor != null && cursor.moveToFirst()) {
- settings = new AccountSettings();
-
- settings.setAccountName(cursor.getString(
- cursor.getColumnIndex(KeychainContract.ApiAccounts.ACCOUNT_NAME)));
- settings.setKeyId(cursor.getLong(
- cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID)));
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- return settings;
- }
-
- public Set<Long> getAllKeyIdsForApp(Uri uri) {
- Set<Long> keyIds = new HashSet<>();
-
- Cursor cursor = mContentResolver.query(uri, null, null, null, null);
- try {
- if (cursor != null) {
- int keyIdColumn = cursor.getColumnIndex(KeychainContract.ApiAccounts.KEY_ID);
- while (cursor.moveToNext()) {
- keyIds.add(cursor.getLong(keyIdColumn));
- }
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- return keyIds;
- }
-
- public HashSet<Long> getAllowedKeyIdsForApp(Uri uri) {
- HashSet<Long> keyIds = new HashSet<>();
-
- Cursor cursor = mContentResolver.query(uri, null, null, null, null);
- try {
- if (cursor != null) {
- int keyIdColumn = cursor.getColumnIndex(KeychainContract.ApiAllowedKeys.KEY_ID);
- while (cursor.moveToNext()) {
- keyIds.add(cursor.getLong(keyIdColumn));
- }
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- return keyIds;
- }
-
- public void saveAllowedKeyIdsForApp(Uri uri, Set<Long> allowedKeyIds)
- throws RemoteException, OperationApplicationException {
- // wipe whole table of allowed keys for this account
- mContentResolver.delete(uri, null, null);
-
- // re-insert allowed key ids
- for (Long keyId : allowedKeyIds) {
- ContentValues values = new ContentValues();
- values.put(ApiAllowedKeys.KEY_ID, keyId);
- mContentResolver.insert(uri, values);
- }
- }
-
- public void addAllowedKeyIdForApp(Uri uri, long allowedKeyId) {
- ContentValues values = new ContentValues();
- values.put(ApiAllowedKeys.KEY_ID, allowedKeyId);
- mContentResolver.insert(uri, values);
- }
-
- public byte[] getApiAppCertificate(String packageName) {
- Uri queryUri = ApiApps.buildByPackageNameUri(packageName);
-
- String[] projection = new String[]{ApiApps.PACKAGE_CERTIFICATE};
-
- Cursor cursor = mContentResolver.query(queryUri, projection, null, null, null);
- try {
- byte[] signature = null;
- if (cursor != null && cursor.moveToFirst()) {
- int signatureCol = 0;
-
- signature = cursor.getBlob(signatureCol);
- }
- return signature;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
-
public ContentResolver getContentResolver() {
return mContentResolver;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/SimpleContentResolverInterface.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/SimpleContentResolverInterface.java
new file mode 100644
index 0000000..0e4d76a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/SimpleContentResolverInterface.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+/** This interface contains the principal methods for database access
+ * from {#android.content.ContentResolver}. It is used to allow substitution
+ * of a ContentResolver in DAOs.
+ *
+ * @see ApiDataAccessObject
+ */
+public interface SimpleContentResolverInterface {
+ Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs, String sortOrder);
+
+ Uri insert(Uri contentUri, ContentValues values);
+
+ int update(Uri contentUri, ContentValues values, String where, String[] selectionArgs);
+
+ int delete(Uri contentUri, String where, String[] selectionArgs);
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java
index 68963d5..bb44314 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java
@@ -99,6 +99,12 @@ public class TemporaryFileProvider extends ContentProvider {
return context.getContentResolver().insert(CONTENT_URI, contentValues);
}
+ public static int setName(Context context, Uri uri, String name) {
+ ContentValues values = new ContentValues();
+ values.put(TemporaryFileColumns.COLUMN_NAME, name);
+ return context.getContentResolver().update(uri, values, null, null);
+ }
+
public static int setMimeType(Context context, Uri uri, String mimetype) {
ContentValues values = new ContentValues();
values.put(TemporaryFileColumns.COLUMN_TYPE, mimetype);
@@ -283,8 +289,11 @@ public class TemporaryFileProvider extends ContentProvider {
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- if (values.size() != 1 || !values.containsKey(TemporaryFileColumns.COLUMN_TYPE)) {
- throw new UnsupportedOperationException("Update supported only for type field!");
+ if (values.size() != 1) {
+ throw new UnsupportedOperationException("Update supported only for one field at a time!");
+ }
+ if (!values.containsKey(TemporaryFileColumns.COLUMN_NAME) && !values.containsKey(TemporaryFileColumns.COLUMN_TYPE)) {
+ throw new UnsupportedOperationException("Update supported only for name and type field!");
}
if (selection != null || selectionArgs != null) {
throw new UnsupportedOperationException("Update supported only for plain uri!");
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/receiver/NetworkReceiver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/receiver/NetworkReceiver.java
new file mode 100644
index 0000000..7c103a9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/receiver/NetworkReceiver.java
@@ -0,0 +1,52 @@
+package org.sufficientlysecure.keychain.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class NetworkReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ ConnectivityManager conn = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = conn.getActiveNetworkInfo();
+ boolean isTypeWifi = (networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
+ boolean isConnected = networkInfo.isConnected();
+
+ if (isTypeWifi && isConnected) {
+
+ // broadcaster receiver disabled
+ setWifiReceiverComponent(false, context);
+ Intent serviceIntent = new Intent(context, KeyserverSyncAdapterService.class);
+ serviceIntent.setAction(KeyserverSyncAdapterService.ACTION_SYNC_NOW);
+ context.startService(serviceIntent);
+ }
+ }
+
+ public void setWifiReceiverComponent(Boolean isEnabled, Context context) {
+
+ PackageManager pm = context.getPackageManager();
+ ComponentName compName = new ComponentName(context,
+ NetworkReceiver.class);
+
+ if (isEnabled) {
+ pm.setComponentEnabledSetting(compName,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
+ Log.d(Constants.TAG, "Wifi Receiver is enabled!");
+ } else {
+ pm.setComponentEnabledSetting(compName,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
+ Log.d(Constants.TAG, "Wifi Receiver is disabled!");
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java
index 7c05edb..4107167 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPendingIntentFactory.java
@@ -51,10 +51,10 @@ public class ApiPendingIntentFactory {
CryptoInputParcel cryptoInput) {
switch (requiredInput.mType) {
- case NFC_MOVE_KEY_TO_CARD:
- case NFC_DECRYPT:
- case NFC_SIGN: {
- return createNfcOperationPendingIntent(data, requiredInput, cryptoInput);
+ case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
+ case SECURITY_TOKEN_DECRYPT:
+ case SECURITY_TOKEN_SIGN: {
+ return createSecurityTokenOperationPendingIntent(data, requiredInput, cryptoInput);
}
case PASSPHRASE: {
@@ -66,7 +66,7 @@ public class ApiPendingIntentFactory {
}
}
- private PendingIntent createNfcOperationPendingIntent(Intent data, RequiredInputParcel requiredInput, CryptoInputParcel cryptoInput) {
+ private PendingIntent createSecurityTokenOperationPendingIntent(Intent data, RequiredInputParcel requiredInput, CryptoInputParcel cryptoInput) {
Intent intent = new Intent(mContext, RemoteSecurityTokenOperationActivity.class);
// pass params through to activity that it can be returned again later to repeat pgp operation
intent.putExtra(RemoteSecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java
index 7edd8b2..47ecdb2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ApiPermissionHelper.java
@@ -18,6 +18,10 @@
package org.sufficientlysecure.keychain.remote;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
@@ -33,15 +37,10 @@ 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.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract;
-import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.util.Log;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-
/**
* Abstract service class for remote APIs that handle app registration and user input.
@@ -49,13 +48,13 @@ import java.util.Arrays;
public class ApiPermissionHelper {
private final Context mContext;
- private final ProviderHelper mProviderHelper;
+ private final ApiDataAccessObject mApiDao;
private PackageManager mPackageManager;
- public ApiPermissionHelper(Context context) {
+ public ApiPermissionHelper(Context context, ApiDataAccessObject apiDao) {
mContext = context;
mPackageManager = context.getPackageManager();
- mProviderHelper = new ProviderHelper(context);
+ mApiDao = apiDao;
}
public static class WrongPackageCertificateException extends Exception {
@@ -66,14 +65,24 @@ public class ApiPermissionHelper {
}
}
+ /** Returns true iff the caller is allowed, or false on any type of problem.
+ * This method should only be used in cases where error handling is dealt with separately.
+ */
+ protected boolean isAllowedIgnoreErrors() {
+ try {
+ return isCallerAllowed();
+ } catch (WrongPackageCertificateException e) {
+ return false;
+ }
+ }
+
/**
* Checks if caller is allowed to access the API
*
* @return null if caller is allowed, or a Bundle with a PendingIntent
*/
- protected Intent isAllowed(Intent data) {
+ protected Intent isAllowedOrReturnIntent(Intent data) {
ApiPendingIntentFactory piFactory = new ApiPendingIntentFactory(mContext);
-
try {
if (isCallerAllowed()) {
return null;
@@ -168,7 +177,7 @@ public class ApiPermissionHelper {
Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(currentPkg, accountName);
- return mProviderHelper.getApiAccountSettings(uri); // can be null!
+ return mApiDao.getApiAccountSettings(uri); // can be null!
}
@Deprecated
@@ -224,35 +233,29 @@ public class ApiPermissionHelper {
private boolean isPackageAllowed(String packageName) throws WrongPackageCertificateException {
Log.d(Constants.TAG, "isPackageAllowed packageName: " + packageName);
- ArrayList<String> allowedPkgs = mProviderHelper.getRegisteredApiApps();
- Log.d(Constants.TAG, "allowed: " + allowedPkgs);
+ byte[] storedPackageCert = mApiDao.getApiAppCertificate(packageName);
- // check if package is allowed to use our service
- if (allowedPkgs.contains(packageName)) {
- Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName);
+ boolean isKnownPackage = storedPackageCert != null;
+ if (!isKnownPackage) {
+ Log.d(Constants.TAG, "Package is NOT allowed! packageName: " + packageName);
+ return false;
+ }
+ Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName);
- // check package signature
- byte[] currentCert;
- try {
- currentCert = getPackageCertificate(packageName);
- } catch (NameNotFoundException e) {
- throw new WrongPackageCertificateException(e.getMessage());
- }
+ byte[] currentPackageCert;
+ try {
+ currentPackageCert = getPackageCertificate(packageName);
+ } catch (NameNotFoundException e) {
+ throw new WrongPackageCertificateException(e.getMessage());
+ }
- byte[] storedCert = mProviderHelper.getApiAppCertificate(packageName);
- if (Arrays.equals(currentCert, storedCert)) {
- Log.d(Constants.TAG,
- "Package certificate is correct! (equals certificate from database)");
- return true;
- } else {
- throw new WrongPackageCertificateException(
- "PACKAGE NOT ALLOWED! Certificate wrong! (Certificate not " +
- "equals certificate from database)");
- }
+ boolean packageCertMatchesStored = Arrays.equals(currentPackageCert, storedPackageCert);
+ if (packageCertMatchesStored) {
+ Log.d(Constants.TAG,"Package certificate matches expected.");
+ return true;
}
- Log.d(Constants.TAG, "Package is NOT allowed! packageName: " + packageName);
- return false;
+ throw new WrongPackageCertificateException("PACKAGE NOT ALLOWED DUE TO CERTIFICATE MISMATCH!");
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java
new file mode 100644
index 0000000..455857f
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/KeychainExternalProvider.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2016 Vincent Breitmoser <look@my.amazin.horse>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.remote;
+
+
+import java.security.AccessControlException;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Binder;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.provider.KeychainExternalContract;
+import org.sufficientlysecure.keychain.provider.KeychainExternalContract.EmailStatus;
+import org.sufficientlysecure.keychain.provider.SimpleContentResolverInterface;
+import org.sufficientlysecure.keychain.util.Log;
+
+
+public class KeychainExternalProvider extends ContentProvider implements SimpleContentResolverInterface {
+ private static final int EMAIL_STATUS = 101;
+ private static final int API_APPS = 301;
+ private static final int API_APPS_BY_PACKAGE_NAME = 302;
+
+
+ private UriMatcher mUriMatcher;
+ private ApiPermissionHelper mApiPermissionHelper;
+
+
+ /**
+ * 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 = KeychainExternalContract.CONTENT_AUTHORITY_EXTERNAL;
+
+ /**
+ * list email_status
+ *
+ * <pre>
+ * email_status/
+ * </pre>
+ */
+ matcher.addURI(authority, KeychainExternalContract.BASE_EMAIL_STATUS, EMAIL_STATUS);
+
+ matcher.addURI(KeychainContract.CONTENT_AUTHORITY, KeychainContract.BASE_API_APPS, API_APPS);
+ matcher.addURI(KeychainContract.CONTENT_AUTHORITY, KeychainContract.BASE_API_APPS + "/*", API_APPS_BY_PACKAGE_NAME);
+
+ return matcher;
+ }
+
+ private KeychainDatabase mKeychainDatabase;
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onCreate() {
+ mUriMatcher = buildUriMatcher();
+ mApiPermissionHelper = new ApiPermissionHelper(getContext(), new ApiDataAccessObject(this));
+ return true;
+ }
+
+ public KeychainDatabase getDb() {
+ if(mKeychainDatabase == null)
+ mKeychainDatabase = new KeychainDatabase(getContext());
+ return mKeychainDatabase;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getType(@NonNull Uri uri) {
+ final int match = mUriMatcher.match(uri);
+ switch (match) {
+ case EMAIL_STATUS:
+ return EmailStatus.CONTENT_TYPE;
+
+ case API_APPS:
+ return ApiApps.CONTENT_TYPE;
+
+ case API_APPS_BY_PACKAGE_NAME:
+ return ApiApps.CONTENT_ITEM_TYPE;
+
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Cursor query(@NonNull 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);
+
+ String groupBy = null;
+
+ switch (match) {
+ case EMAIL_STATUS: {
+ boolean callerIsAllowed = mApiPermissionHelper.isAllowedIgnoreErrors();
+ if (!callerIsAllowed) {
+ throw new AccessControlException("An application must register before use of KeychainExternalProvider!");
+ }
+
+ HashMap<String, String> projectionMap = new HashMap<>();
+ projectionMap.put(EmailStatus._ID, "email AS _id");
+ projectionMap.put(EmailStatus.EMAIL_ADDRESS,
+ Tables.USER_PACKETS + "." + UserPackets.USER_ID + " AS " + EmailStatus.EMAIL_ADDRESS);
+ // we take the minimum (>0) here, where "1" is "verified by known secret key", "2" is "self-certified"
+ projectionMap.put(EmailStatus.EMAIL_STATUS, "CASE ( MIN (" + Certs.VERIFIED + " ) ) "
+ // remap to keep this provider contract independent from our internal representation
+ + " WHEN " + Certs.VERIFIED_SELF + " THEN 1"
+ + " WHEN " + Certs.VERIFIED_SECRET + " THEN 2"
+ + " END AS " + EmailStatus.EMAIL_STATUS);
+ qb.setProjectionMap(projectionMap);
+
+ if (projection == null) {
+ throw new IllegalArgumentException("Please provide a projection!");
+ }
+
+ qb.setTables(
+ Tables.USER_PACKETS
+ + " INNER JOIN " + Tables.CERTS + " ON ("
+ + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = "
+ + Tables.CERTS + "." + Certs.MASTER_KEY_ID
+ + " AND " + Tables.USER_PACKETS + "." + UserPackets.RANK + " = "
+ + Tables.CERTS + "." + Certs.RANK
+ // verified == 0 has no self-cert, which is basically an error case. never return that!
+ + " AND " + Tables.CERTS + "." + Certs.VERIFIED + " > 0"
+ + ")"
+ );
+ qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.USER_ID + " IS NOT NULL");
+ // in case there are multiple verifying certificates
+ groupBy = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + ", "
+ + Tables.USER_PACKETS + "." + UserPackets.USER_ID;
+
+ if (TextUtils.isEmpty(sortOrder)) {
+ sortOrder = EmailStatus.EMAIL_ADDRESS + " ASC, " + EmailStatus.EMAIL_STATUS + " DESC";
+ }
+
+ // uri to watch is all /key_rings/
+ uri = KeyRings.CONTENT_URI;
+
+ boolean gotCondition = false;
+ String emailWhere = "";
+ // JAVA ♥
+ for (int i = 0; i < selectionArgs.length; ++i) {
+ if (selectionArgs[i].length() == 0) {
+ continue;
+ }
+ if (i != 0) {
+ emailWhere += " OR ";
+ }
+ emailWhere += UserPackets.USER_ID + " LIKE ";
+ // match '*<email>', so it has to be at the *end* of the user id
+ emailWhere += DatabaseUtils.sqlEscapeString("%<" + selectionArgs[i] + ">");
+ gotCondition = true;
+ }
+
+ if (gotCondition) {
+ qb.appendWhere(" AND (" + emailWhere + ")");
+ } else {
+ // TODO better way to do this?
+ Log.e(Constants.TAG, "Malformed find by email query!");
+ qb.appendWhere(" AND 0");
+ }
+
+ break;
+ }
+
+ case API_APPS_BY_PACKAGE_NAME: {
+ String requestedPackageName = uri.getLastPathSegment();
+ checkIfPackageBelongsToCaller(getContext(), requestedPackageName);
+
+ qb.setTables(Tables.API_APPS);
+ qb.appendWhere(ApiApps.PACKAGE_NAME + " = ");
+ qb.appendWhereEscapeString(requestedPackageName);
+
+ 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 cursor = qb.query(db, projection, selection, null, groupBy, null, orderBy);
+ if (cursor != null) {
+ // Tell the cursor what uri to watch, so it knows when its source data changes
+ cursor.setNotificationUri(getContext().getContentResolver(), uri);
+ }
+
+ Log.d(Constants.TAG,
+ "Query: " + qb.buildQuery(projection, selection, null, null, orderBy, null));
+
+ return cursor;
+ }
+
+ private void checkIfPackageBelongsToCaller(Context context, String requestedPackageName) {
+ int callerUid = Binder.getCallingUid();
+ String[] callerPackageNames = context.getPackageManager().getPackagesForUid(callerUid);
+ if (callerPackageNames == null) {
+ throw new IllegalStateException("Failed to retrieve caller package name, this is an error!");
+ }
+
+ boolean packageBelongsToCaller = false;
+ for (String p : callerPackageNames) {
+ if (p.equals(requestedPackageName)) {
+ packageBelongsToCaller = true;
+ break;
+ }
+ }
+ if (!packageBelongsToCaller) {
+ throw new SecurityException("ExternalProvider may only check status of caller package!");
+ }
+ }
+
+ @Override
+ public Uri insert(@NonNull Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, String additionalSelection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
index a5dc2a0..88cd066 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2016 Vincent Breitmoser <look@my.amazin.horse>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,11 +18,24 @@
package org.sufficientlysecure.keychain.remote;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
@@ -43,12 +57,15 @@ import org.sufficientlysecure.keychain.operations.results.ExportResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel;
import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
+import org.sufficientlysecure.keychain.pgp.KeyRing;
+import org.sufficientlysecure.keychain.pgp.KeyRing.UserId;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel;
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
+import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
@@ -61,18 +78,12 @@ import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Passphrase;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-
public class OpenPgpService extends Service {
- static final String[] EMAIL_SEARCH_PROJECTION = new String[]{
+ public static final List<Integer> SUPPORTED_VERSIONS =
+ Collections.unmodifiableList(Arrays.asList(3, 4, 5, 6, 7, 8, 9, 10, 11));
+
+ static final String[] KEY_SEARCH_PROJECTION = new String[]{
KeyRings._ID,
KeyRings.MASTER_KEY_ID,
KeyRings.IS_EXPIRED,
@@ -80,35 +91,50 @@ public class OpenPgpService extends Service {
};
// do not pre-select revoked or expired keys
- static final String EMAIL_SEARCH_WHERE = Tables.KEYS + "." + KeychainContract.KeyRings.IS_REVOKED
+ static final String KEY_SEARCH_WHERE = Tables.KEYS + "." + KeychainContract.KeyRings.IS_REVOKED
+ " = 0 AND " + KeychainContract.KeyRings.IS_EXPIRED + " = 0";
private ApiPermissionHelper mApiPermissionHelper;
private ProviderHelper mProviderHelper;
+ private ApiDataAccessObject mApiDao;
@Override
public void onCreate() {
super.onCreate();
- mApiPermissionHelper = new ApiPermissionHelper(this);
+ mApiPermissionHelper = new ApiPermissionHelper(this, new ApiDataAccessObject(this));
mProviderHelper = new ProviderHelper(this);
+ mApiDao = new ApiDataAccessObject(this);
}
- /**
- * Search database for key ids based on emails.
- */
- private Intent returnKeyIdsFromEmails(Intent data, String[] encryptionUserIds) {
+ private static class KeyIdResult {
+ final Intent mResultIntent;
+ final HashSet<Long> mKeyIds;
+
+ KeyIdResult(Intent resultIntent) {
+ mResultIntent = resultIntent;
+ mKeyIds = null;
+ }
+ KeyIdResult(HashSet<Long> keyIds) {
+ mResultIntent = null;
+ mKeyIds = keyIds;
+ }
+ }
+
+ private KeyIdResult returnKeyIdsFromEmails(Intent data, String[] encryptionUserIds, boolean isOpportunistic) {
boolean noUserIdsCheck = (encryptionUserIds == null || encryptionUserIds.length == 0);
boolean missingUserIdsCheck = false;
boolean duplicateUserIdsCheck = false;
- ArrayList<Long> keyIds = new ArrayList<>();
+ HashSet<Long> keyIds = new HashSet<>();
ArrayList<String> missingEmails = new ArrayList<>();
ArrayList<String> duplicateEmails = new ArrayList<>();
if (!noUserIdsCheck) {
- for (String email : encryptionUserIds) {
+ for (String rawUserId : encryptionUserIds) {
+ UserId userId = KeyRing.splitUserId(rawUserId);
+ String email = userId.email != null ? userId.email : rawUserId;
// try to find the key for this specific email
Uri uri = KeyRings.buildUnifiedKeyRingsFindByEmailUri(email);
- Cursor cursor = getContentResolver().query(uri, EMAIL_SEARCH_PROJECTION, EMAIL_SEARCH_WHERE, null, null);
+ Cursor cursor = getContentResolver().query(uri, KEY_SEARCH_PROJECTION, KEY_SEARCH_WHERE, null, null);
try {
// result should be one entry containing the key id
if (cursor != null && cursor.moveToFirst()) {
@@ -137,15 +163,17 @@ public class OpenPgpService extends Service {
}
}
- // convert ArrayList<Long> to long[]
- long[] keyIdsArray = new long[keyIds.size()];
- for (int i = 0; i < keyIdsArray.length; i++) {
- keyIdsArray[i] = keyIds.get(i);
+ if (isOpportunistic && (noUserIdsCheck || missingUserIdsCheck)) {
+ Intent result = new Intent();
+ result.putExtra(OpenPgpApi.RESULT_ERROR,
+ new OpenPgpError(OpenPgpError.OPPORTUNISTIC_MISSING_KEYS, "missing keys in opportunistic mode"));
+ result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
+ return new KeyIdResult(result);
}
if (noUserIdsCheck || missingUserIdsCheck || duplicateUserIdsCheck) {
- // allow the user to verify pub key selection
-
+ // convert ArrayList<Long> to long[]
+ long[] keyIdsArray = getUnboxedLongArray(keyIds);
ApiPendingIntentFactory piFactory = new ApiPendingIntentFactory(getBaseContext());
PendingIntent pi = piFactory.createSelectPublicKeyPendingIntent(data, keyIdsArray,
missingEmails, duplicateEmails, noUserIdsCheck);
@@ -154,19 +182,15 @@ public class OpenPgpService extends Service {
Intent result = new Intent();
result.putExtra(OpenPgpApi.RESULT_INTENT, pi);
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
- return result;
- } else {
- // everything was easy, we have exactly one key for every email
-
- if (keyIdsArray.length == 0) {
- Log.e(Constants.TAG, "keyIdsArray.length == 0, should never happen!");
- }
+ return new KeyIdResult(result);
+ }
- Intent result = new Intent();
- result.putExtra(OpenPgpApi.RESULT_KEY_IDS, keyIdsArray);
- result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
- return result;
+ // everything was easy, we have exactly one key for every email
+ if (keyIds.isEmpty()) {
+ Log.e(Constants.TAG, "keyIdsArray.length == 0, should never happen!");
}
+
+ return new KeyIdResult(keyIds);
}
private Intent signImpl(Intent data, InputStream inputStream,
@@ -280,20 +304,31 @@ public class OpenPgpService extends Service {
compressionId = PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.UNCOMPRESSED;
}
- // first try to get key ids from non-ambiguous key id extra
- long[] keyIds = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS);
- if (keyIds == null) {
+ long[] keyIds;
+ {
+ HashSet<Long> encryptKeyIds = new HashSet<>();
+
// get key ids based on given user ids
- String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS);
- // give params through to activity...
- Intent result = returnKeyIdsFromEmails(data, userIds);
+ if (data.hasExtra(OpenPgpApi.EXTRA_USER_IDS)) {
+ String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS);
+ boolean isOpportunistic = data.getBooleanExtra(OpenPgpApi.EXTRA_OPPORTUNISTIC_ENCRYPTION, false);
+ // give params through to activity...
+ KeyIdResult result = returnKeyIdsFromEmails(data, userIds, isOpportunistic);
+
+ if (result.mResultIntent != null) {
+ return result.mResultIntent;
+ }
+ encryptKeyIds.addAll(result.mKeyIds);
+ }
- if (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0) == OpenPgpApi.RESULT_CODE_SUCCESS) {
- keyIds = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS);
- } else {
- // if not success -> result contains a PendingIntent for user interaction
- return result;
+ // add key ids from non-ambiguous key id extra
+ if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) {
+ for (long keyId : data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)) {
+ encryptKeyIds.add(keyId);
+ }
}
+
+ keyIds = getUnboxedLongArray(encryptKeyIds);
}
// TODO this is not correct!
@@ -305,8 +340,7 @@ public class OpenPgpService extends Service {
.setVersionHeader(null)
.setCompressionAlgorithm(compressionId)
.setSymmetricEncryptionAlgorithm(PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT)
- .setEncryptionMasterKeyIds(keyIds)
- .setFailOnMissingEncryptionKeyIds(true);
+ .setEncryptionMasterKeyIds(keyIds);
if (sign) {
@@ -405,11 +439,11 @@ public class OpenPgpService extends Service {
}
String currentPkg = mApiPermissionHelper.getCurrentCallingPackage();
- HashSet<Long> allowedKeyIds = mProviderHelper.getAllowedKeyIdsForApp(
+ HashSet<Long> allowedKeyIds = mApiDao.getAllowedKeyIdsForApp(
KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg));
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 7) {
- allowedKeyIds.addAll(mProviderHelper.getAllKeyIdsForApp(
+ allowedKeyIds.addAll(mApiDao.getAllKeyIdsForApp(
ApiAccounts.buildBaseUri(currentPkg)));
}
@@ -422,6 +456,15 @@ public class OpenPgpService extends Service {
cryptoInput.mPassphrase =
new Passphrase(data.getCharArrayExtra(OpenPgpApi.EXTRA_PASSPHRASE));
}
+ if (data.hasExtra(OpenPgpApi.EXTRA_DECRYPTION_RESULT_WRAPPER)) {
+ // this is wrapped in a Bundle to avoid ClassLoader problems
+ Bundle wrapperBundle = data.getBundleExtra(OpenPgpApi.EXTRA_DECRYPTION_RESULT_WRAPPER);
+ wrapperBundle.setClassLoader(getClassLoader());
+ OpenPgpDecryptionResult decryptionResult = wrapperBundle.getParcelable(OpenPgpApi.EXTRA_DECRYPTION_RESULT);
+ if (decryptionResult != null && decryptionResult.hasDecryptedSessionKey()) {
+ cryptoInput.addCryptoData(decryptionResult.sessionKey, decryptionResult.decryptedSessionKey);
+ }
+ }
byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE);
@@ -582,7 +625,8 @@ public class OpenPgpService extends Service {
try {
// try to find key, throws NotFoundException if not in db!
CanonicalizedPublicKeyRing keyRing =
- mProviderHelper.getCanonicalizedPublicKeyRing(masterKeyId);
+ mProviderHelper.getCanonicalizedPublicKeyRing(
+ KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(masterKeyId));
Intent result = new Intent();
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
@@ -669,7 +713,21 @@ public class OpenPgpService extends Service {
} else {
// get key ids based on given user ids
String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS);
- return returnKeyIdsFromEmails(data, userIds);
+ KeyIdResult keyResult = returnKeyIdsFromEmails(data, userIds, false);
+ if (keyResult.mResultIntent != null) {
+ return keyResult.mResultIntent;
+ }
+
+ if (keyResult.mKeyIds == null) {
+ throw new AssertionError("one of requiredUserInteraction and keyIds must be non-null, this is a bug!");
+ }
+
+ long[] keyIds = getUnboxedLongArray(keyResult.mKeyIds);
+
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
+ resultIntent.putExtra(OpenPgpApi.RESULT_KEY_IDS, keyIds);
+ return resultIntent;
}
}
@@ -716,6 +774,26 @@ public class OpenPgpService extends Service {
}
}
+ @NonNull
+ private static long[] getUnboxedLongArray(@NonNull Collection<Long> arrayList) {
+ long[] result = new long[arrayList.size()];
+ int i = 0;
+ for (Long e : arrayList) {
+ result[i++] = e;
+ }
+ return result;
+ }
+
+ private Intent checkPermissionImpl(@NonNull Intent data) {
+ Intent permissionIntent = mApiPermissionHelper.isAllowedOrReturnIntent(data);
+ if (permissionIntent != null) {
+ return permissionIntent;
+ }
+ Intent result = new Intent();
+ result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
+ return result;
+ }
+
private Intent getSignKeyMasterId(Intent data) {
// NOTE: Accounts are deprecated on API version >= 7
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 7) {
@@ -765,20 +843,19 @@ public class OpenPgpService extends Service {
// version code is required and needs to correspond to version code of service!
// History of versions in openpgp-api's CHANGELOG.md
- List<Integer> supportedVersions = Arrays.asList(3, 4, 5, 6, 7, 8, 9, 10);
- if (!supportedVersions.contains(data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1))) {
+ if (!SUPPORTED_VERSIONS.contains(data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1))) {
Intent result = new Intent();
OpenPgpError error = new OpenPgpError
(OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n"
+ "used API version: " + data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) + "\n"
- + "supported API versions: " + supportedVersions);
+ + "supported API versions: " + SUPPORTED_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 OpenKeychain
- Intent result = mApiPermissionHelper.isAllowed(data);
+ Intent result = mApiPermissionHelper.isAllowedOrReturnIntent(data);
if (result != null) {
return result;
}
@@ -845,6 +922,9 @@ public class OpenPgpService extends Service {
String action = data.getAction();
switch (action) {
+ case OpenPgpApi.ACTION_CHECK_PERMISSION: {
+ return checkPermissionImpl(data);
+ }
case OpenPgpApi.ACTION_CLEARTEXT_SIGN: {
return signImpl(data, inputStream, outputStream, true);
}
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
index e19757d..27700b5 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AccountSettingsActivity.java
@@ -29,7 +29,7 @@ import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.SingletonResult;
-import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.remote.AccountSettings;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.util.Log;
@@ -98,7 +98,7 @@ public class AccountSettingsActivity extends BaseActivity {
}
private void loadData(Uri accountUri) {
- AccountSettings settings = new ProviderHelper(this).getApiAccountSettings(accountUri);
+ AccountSettings settings = new ApiDataAccessObject(this).getApiAccountSettings(accountUri);
mAccountSettingsFragment.setAccSettings(settings);
}
@@ -110,7 +110,7 @@ public class AccountSettingsActivity extends BaseActivity {
}
private void save() {
- new ProviderHelper(this).updateApiAccount(mAccountUri, mAccountSettingsFragment.getAccSettings());
+ new ApiDataAccessObject(this).updateApiAccount(mAccountUri, mAccountSettingsFragment.getAccSettings());
SingletonResult result = new SingletonResult(
SingletonResult.RESULT_OK, LogType.MSG_ACC_SAVED);
Intent intent = new Intent();
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java
index 3b5fdfd..249c0c2 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsActivity.java
@@ -37,8 +37,8 @@ import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract;
-import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.remote.AppSettings;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.dialog.AdvancedAppSettingsDialogFragment;
@@ -182,7 +182,7 @@ public class AppSettingsActivity extends BaseActivity {
}
private void loadData(Bundle savedInstanceState, Uri appUri) {
- mAppSettings = new ProviderHelper(this).getApiAppSettings(appUri);
+ mAppSettings = new ApiDataAccessObject(this).getApiAppSettings(appUri);
// get application name and icon from package manager
String appName;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java
index caa173f..22a03a7 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppSettingsAllowedKeysListFragment.java
@@ -36,8 +36,8 @@ import android.widget.ListView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
+import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract;
-import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.adapter.KeySelectableAdapter;
import org.sufficientlysecure.keychain.ui.widget.FixedListView;
@@ -47,7 +47,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
private static final String ARG_DATA_URI = "uri";
private KeySelectableAdapter mAdapter;
- private ProviderHelper mProviderHelper;
+ private ApiDataAccessObject mApiDao;
private Uri mDataUri;
@@ -69,7 +69,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mProviderHelper = new ProviderHelper(getActivity());
+ mApiDao = new ApiDataAccessObject(getActivity());
}
@Override
@@ -107,7 +107,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
// application this would come from a resource.
setEmptyText(getString(R.string.list_empty));
- Set<Long> checked = mProviderHelper.getAllKeyIdsForApp(mDataUri);
+ Set<Long> checked = mApiDao.getAllKeyIdsForApp(mDataUri);
mAdapter = new KeySelectableAdapter(getActivity(), null, 0, checked);
setListAdapter(mAdapter);
getListView().setOnItemClickListener(mAdapter);
@@ -141,7 +141,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
public void saveAllowedKeys() {
try {
- mProviderHelper.saveAllowedKeyIdsForApp(mDataUri, getSelectedMasterKeyIds());
+ mApiDao.saveAllowedKeyIdsForApp(mDataUri, getSelectedMasterKeyIds());
} catch (RemoteException | OperationApplicationException e) {
Log.e(Constants.TAG, "Problem saving allowed key ids!", e);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteCreateAccountActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteCreateAccountActivity.java
index adc1f5a..a4a0b24 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteCreateAccountActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteCreateAccountActivity.java
@@ -17,6 +17,7 @@
package org.sufficientlysecure.keychain.remote.ui;
+
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -25,8 +26,8 @@ import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract;
-import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.remote.AccountSettings;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.util.Notify;
@@ -56,7 +57,7 @@ public class RemoteCreateAccountActivity extends BaseActivity {
final String packageName = extras.getString(EXTRA_PACKAGE_NAME);
final String accName = extras.getString(EXTRA_ACC_NAME);
- final ProviderHelper providerHelper = new ProviderHelper(this);
+ final ApiDataAccessObject apiDao = new ApiDataAccessObject(this);
mAccSettingsFragment = (AccountSettingsFragment) getSupportFragmentManager().findFragmentById(
R.id.api_account_settings_fragment);
@@ -65,7 +66,7 @@ public class RemoteCreateAccountActivity extends BaseActivity {
// update existing?
Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(packageName, accName);
- AccountSettings settings = providerHelper.getApiAccountSettings(uri);
+ AccountSettings settings = apiDao.getApiAccountSettings(uri);
if (settings == null) {
// create new account
settings = new AccountSettings(accName);
@@ -94,11 +95,11 @@ public class RemoteCreateAccountActivity extends BaseActivity {
if (mUpdateExistingAccount) {
Uri baseUri = KeychainContract.ApiAccounts.buildBaseUri(packageName);
Uri accountUri = baseUri.buildUpon().appendEncodedPath(accName).build();
- providerHelper.updateApiAccount(
+ apiDao.updateApiAccount(
accountUri,
mAccSettingsFragment.getAccSettings());
} else {
- providerHelper.insertApiAccount(
+ apiDao.insertApiAccount(
KeychainContract.ApiAccounts.buildBaseUri(packageName),
mAccSettingsFragment.getAccSettings());
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteRegisterActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteRegisterActivity.java
index d31f9e3..0ce9e66 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteRegisterActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteRegisterActivity.java
@@ -17,13 +17,14 @@
package org.sufficientlysecure.keychain.remote.ui;
+
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.remote.AppSettings;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.util.Log;
@@ -52,7 +53,7 @@ public class RemoteRegisterActivity extends BaseActivity {
final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE);
Log.d(Constants.TAG, "ACTION_REGISTER packageName: " + packageName);
- final ProviderHelper providerHelper = new ProviderHelper(this);
+ final ApiDataAccessObject apiDao = new ApiDataAccessObject(this);
mAppSettingsHeaderFragment = (AppSettingsHeaderFragment) getSupportFragmentManager().findFragmentById(
R.id.api_app_settings_fragment);
@@ -67,8 +68,7 @@ public class RemoteRegisterActivity extends BaseActivity {
@Override
public void onClick(View v) {
// Allow
-
- providerHelper.insertApiApp(mAppSettingsHeaderFragment.getAppSettings());
+ apiDao.insertApiApp(mAppSettingsHeaderFragment.getAppSettings());
// give data through for new service call
Intent resultData = extras.getParcelable(EXTRA_DATA);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdListFragment.java
index 852e8bb..73ce5fe 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdListFragment.java
@@ -17,6 +17,7 @@
package org.sufficientlysecure.keychain.remote.ui;
+
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@@ -36,9 +37,9 @@ import org.openintents.openpgp.util.OpenPgpApi;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
+import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
-import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.adapter.SelectKeyCursorAdapter;
import org.sufficientlysecure.keychain.ui.widget.FixedListView;
import org.sufficientlysecure.keychain.util.Log;
@@ -49,7 +50,7 @@ public class SelectSignKeyIdListFragment extends ListFragmentWorkaround implemen
public static final String ARG_DATA = "data";
private SelectKeyCursorAdapter mAdapter;
- private ProviderHelper mProviderHelper;
+ private ApiDataAccessObject mApiDao;
private Uri mDataUri;
@@ -72,7 +73,7 @@ public class SelectSignKeyIdListFragment extends ListFragmentWorkaround implemen
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mProviderHelper = new ProviderHelper(getActivity());
+ mApiDao = new ApiDataAccessObject(getActivity());
}
@Override
@@ -116,7 +117,7 @@ public class SelectSignKeyIdListFragment extends ListFragmentWorkaround implemen
Uri allowedKeysUri = mDataUri.buildUpon().appendPath(KeychainContract.PATH_ALLOWED_KEYS).build();
Log.d(Constants.TAG, "allowedKeysUri: " + allowedKeysUri);
- mProviderHelper.addAllowedKeyIdForApp(allowedKeysUri, masterKeyId);
+ mApiDao.addAllowedKeyIdForApp(allowedKeysUri, masterKeyId);
resultData.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, masterKeyId);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java
new file mode 100644
index 0000000..905deca
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/CardException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.securitytoken;
+
+import java.io.IOException;
+
+public class CardException extends IOException {
+ private short mResponseCode;
+
+ public CardException(String detailMessage, short responseCode) {
+ super(detailMessage);
+ mResponseCode = responseCode;
+ }
+
+ public short getResponseCode() {
+ return mResponseCode;
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java
new file mode 100644
index 0000000..0e28f02
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/KeyType.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.securitytoken;
+
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
+
+public enum KeyType {
+ SIGN(0, 0xB6, 0xCE, 0xC7),
+ ENCRYPT(1, 0xB8, 0xCF, 0xC8),
+ AUTH(2, 0xA4, 0xD0, 0xC9),;
+
+ private final int mIdx;
+ private final int mSlot;
+ private final int mTimestampObjectId;
+ private final int mFingerprintObjectId;
+
+ KeyType(final int idx, final int slot, final int timestampObjectId, final int fingerprintObjectId) {
+ this.mIdx = idx;
+ this.mSlot = slot;
+ this.mTimestampObjectId = timestampObjectId;
+ this.mFingerprintObjectId = fingerprintObjectId;
+ }
+
+ public static KeyType from(final CanonicalizedSecretKey key) {
+ if (key.canSign() || key.canCertify()) {
+ return SIGN;
+ } else if (key.canEncrypt()) {
+ return ENCRYPT;
+ } else if (key.canAuthenticate()) {
+ return AUTH;
+ }
+ return null;
+ }
+
+ public int getIdx() {
+ return mIdx;
+ }
+
+ public int getmSlot() {
+ return mSlot;
+ }
+
+ public int getTimestampObjectId() {
+ return mTimestampObjectId;
+ }
+
+ public int getmFingerprintObjectId() {
+ return mFingerprintObjectId;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java
new file mode 100644
index 0000000..ba36ebd
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/NfcTransport.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.securitytoken;
+
+import android.nfc.Tag;
+
+import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
+
+import java.io.IOException;
+
+import nordpol.IsoCard;
+import nordpol.android.AndroidCard;
+
+public class NfcTransport implements Transport {
+ // timeout is set to 100 seconds to avoid cancellation during calculation
+ private static final int TIMEOUT = 100 * 1000;
+ private final Tag mTag;
+ private IsoCard mIsoCard;
+
+ public NfcTransport(Tag tag) {
+ this.mTag = tag;
+ }
+
+ /**
+ * Transmit and receive data
+ * @param data data to transmit
+ * @return received data
+ * @throws IOException
+ */
+ @Override
+ public byte[] transceive(final byte[] data) throws IOException {
+ return mIsoCard.transceive(data);
+ }
+
+ /**
+ * Disconnect and release connection
+ */
+ @Override
+ public void release() {
+ // Not supported
+ }
+
+ @Override
+ public boolean isConnected() {
+ return mIsoCard != null && mIsoCard.isConnected();
+ }
+
+ /**
+ * Check if Transport supports persistent connections e.g connections which can
+ * handle multiple operations in one session
+ * @return true if transport supports persistent connections
+ */
+ @Override
+ public boolean isPersistentConnectionAllowed() {
+ return false;
+ }
+
+ /**
+ * Connect to NFC device.
+ * <p/>
+ * On general communication, see also
+ * http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-a.aspx
+ * <p/>
+ * References to pages are generally related to the OpenPGP Application
+ * on ISO SmartCard Systems specification.
+ */
+ @Override
+ public void connect() throws IOException {
+ mIsoCard = AndroidCard.get(mTag);
+ if (mIsoCard == null) {
+ throw new BaseSecurityTokenActivity.IsoDepNotSupportedException("Tag does not support ISO-DEP (ISO 14443-4)");
+ }
+
+ mIsoCard.setTimeout(TIMEOUT);
+ mIsoCard.connect();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final NfcTransport that = (NfcTransport) o;
+
+ if (mTag != null ? !mTag.equals(that.mTag) : that.mTag != null) return false;
+ if (mIsoCard != null ? !mIsoCard.equals(that.mIsoCard) : that.mIsoCard != null)
+ return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mTag != null ? mTag.hashCode() : 0;
+ result = 31 * result + (mIsoCard != null ? mIsoCard.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java
new file mode 100644
index 0000000..30893af
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/SecurityTokenHelper.java
@@ -0,0 +1,756 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ * Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ * Copyright (C) 2013-2014 Signe Rüsch
+ * Copyright (C) 2013-2014 Philipp Jakubeit
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+package org.sufficientlysecure.keychain.securitytoken;
+
+import android.support.annotation.NonNull;
+
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.util.Arrays;
+import org.bouncycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.util.Iso7816TLV;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.interfaces.RSAPrivateCrtKey;
+
+import nordpol.Apdu;
+
+/**
+ * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant
+ * devices.
+ * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
+ */
+public class SecurityTokenHelper {
+ // Fidesmo constants
+ private static final String FIDESMO_APPS_AID_PREFIX = "A000000617";
+
+ private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+ private Transport mTransport;
+
+ private Passphrase mPin;
+ private Passphrase mAdminPin;
+ private boolean mPw1ValidForMultipleSignatures;
+ private boolean mPw1ValidatedForSignature;
+ private boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming?
+ private boolean mPw3Validated;
+
+ protected SecurityTokenHelper() {
+ }
+
+ public static SecurityTokenHelper getInstance() {
+ return LazyHolder.SECURITY_TOKEN_HELPER;
+ }
+
+ private static String getHex(byte[] raw) {
+ return new String(Hex.encode(raw));
+ }
+
+ private String getHolderName(String name) {
+ try {
+ String slength;
+ int ilength;
+ name = name.substring(6);
+ slength = name.substring(0, 2);
+ ilength = Integer.parseInt(slength, 16) * 2;
+ name = name.substring(2, ilength + 2);
+ name = (new String(Hex.decode(name))).replace('<', ' ');
+ return name;
+ } catch (IndexOutOfBoundsException e) {
+ // try-catch for https://github.com/FluffyKaon/OpenPGP-Card
+ // Note: This should not happen, but happens with
+ // https://github.com/FluffyKaon/OpenPGP-Card, thus return an empty string for now!
+
+ Log.e(Constants.TAG, "Couldn't get holder name, returning empty string!", e);
+ return "";
+ }
+ }
+
+ public Passphrase getPin() {
+ return mPin;
+ }
+
+ public void setPin(final Passphrase pin) {
+ this.mPin = pin;
+ }
+
+ public Passphrase getAdminPin() {
+ return mAdminPin;
+ }
+
+ public void setAdminPin(final Passphrase adminPin) {
+ this.mAdminPin = adminPin;
+ }
+
+ public void changeKey(CanonicalizedSecretKey secretKey, Passphrase passphrase) throws IOException {
+ long keyGenerationTimestamp = secretKey.getCreationTime().getTime() / 1000;
+ byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
+ KeyType keyType = KeyType.from(secretKey);
+
+ if (keyType == null) {
+ throw new IOException("Inappropriate key flags for smart card key.");
+ }
+
+ // Slot is empty, or contains this key already. PUT KEY operation is safe
+ boolean canPutKey = isSlotEmpty(keyType)
+ || keyMatchesFingerPrint(keyType, secretKey.getFingerprint());
+
+ if (!canPutKey) {
+ throw new IOException(String.format("Key slot occupied; card must be reset to put new %s key.",
+ keyType.toString()));
+ }
+
+ putKey(keyType.getmSlot(), secretKey, passphrase);
+ putData(keyType.getmFingerprintObjectId(), secretKey.getFingerprint());
+ putData(keyType.getTimestampObjectId(), timestampBytes);
+ }
+
+ private boolean isSlotEmpty(KeyType keyType) throws IOException {
+ // Note: special case: This should not happen, but happens with
+ // https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true
+ if (getKeyFingerprint(keyType) == null) return true;
+
+ return keyMatchesFingerPrint(keyType, BLANK_FINGERPRINT);
+ }
+
+ public boolean keyMatchesFingerPrint(KeyType keyType, byte[] fingerprint) throws IOException {
+ return java.util.Arrays.equals(getKeyFingerprint(keyType), fingerprint);
+ }
+
+ /**
+ * Connect to device and select pgp applet
+ *
+ * @throws IOException
+ */
+ public void connectToDevice() throws IOException {
+ // Connect on transport layer
+ mTransport.connect();
+
+ // Connect on smartcard layer
+
+ // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
+ // See specification, page 51
+ String accepted = "9000";
+
+ // Command APDU (page 51) for SELECT FILE command (page 29)
+ String opening =
+ "00" // CLA
+ + "A4" // INS
+ + "04" // P1
+ + "00" // P2
+ + "06" // Lc (number of bytes)
+ + "D27600012401" // Data (6 bytes)
+ + "00"; // Le
+ String response = communicate(opening); // activate connection
+ if (!response.endsWith(accepted)) {
+ throw new CardException("Initialization failed!", parseCardStatus(response));
+ }
+
+ byte[] pwStatusBytes = getPwStatusBytes();
+ mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1);
+ mPw1ValidatedForSignature = false;
+ mPw1ValidatedForDecrypt = false;
+ mPw3Validated = false;
+ }
+
+ /**
+ * Parses out the status word from a JavaCard response string.
+ *
+ * @param response A hex string with the response from the card
+ * @return A short indicating the SW1/SW2, or 0 if a status could not be determined.
+ */
+ private short parseCardStatus(String response) {
+ if (response.length() < 4) {
+ return 0; // invalid input
+ }
+
+ try {
+ return Short.parseShort(response.substring(response.length() - 4), 16);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for
+ * conformance to the token's requirements for key length.
+ *
+ * @param pw For PW1, this is 0x81. For PW3 (Admin PIN), mode is 0x83.
+ * @param newPin The new PW1 or PW3.
+ */
+ public void modifyPin(int pw, byte[] newPin) throws IOException {
+ final int MAX_PW1_LENGTH_INDEX = 1;
+ final int MAX_PW3_LENGTH_INDEX = 3;
+
+ byte[] pwStatusBytes = getPwStatusBytes();
+
+ if (pw == 0x81) {
+ if (newPin.length < 6 || newPin.length > pwStatusBytes[MAX_PW1_LENGTH_INDEX]) {
+ throw new IOException("Invalid PIN length");
+ }
+ } else if (pw == 0x83) {
+ if (newPin.length < 8 || newPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) {
+ throw new IOException("Invalid PIN length");
+ }
+ } else {
+ throw new IOException("Invalid PW index for modify PIN operation");
+ }
+
+ byte[] pin;
+ if (pw == 0x83) {
+ pin = mAdminPin.toStringUnsafe().getBytes();
+ } else {
+ pin = mPin.toStringUnsafe().getBytes();
+ }
+
+ // Command APDU for CHANGE REFERENCE DATA command (page 32)
+ String changeReferenceDataApdu = "00" // CLA
+ + "24" // INS
+ + "00" // P1
+ + String.format("%02x", pw) // P2
+ + String.format("%02x", pin.length + newPin.length) // Lc
+ + getHex(pin)
+ + getHex(newPin);
+ String response = communicate(changeReferenceDataApdu); // change PIN
+ if (!response.equals("9000")) {
+ throw new CardException("Failed to change PIN", parseCardStatus(response));
+ }
+ }
+
+ /**
+ * Call DECIPHER command
+ *
+ * @param encryptedSessionKey the encoded session key
+ * @return the decoded session key
+ */
+ public byte[] decryptSessionKey(byte[] encryptedSessionKey) throws IOException {
+ if (!mPw1ValidatedForDecrypt) {
+ verifyPin(0x82); // (Verify PW1 with mode 82 for decryption)
+ }
+
+ String firstApdu = "102a8086fe";
+ String secondApdu = "002a808603";
+ String le = "00";
+
+ byte[] one = new byte[254];
+ // leave out first byte:
+ System.arraycopy(encryptedSessionKey, 1, one, 0, one.length);
+
+ byte[] two = new byte[encryptedSessionKey.length - 1 - one.length];
+ for (int i = 0; i < two.length; i++) {
+ two[i] = encryptedSessionKey[i + one.length + 1];
+ }
+
+ communicate(firstApdu + getHex(one));
+ String second = communicate(secondApdu + getHex(two) + le);
+
+ String decryptedSessionKey = getDataField(second);
+
+ return Hex.decode(decryptedSessionKey);
+ }
+
+ /**
+ * Verifies the user's PW1 or PW3 with the appropriate mode.
+ *
+ * @param mode For PW1, this is 0x81 for signing, 0x82 for everything else.
+ * For PW3 (Admin PIN), mode is 0x83.
+ */
+ private void verifyPin(int mode) throws IOException {
+ if (mPin != null || mode == 0x83) {
+
+ byte[] pin;
+ if (mode == 0x83) {
+ pin = mAdminPin.toStringUnsafe().getBytes();
+ } else {
+ pin = mPin.toStringUnsafe().getBytes();
+ }
+
+ // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
+ // See specification, page 51
+ String accepted = "9000";
+ String response = tryPin(mode, pin); // login
+ if (!response.equals(accepted)) {
+ throw new CardException("Bad PIN!", parseCardStatus(response));
+ }
+
+ if (mode == 0x81) {
+ mPw1ValidatedForSignature = true;
+ } else if (mode == 0x82) {
+ mPw1ValidatedForDecrypt = true;
+ } else if (mode == 0x83) {
+ mPw3Validated = true;
+ }
+ }
+ }
+
+ /**
+ * Stores a data object on the token. Automatically validates the proper PIN for the operation.
+ * Supported for all data objects < 255 bytes in length. Only the cardholder certificate
+ * (0x7F21) can exceed this length.
+ *
+ * @param dataObject The data object to be stored.
+ * @param data The data to store in the object
+ */
+ private void putData(int dataObject, byte[] data) throws IOException {
+ if (data.length > 254) {
+ throw new IOException("Cannot PUT DATA with length > 254");
+ }
+ if (dataObject == 0x0101 || dataObject == 0x0103) {
+ if (!mPw1ValidatedForDecrypt) {
+ verifyPin(0x82); // (Verify PW1 for non-signing operations)
+ }
+ } else if (!mPw3Validated) {
+ verifyPin(0x83); // (Verify PW3)
+ }
+
+ String putDataApdu = "00" // CLA
+ + "DA" // INS
+ + String.format("%02x", (dataObject & 0xFF00) >> 8) // P1
+ + String.format("%02x", dataObject & 0xFF) // P2
+ + String.format("%02x", data.length) // Lc
+ + getHex(data);
+
+ String response = communicate(putDataApdu); // put data
+ if (!response.equals("9000")) {
+ throw new CardException("Failed to put data.", parseCardStatus(response));
+ }
+ }
+
+ /**
+ * Puts a key on the token in the given slot.
+ *
+ * @param slot The slot on the token where the key should be stored:
+ * 0xB6: Signature Key
+ * 0xB8: Decipherment Key
+ * 0xA4: Authentication Key
+ */
+ private void putKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
+ throws IOException {
+ if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
+ throw new IOException("Invalid key slot");
+ }
+
+ RSAPrivateCrtKey crtSecretKey;
+ try {
+ secretKey.unlock(passphrase);
+ crtSecretKey = secretKey.getCrtSecretKey();
+ } catch (PgpGeneralException e) {
+ throw new IOException(e.getMessage());
+ }
+
+ // Shouldn't happen; the UI should block the user from getting an incompatible key this far.
+ if (crtSecretKey.getModulus().bitLength() > 2048) {
+ throw new IOException("Key too large to export to Security Token.");
+ }
+
+ // Should happen only rarely; all GnuPG keys since 2006 use public exponent 65537.
+ if (!crtSecretKey.getPublicExponent().equals(new BigInteger("65537"))) {
+ throw new IOException("Invalid public exponent for smart Security Token.");
+ }
+
+ if (!mPw3Validated) {
+ verifyPin(0x83); // (Verify PW3 with mode 83)
+ }
+
+ byte[] header = Hex.decode(
+ "4D82" + "03A2" // Extended header list 4D82, length of 930 bytes. (page 23)
+ + String.format("%02x", slot) + "00" // CRT to indicate targeted key, no length
+ + "7F48" + "15" // Private key template 0x7F48, length 21 (decimal, 0x15 hex)
+ + "9103" // Public modulus, length 3
+ + "928180" // Prime P, length 128
+ + "938180" // Prime Q, length 128
+ + "948180" // Coefficient (1/q mod p), length 128
+ + "958180" // Prime exponent P (d mod (p - 1)), length 128
+ + "968180" // Prime exponent Q (d mod (1 - 1)), length 128
+ + "97820100" // Modulus, length 256, last item in private key template
+ + "5F48" + "820383");// DO 5F48; 899 bytes of concatenated key data will follow
+ byte[] dataToSend = new byte[934];
+ byte[] currentKeyObject;
+ int offset = 0;
+
+ System.arraycopy(header, 0, dataToSend, offset, header.length);
+ offset += header.length;
+ currentKeyObject = crtSecretKey.getPublicExponent().toByteArray();
+ System.arraycopy(currentKeyObject, 0, dataToSend, offset, 3);
+ offset += 3;
+ // NOTE: For a 2048-bit key, these lengths are fixed. However, bigint includes a leading 0
+ // in the array to represent sign, so we take care to set the offset to 1 if necessary.
+ currentKeyObject = crtSecretKey.getPrimeP().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getPrimeQ().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getCrtCoefficient().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getPrimeExponentP().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getPrimeExponentQ().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
+ Arrays.fill(currentKeyObject, (byte) 0);
+ offset += 128;
+ currentKeyObject = crtSecretKey.getModulus().toByteArray();
+ System.arraycopy(currentKeyObject, currentKeyObject.length - 256, dataToSend, offset, 256);
+
+ String putKeyCommand = "10DB3FFF";
+ String lastPutKeyCommand = "00DB3FFF";
+
+ // Now we're ready to communicate with the token.
+ offset = 0;
+ String response;
+ while (offset < dataToSend.length) {
+ int dataRemaining = dataToSend.length - offset;
+ if (dataRemaining > 254) {
+ response = communicate(
+ putKeyCommand + "FE" + Hex.toHexString(dataToSend, offset, 254)
+ );
+ offset += 254;
+ } else {
+ int length = dataToSend.length - offset;
+ response = communicate(
+ lastPutKeyCommand + String.format("%02x", length)
+ + Hex.toHexString(dataToSend, offset, length));
+ offset += length;
+ }
+
+ if (!response.endsWith("9000")) {
+ throw new CardException("Key export to Security Token failed", parseCardStatus(response));
+ }
+ }
+
+ // Clear array with secret data before we return.
+ Arrays.fill(dataToSend, (byte) 0);
+ }
+
+ /**
+ * Return fingerprints of all keys from application specific data stored
+ * on tag, or null if data not available.
+ *
+ * @return The fingerprints of all subkeys in a contiguous byte array.
+ */
+ public byte[] getFingerprints() throws IOException {
+ String data = "00CA006E00";
+ byte[] buf = mTransport.transceive(Hex.decode(data));
+
+ Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true);
+ Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint());
+
+ Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5);
+ if (fptlv == null) {
+ return null;
+ }
+ return fptlv.mV;
+ }
+
+ /**
+ * Return the PW Status Bytes from the token. This is a simple DO; no TLV decoding needed.
+ *
+ * @return Seven bytes in fixed format, plus 0x9000 status word at the end.
+ */
+ private byte[] getPwStatusBytes() throws IOException {
+ String data = "00CA00C400";
+ return mTransport.transceive(Hex.decode(data));
+ }
+
+ public byte[] getAid() throws IOException {
+ String info = "00CA004F00";
+ return mTransport.transceive(Hex.decode(info));
+ }
+
+ public String getUserId() throws IOException {
+ String info = "00CA006500";
+ return getHolderName(communicate(info));
+ }
+
+ /**
+ * Call COMPUTE DIGITAL SIGNATURE command and returns the MPI value
+ *
+ * @param hash the hash for signing
+ * @return a big integer representing the MPI for the given hash
+ */
+ public byte[] calculateSignature(byte[] hash, int hashAlgo) throws IOException {
+ if (!mPw1ValidatedForSignature) {
+ verifyPin(0x81); // (Verify PW1 with mode 81 for signing)
+ }
+
+ // dsi, including Lc
+ String dsi;
+
+ Log.i(Constants.TAG, "Hash: " + hashAlgo);
+ switch (hashAlgo) {
+ case HashAlgorithmTags.SHA1:
+ if (hash.length != 20) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 10!");
+ }
+ dsi = "23" // Lc
+ + "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
+ + "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
+ + "0605" + "2B0E03021A" // OID of SHA1
+ + "0500" // TLV coding of ZERO
+ + "0414" + getHex(hash); // 0x14 are 20 hash bytes
+ break;
+ case HashAlgorithmTags.RIPEMD160:
+ if (hash.length != 20) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 20!");
+ }
+ dsi = "233021300906052B2403020105000414" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA224:
+ if (hash.length != 28) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 28!");
+ }
+ dsi = "2F302D300D06096086480165030402040500041C" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA256:
+ if (hash.length != 32) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 32!");
+ }
+ dsi = "333031300D060960864801650304020105000420" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA384:
+ if (hash.length != 48) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 48!");
+ }
+ dsi = "433041300D060960864801650304020205000430" + getHex(hash);
+ break;
+ case HashAlgorithmTags.SHA512:
+ if (hash.length != 64) {
+ throw new IOException("Bad hash length (" + hash.length + ", expected 64!");
+ }
+ dsi = "533051300D060960864801650304020305000440" + getHex(hash);
+ break;
+ default:
+ throw new IOException("Not supported hash algo!");
+ }
+
+ // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
+ String apdu =
+ "002A9E9A" // CLA, INS, P1, P2
+ + dsi // digital signature input
+ + "00"; // Le
+
+ String response = communicate(apdu);
+
+ if (response.length() < 4) {
+ throw new CardException("Bad response", (short) 0);
+ }
+ // split up response into signature and status
+ String status = response.substring(response.length() - 4);
+ String signature = response.substring(0, response.length() - 4);
+
+ // while we are getting 0x61 status codes, retrieve more data
+ while (status.substring(0, 2).equals("61")) {
+ Log.d(Constants.TAG, "requesting more data, status " + status);
+ // Send GET RESPONSE command
+ response = communicate("00C00000" + status.substring(2));
+ status = response.substring(response.length() - 4);
+ signature += response.substring(0, response.length() - 4);
+ }
+
+ Log.d(Constants.TAG, "final response:" + status);
+
+ if (!mPw1ValidForMultipleSignatures) {
+ mPw1ValidatedForSignature = false;
+ }
+
+ if (!"9000".equals(status)) {
+ throw new CardException("Bad NFC response code: " + status, parseCardStatus(response));
+ }
+
+ // Make sure the signature we received is actually the expected number of bytes long!
+ if (signature.length() != 256 && signature.length() != 512) {
+ throw new IOException("Bad signature length! Expected 128 or 256 bytes, got " + signature.length() / 2);
+ }
+
+ return Hex.decode(signature);
+ }
+
+ /**
+ * Transceive data via NFC encoded as Hex
+ */
+ private String communicate(String apdu) throws IOException {
+ return getHex(mTransport.transceive(Hex.decode(apdu)));
+ }
+
+ public Transport getTransport() {
+ return mTransport;
+ }
+
+ public void setTransport(Transport mTransport) {
+ this.mTransport = mTransport;
+ }
+
+ public boolean isFidesmoToken() {
+ if (isConnected()) { // Check if we can still talk to the card
+ try {
+ // By trying to select any apps that have the Fidesmo AID prefix we can
+ // see if it is a Fidesmo device or not
+ byte[] mSelectResponse = mTransport.transceive(Apdu.select(FIDESMO_APPS_AID_PREFIX));
+ // Compare the status returned by our select with the OK status code
+ return Apdu.hasStatus(mSelectResponse, Apdu.OK_APDU);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Card communication failed!", e);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Generates a key on the card in the given slot. If the slot is 0xB6 (the signature key),
+ * this command also has the effect of resetting the digital signature counter.
+ * NOTE: This does not set the key fingerprint data object! After calling this command, you
+ * must construct a public key packet using the returned public key data objects, compute the
+ * key fingerprint, and store it on the card using: putData(0xC8, key.getFingerprint())
+ *
+ * @param slot The slot on the card where the key should be generated:
+ * 0xB6: Signature Key
+ * 0xB8: Decipherment Key
+ * 0xA4: Authentication Key
+ * @return the public key data objects, in TLV format. For RSA this will be the public modulus
+ * (0x81) and exponent (0x82). These may come out of order; proper TLV parsing is required.
+ */
+ public byte[] generateKey(int slot) throws IOException {
+ if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
+ throw new IOException("Invalid key slot");
+ }
+
+ if (!mPw3Validated) {
+ verifyPin(0x83); // (Verify PW3 with mode 83)
+ }
+
+ String generateKeyApdu = "0047800002" + String.format("%02x", slot) + "0000";
+ String getResponseApdu = "00C00000";
+
+ String first = communicate(generateKeyApdu);
+ String second = communicate(getResponseApdu);
+
+ if (!second.endsWith("9000")) {
+ throw new IOException("On-card key generation failed");
+ }
+
+ String publicKeyData = getDataField(first) + getDataField(second);
+
+ Log.d(Constants.TAG, "Public Key Data Objects: " + publicKeyData);
+
+ return Hex.decode(publicKeyData);
+ }
+
+ private String getDataField(String output) {
+ return output.substring(0, output.length() - 4);
+ }
+
+ private String tryPin(int mode, byte[] pin) throws IOException {
+ // Command APDU for VERIFY command (page 32)
+ String login =
+ "00" // CLA
+ + "20" // INS
+ + "00" // P1
+ + String.format("%02x", mode) // P2
+ + String.format("%02x", pin.length) // Lc
+ + Hex.toHexString(pin);
+
+ return communicate(login);
+ }
+
+ /**
+ * Resets security token, which deletes all keys and data objects.
+ * This works by entering a wrong PIN and then Admin PIN 4 times respectively.
+ * Afterwards, the token is reactivated.
+ */
+ public void resetAndWipeToken() throws IOException {
+ String accepted = "9000";
+
+ // try wrong PIN 4 times until counter goes to C0
+ byte[] pin = "XXXXXX".getBytes();
+ for (int i = 0; i <= 4; i++) {
+ String response = tryPin(0x81, pin);
+ if (response.equals(accepted)) { // Should NOT accept!
+ throw new CardException("Should never happen, XXXXXX has been accepted!", parseCardStatus(response));
+ }
+ }
+
+ // try wrong Admin PIN 4 times until counter goes to C0
+ byte[] adminPin = "XXXXXXXX".getBytes();
+ for (int i = 0; i <= 4; i++) {
+ String response = tryPin(0x83, adminPin);
+ if (response.equals(accepted)) { // Should NOT accept!
+ throw new CardException("Should never happen, XXXXXXXX has been accepted", parseCardStatus(response));
+ }
+ }
+
+ // reactivate token!
+ String reactivate1 = "00" + "e6" + "00" + "00";
+ String reactivate2 = "00" + "44" + "00" + "00";
+ String response1 = communicate(reactivate1);
+ String response2 = communicate(reactivate2);
+ if (!response1.equals(accepted) || !response2.equals(accepted)) {
+ throw new CardException("Reactivating failed!", parseCardStatus(response1));
+ }
+
+ }
+
+ /**
+ * Return the fingerprint from application specific data stored on tag, or
+ * null if it doesn't exist.
+ *
+ * @param keyType key type
+ * @return The fingerprint of the requested key, or null if not found.
+ */
+ public byte[] getKeyFingerprint(@NonNull KeyType keyType) throws IOException {
+ byte[] data = getFingerprints();
+ if (data == null) {
+ return null;
+ }
+
+ // return the master key fingerprint
+ ByteBuffer fpbuf = ByteBuffer.wrap(data);
+ byte[] fp = new byte[20];
+ fpbuf.position(keyType.getIdx() * 20);
+ fpbuf.get(fp, 0, 20);
+
+ return fp;
+ }
+
+ public boolean isPersistentConnectionAllowed() {
+ return mTransport != null && mTransport.isPersistentConnectionAllowed();
+ }
+
+ public boolean isConnected() {
+ return mTransport != null && mTransport.isConnected();
+ }
+
+ private static class LazyHolder {
+ private static final SecurityTokenHelper SECURITY_TOKEN_HELPER = new SecurityTokenHelper();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java
new file mode 100644
index 0000000..294eaa9
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/Transport.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.securitytoken;
+
+import java.io.IOException;
+
+/**
+ * Abstraction for transmitting APDU commands
+ */
+public interface Transport {
+ /**
+ * Transmit and receive data
+ * @param data data to transmit
+ * @return received data
+ * @throws IOException
+ */
+ byte[] transceive(byte[] data) throws IOException;
+
+ /**
+ * Disconnect and release connection
+ */
+ void release();
+
+ /**
+ * Check if device is was connected to and still is connected
+ * @return connection status
+ */
+ boolean isConnected();
+
+ /**
+ * Check if Transport supports persistent connections e.g connections which can
+ * handle multiple operations in one session
+ * @return true if transport supports persistent connections
+ */
+ boolean isPersistentConnectionAllowed();
+
+
+ /**
+ * Connect to device
+ * @throws IOException
+ */
+ void connect() throws IOException;
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java
new file mode 100644
index 0000000..dfe9142
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransport.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.securitytoken;
+
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbEndpoint;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+
+import org.bouncycastle.util.Arrays;
+import org.bouncycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Based on USB CCID Specification rev. 1.1
+ * http://www.usb.org/developers/docs/devclass_docs/DWG_Smart-Card_CCID_Rev110.pdf
+ * Implements small subset of these features
+ */
+public class UsbTransport implements Transport {
+ private static final int USB_CLASS_SMARTCARD = 11;
+ private static final int TIMEOUT = 20 * 1000; // 20s
+
+ private final UsbManager mUsbManager;
+ private final UsbDevice mUsbDevice;
+ private UsbInterface mUsbInterface;
+ private UsbEndpoint mBulkIn;
+ private UsbEndpoint mBulkOut;
+ private UsbDeviceConnection mConnection;
+ private byte mCounter;
+
+ public UsbTransport(UsbDevice usbDevice, UsbManager usbManager) {
+ mUsbDevice = usbDevice;
+ mUsbManager = usbManager;
+ }
+
+
+ /**
+ * Manage ICC power, Yubikey requires to power on ICC
+ * Spec: 6.1.1 PC_to_RDR_IccPowerOn; 6.1.2 PC_to_RDR_IccPowerOff
+ *
+ * @param on true to turn ICC on, false to turn it off
+ * @throws UsbTransportException
+ */
+ private void setIccPower(boolean on) throws UsbTransportException {
+ final byte[] iccPowerCommand = {
+ (byte) (on ? 0x62 : 0x63),
+ 0x00, 0x00, 0x00, 0x00,
+ 0x00,
+ mCounter++,
+ 0x00,
+ 0x00, 0x00
+ };
+
+ sendRaw(iccPowerCommand);
+ byte[] bytes;
+ do {
+ bytes = receive();
+ } while (isDataBlockNotReady(bytes));
+ checkDataBlockResponse(bytes);
+ }
+
+ /**
+ * Get first class 11 (Chip/Smartcard) interface of the device
+ *
+ * @param device {@link UsbDevice} which will be searched
+ * @return {@link UsbInterface} of smartcard or null if it doesn't exist
+ */
+ @Nullable
+ private static UsbInterface getSmartCardInterface(UsbDevice device) {
+ for (int i = 0; i < device.getInterfaceCount(); i++) {
+ UsbInterface anInterface = device.getInterface(i);
+ if (anInterface.getInterfaceClass() == USB_CLASS_SMARTCARD) {
+ return anInterface;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get device's bulk-in and bulk-out endpoints
+ *
+ * @param usbInterface usb device interface
+ * @return pair of builk-in and bulk-out endpoints respectively
+ */
+ @NonNull
+ private static Pair<UsbEndpoint, UsbEndpoint> getIoEndpoints(final UsbInterface usbInterface) {
+ UsbEndpoint bulkIn = null, bulkOut = null;
+ for (int i = 0; i < usbInterface.getEndpointCount(); i++) {
+ final UsbEndpoint endpoint = usbInterface.getEndpoint(i);
+ if (endpoint.getType() != UsbConstants.USB_ENDPOINT_XFER_BULK) {
+ continue;
+ }
+
+ if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) {
+ bulkIn = endpoint;
+ } else if (endpoint.getDirection() == UsbConstants.USB_DIR_OUT) {
+ bulkOut = endpoint;
+ }
+ }
+ return new Pair<>(bulkIn, bulkOut);
+ }
+
+ /**
+ * Release interface and disconnect
+ */
+ @Override
+ public void release() {
+ if (mConnection != null) {
+ mConnection.releaseInterface(mUsbInterface);
+ mConnection.close();
+ mConnection = null;
+ }
+
+ Log.d(Constants.TAG, "Usb transport disconnected");
+ }
+
+ /**
+ * Check if device is was connected to and still is connected
+ * @return true if device is connected
+ */
+ @Override
+ public boolean isConnected() {
+ return mConnection != null && mUsbManager.getDeviceList().containsValue(mUsbDevice) &&
+ mConnection.getSerial() != null;
+ }
+
+ /**
+ * Check if Transport supports persistent connections e.g connections which can
+ * handle multiple operations in one session
+ * @return true if transport supports persistent connections
+ */
+ @Override
+ public boolean isPersistentConnectionAllowed() {
+ return true;
+ }
+
+ /**
+ * Connect to OTG device
+ * @throws IOException
+ */
+ @Override
+ public void connect() throws IOException {
+ mCounter = 0;
+ mUsbInterface = getSmartCardInterface(mUsbDevice);
+ if (mUsbInterface == null) {
+ // Shouldn't happen as we whitelist only class 11 devices
+ throw new UsbTransportException("USB error - device doesn't have class 11 interface");
+ }
+
+ final Pair<UsbEndpoint, UsbEndpoint> ioEndpoints = getIoEndpoints(mUsbInterface);
+ mBulkIn = ioEndpoints.first;
+ mBulkOut = ioEndpoints.second;
+
+ if (mBulkIn == null || mBulkOut == null) {
+ throw new UsbTransportException("USB error - invalid class 11 interface");
+ }
+
+ mConnection = mUsbManager.openDevice(mUsbDevice);
+ if (mConnection == null) {
+ throw new UsbTransportException("USB error - failed to connect to device");
+ }
+
+ if (!mConnection.claimInterface(mUsbInterface, true)) {
+ throw new UsbTransportException("USB error - failed to claim interface");
+ }
+
+ setIccPower(true);
+ Log.d(Constants.TAG, "Usb transport connected");
+ }
+
+ /**
+ * Transmit and receive data
+ * @param data data to transmit
+ * @return received data
+ * @throws UsbTransportException
+ */
+ @Override
+ public byte[] transceive(byte[] data) throws UsbTransportException {
+ sendXfrBlock(data);
+ byte[] bytes;
+ do {
+ bytes = receive();
+ } while (isDataBlockNotReady(bytes));
+
+ checkDataBlockResponse(bytes);
+ // Discard header
+ return Arrays.copyOfRange(bytes, 10, bytes.length);
+ }
+
+ /**
+ * Transmits XfrBlock
+ * 6.1.4 PC_to_RDR_XfrBlock
+ * @param payload payload to transmit
+ * @throws UsbTransportException
+ */
+ private void sendXfrBlock(byte[] payload) throws UsbTransportException {
+ int l = payload.length;
+ byte[] data = Arrays.concatenate(new byte[]{
+ 0x6f,
+ (byte) l, (byte) (l >> 8), (byte) (l >> 16), (byte) (l >> 24),
+ 0x00,
+ mCounter++,
+ 0x00,
+ 0x00, 0x00},
+ payload);
+
+ int send = 0;
+ while (send < data.length) {
+ final int len = Math.min(mBulkIn.getMaxPacketSize(), data.length - send);
+ sendRaw(Arrays.copyOfRange(data, send, send + len));
+ send += len;
+ }
+ }
+
+ private byte[] receive() throws UsbTransportException {
+ byte[] buffer = new byte[mBulkIn.getMaxPacketSize()];
+ byte[] result = null;
+ int readBytes = 0, totalBytes = 0;
+
+ do {
+ int res = mConnection.bulkTransfer(mBulkIn, buffer, buffer.length, TIMEOUT);
+ if (res < 0) {
+ throw new UsbTransportException("USB error - failed to receive response " + res);
+ }
+ if (result == null) {
+ if (res < 10) {
+ throw new UsbTransportException("USB-CCID error - failed to receive CCID header");
+ }
+ totalBytes = ByteBuffer.wrap(buffer, 1, 4).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get() + 10;
+ result = new byte[totalBytes];
+ }
+ System.arraycopy(buffer, 0, result, readBytes, res);
+ readBytes += res;
+ } while (readBytes < totalBytes);
+
+ return result;
+ }
+
+ private void sendRaw(final byte[] data) throws UsbTransportException {
+ final int tr1 = mConnection.bulkTransfer(mBulkOut, data, data.length, TIMEOUT);
+ if (tr1 != data.length) {
+ throw new UsbTransportException("USB error - failed to transmit data " + tr1);
+ }
+ }
+
+ private byte getStatus(byte[] bytes) {
+ return (byte) ((bytes[7] >> 6) & 0x03);
+ }
+
+ private void checkDataBlockResponse(byte[] bytes) throws UsbTransportException {
+ final byte status = getStatus(bytes);
+ if (status != 0) {
+ throw new UsbTransportException("USB-CCID error - status " + status + " error code: " + Hex.toHexString(bytes, 8, 1));
+ }
+ }
+
+ private boolean isDataBlockNotReady(byte[] bytes) {
+ return getStatus(bytes) == 2;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final UsbTransport that = (UsbTransport) o;
+
+ return mUsbDevice != null ? mUsbDevice.equals(that.mUsbDevice) : that.mUsbDevice == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return mUsbDevice != null ? mUsbDevice.hashCode() : 0;
+ }
+
+ public UsbDevice getUsbDevice() {
+ return mUsbDevice;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java
new file mode 100644
index 0000000..6d9212d
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/UsbTransportException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.securitytoken;
+
+import java.io.IOException;
+
+public class UsbTransportException extends IOException {
+ public UsbTransportException() {
+ }
+
+ public UsbTransportException(final String detailMessage) {
+ super(detailMessage);
+ }
+
+ public UsbTransportException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public UsbTransportException(final Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ChangeUnlockParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ChangeUnlockParcel.java
new file mode 100644
index 0000000..974bb24
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ChangeUnlockParcel.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+package org.sufficientlysecure.keychain.service;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.sufficientlysecure.keychain.util.Passphrase;
+
+public class ChangeUnlockParcel implements Parcelable {
+
+ // the master key id of keyring.
+ public Long mMasterKeyId;
+ // the key fingerprint, for safety.
+ public byte[] mFingerprint;
+ // The new passphrase to use
+ public final Passphrase mNewPassphrase;
+
+ public ChangeUnlockParcel(Passphrase newPassphrase) {
+ mNewPassphrase = newPassphrase;
+ }
+
+ public ChangeUnlockParcel(Long masterKeyId, byte[] fingerprint, Passphrase newPassphrase) {
+ if (newPassphrase == null) {
+ throw new AssertionError("newPassphrase must be non-null. THIS IS A BUG!");
+ }
+
+ mMasterKeyId = masterKeyId;
+ mFingerprint = fingerprint;
+ mNewPassphrase = newPassphrase;
+ }
+
+ public ChangeUnlockParcel(Parcel source) {
+ mMasterKeyId = source.readInt() != 0 ? source.readLong() : null;
+ mFingerprint = source.createByteArray();
+ mNewPassphrase = source.readParcelable(Passphrase.class.getClassLoader());
+ }
+
+ @Override
+ public void writeToParcel(Parcel destination, int flags) {
+ destination.writeInt(mMasterKeyId == null ? 0 : 1);
+ if (mMasterKeyId != null) {
+ destination.writeLong(mMasterKeyId);
+ }
+ destination.writeByteArray(mFingerprint);
+ destination.writeParcelable(mNewPassphrase, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<ChangeUnlockParcel> CREATOR = new Creator<ChangeUnlockParcel>() {
+ public ChangeUnlockParcel createFromParcel(final Parcel source) {
+ return new ChangeUnlockParcel(source);
+ }
+
+ public ChangeUnlockParcel[] newArray(final int size) {
+ return new ChangeUnlockParcel[size];
+ }
+ };
+
+ public String toString() {
+ String out = "mMasterKeyId: " + mMasterKeyId + "\n";
+ out += "passphrase (" + mNewPassphrase + ")";
+
+ return out;
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java
index cdfe2f1..965d151 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java
@@ -143,6 +143,14 @@ public class ContactSyncAdapterService extends Service {
}
public static void requestContactsSync() {
+ // if user has disabled automatic sync, do nothing
+ boolean isSyncEnabled = ContentResolver.getSyncAutomatically(new Account
+ (Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE), ContactsContract.AUTHORITY);
+
+ if (!isSyncEnabled) {
+ return;
+ }
+
Bundle extras = new Bundle();
// no need to wait, do it immediately
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java
index cf51e3b..c287f6b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java
@@ -38,6 +38,7 @@ import org.sufficientlysecure.keychain.operations.BackupOperation;
import org.sufficientlysecure.keychain.operations.ImportOperation;
import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation;
import org.sufficientlysecure.keychain.operations.InputDataOperation;
+import org.sufficientlysecure.keychain.operations.ChangeUnlockOperation;
import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
import org.sufficientlysecure.keychain.operations.RevokeOperation;
import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
@@ -116,6 +117,8 @@ public class KeychainService extends Service implements Progressable {
op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof SaveKeyringParcel) {
op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled);
+ } else if (inputParcel instanceof ChangeUnlockParcel) {
+ op = new ChangeUnlockOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof RevokeKeyringParcel) {
op = new RevokeOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof CertifyActionsParcel) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java
index 1c59782..b71fbad 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java
@@ -11,11 +11,14 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.PeriodicSync;
import android.content.SyncResult;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -35,6 +38,7 @@ import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.receiver.NetworkReceiver;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
@@ -68,7 +72,7 @@ public class KeyserverSyncAdapterService extends Service {
private static final String ACTION_IGNORE_TOR = "ignore_tor";
private static final String ACTION_UPDATE_ALL = "update_all";
- private static final String ACTION_SYNC_NOW = "sync_now";
+ public static final String ACTION_SYNC_NOW = "sync_now";
private static final String ACTION_DISMISS_NOTIFICATION = "cancel_sync";
private static final String ACTION_START_ORBOT = "start_orbot";
private static final String ACTION_CANCEL = "cancel";
@@ -176,8 +180,25 @@ public class KeyserverSyncAdapterService extends Service {
@Override
public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
- Log.d(Constants.TAG, "Performing a keyserver sync!");
+ Preferences prefs = Preferences.getPreferences(getContext());
+
+ // for a wifi-ONLY sync
+ if (prefs.getWifiOnlySync()) {
+
+ ConnectivityManager connMgr = (ConnectivityManager)
+ getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+ boolean isNotOnWifi = !(networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
+ boolean isNotConnected = !(networkInfo.isConnected());
+
+ // if Wi-Fi connection doesn't exist then receiver is enabled
+ if (isNotOnWifi && isNotConnected) {
+ new NetworkReceiver().setWifiReceiverComponent(true, getContext());
+ return;
+ }
+ }
+ Log.d(Constants.TAG, "Performing a keyserver sync!");
PowerManager pm = (PowerManager) KeyserverSyncAdapterService.this
.getSystemService(Context.POWER_SERVICE);
@SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20
@@ -509,6 +530,10 @@ public class KeyserverSyncAdapterService extends Service {
return builder.build();
}
+ /**
+ * creates a new sync if one does not exist, or updates an existing sync if the sync interval
+ * has changed.
+ */
public static void enableKeyserverSync(Context context) {
Account account = KeychainApplication.createAccountIfNecessary(context);
@@ -519,12 +544,26 @@ public class KeyserverSyncAdapterService extends Service {
ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY, true);
- ContentResolver.addPeriodicSync(
- account,
- Constants.PROVIDER_AUTHORITY,
- new Bundle(),
- SYNC_INTERVAL
- );
+
+ boolean intervalChanged = false;
+ boolean syncExists = Preferences.getKeyserverSyncEnabled(context);
+
+ if (syncExists) {
+ long oldInterval = ContentResolver.getPeriodicSyncs(
+ account, Constants.PROVIDER_AUTHORITY).get(0).period;
+ if (oldInterval != SYNC_INTERVAL) {
+ intervalChanged = true;
+ }
+ }
+
+ if (!syncExists || intervalChanged) {
+ ContentResolver.addPeriodicSync(
+ account,
+ Constants.PROVIDER_AUTHORITY,
+ new Bundle(),
+ SYNC_INTERVAL
+ );
+ }
}
private boolean isSyncEnabled() {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java
index 472eb3b..db6bbcb 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java
@@ -49,8 +49,6 @@ public class SaveKeyringParcel implements Parcelable {
// the key fingerprint, for safety. MUST be null for a new key.
public byte[] mFingerprint;
- public ChangeUnlockParcel mNewUnlock;
-
public ArrayList<String> mAddUserIds;
public ArrayList<WrappedUserAttribute> mAddUserAttribute;
public ArrayList<SubkeyAdd> mAddSubKeys;
@@ -70,6 +68,9 @@ public class SaveKeyringParcel implements Parcelable {
private boolean mUploadAtomic;
private String mKeyserver;
+ // private because we have to set other details like key id
+ private ChangeUnlockParcel mNewUnlock;
+
public SaveKeyringParcel() {
reset();
}
@@ -102,6 +103,18 @@ public class SaveKeyringParcel implements Parcelable {
mKeyserver = keysever;
}
+ public void setNewUnlock(ChangeUnlockParcel parcel) {
+ mNewUnlock = parcel;
+ }
+
+ public ChangeUnlockParcel getChangeUnlockParcel() {
+ if(mNewUnlock != null) {
+ mNewUnlock.mMasterKeyId = mMasterKeyId;
+ mNewUnlock.mFingerprint = mFingerprint;
+ }
+ return mNewUnlock;
+ }
+
public boolean isUpload() {
return mUpload;
}
@@ -344,64 +357,6 @@ public class SaveKeyringParcel implements Parcelable {
// BRAINPOOL_P256, BRAINPOOL_P384, BRAINPOOL_P512
}
- /** This subclass contains information on how the passphrase should be changed.
- *
- * If no changes are to be made, this class should NOT be used!
- *
- * At this point, there must be *exactly one* non-null value here, which specifies the type
- * of unlocking mechanism to use.
- *
- */
- public static class ChangeUnlockParcel implements Parcelable {
-
- // The new passphrase to use
- public final Passphrase mNewPassphrase;
- // A new pin to use. Must only contain [0-9]+
- public final Passphrase mNewPin;
-
- public ChangeUnlockParcel(Passphrase newPassphrase) {
- this(newPassphrase, null);
- }
- public ChangeUnlockParcel(Passphrase newPassphrase, Passphrase newPin) {
- if (newPassphrase == null && newPin == null) {
- throw new RuntimeException("Cannot set both passphrase and pin. THIS IS A BUG!");
- }
- mNewPassphrase = newPassphrase;
- mNewPin = newPin;
- }
-
- public ChangeUnlockParcel(Parcel source) {
- mNewPassphrase = source.readParcelable(Passphrase.class.getClassLoader());
- mNewPin = source.readParcelable(Passphrase.class.getClassLoader());
- }
-
- @Override
- public void writeToParcel(Parcel destination, int flags) {
- destination.writeParcelable(mNewPassphrase, flags);
- destination.writeParcelable(mNewPin, flags);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public static final Creator<ChangeUnlockParcel> CREATOR = new Creator<ChangeUnlockParcel>() {
- public ChangeUnlockParcel createFromParcel(final Parcel source) {
- return new ChangeUnlockParcel(source);
- }
- public ChangeUnlockParcel[] newArray(final int size) {
- return new ChangeUnlockParcel[size];
- }
- };
-
- public String toString() {
- return mNewPassphrase != null
- ? ("passphrase (" + mNewPassphrase + ")")
- : ("pin (" + mNewPin + ")");
- }
-
- }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java
index 8494189..080c34c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java
@@ -17,18 +17,18 @@
package org.sufficientlysecure.keychain.service.input;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import org.sufficientlysecure.keychain.util.ParcelableProxy;
-import org.sufficientlysecure.keychain.util.Passphrase;
-import java.net.Proxy;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.sufficientlysecure.keychain.util.ParcelableProxy;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
/**
* This is a base class for the input of crypto operations.
*/
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java
index 429d7a7..84c139d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java
@@ -14,8 +14,8 @@ import java.util.Date;
public class RequiredInputParcel implements Parcelable {
public enum RequiredInputType {
- PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, NFC_SIGN, NFC_DECRYPT,
- NFC_MOVE_KEY_TO_CARD, NFC_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY,
+ PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, SECURITY_TOKEN_SIGN, SECURITY_TOKEN_DECRYPT,
+ SECURITY_TOKEN_MOVE_KEY_TO_CARD, SECURITY_TOKEN_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY,
}
public Date mSignatureTime;
@@ -89,22 +89,22 @@ public class RequiredInputParcel implements Parcelable {
return new RequiredInputParcel(RequiredInputType.ENABLE_ORBOT, null, null, null, 0L, 0L);
}
- public static RequiredInputParcel createNfcSignOperation(
+ public static RequiredInputParcel createSecurityTokenSignOperation(
long masterKeyId, long subKeyId,
byte[] inputHash, int signAlgo, Date signatureTime) {
- return new RequiredInputParcel(RequiredInputType.NFC_SIGN,
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_SIGN,
new byte[][] { inputHash }, new int[] { signAlgo },
signatureTime, masterKeyId, subKeyId);
}
- public static RequiredInputParcel createNfcDecryptOperation(
+ public static RequiredInputParcel createSecurityTokenDecryptOperation(
long masterKeyId, long subKeyId, byte[] encryptedSessionKey) {
- return new RequiredInputParcel(RequiredInputType.NFC_DECRYPT,
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_DECRYPT,
new byte[][] { encryptedSessionKey }, null, null, masterKeyId, subKeyId);
}
- public static RequiredInputParcel createNfcReset() {
- return new RequiredInputParcel(RequiredInputType.NFC_RESET_CARD,
+ public static RequiredInputParcel createSecurityTokenReset() {
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_RESET_CARD,
null, null, null, null, null);
}
@@ -188,14 +188,14 @@ public class RequiredInputParcel implements Parcelable {
}
};
- public static class NfcSignOperationsBuilder {
+ public static class SecurityTokenSignOperationsBuilder {
Date mSignatureTime;
ArrayList<Integer> mSignAlgos = new ArrayList<>();
ArrayList<byte[]> mInputHashes = new ArrayList<>();
long mMasterKeyId;
long mSubKeyId;
- public NfcSignOperationsBuilder(Date signatureTime, long masterKeyId, long subKeyId) {
+ public SecurityTokenSignOperationsBuilder(Date signatureTime, long masterKeyId, long subKeyId) {
mSignatureTime = signatureTime;
mMasterKeyId = masterKeyId;
mSubKeyId = subKeyId;
@@ -209,7 +209,7 @@ public class RequiredInputParcel implements Parcelable {
signAlgos[i] = mSignAlgos.get(i);
}
- return new RequiredInputParcel(RequiredInputType.NFC_SIGN,
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_SIGN,
inputHashes, signAlgos, mSignatureTime, mMasterKeyId, mSubKeyId);
}
@@ -222,7 +222,7 @@ public class RequiredInputParcel implements Parcelable {
if (!mSignatureTime.equals(input.mSignatureTime)) {
throw new AssertionError("input times must match, this is a programming error!");
}
- if (input.mType != RequiredInputType.NFC_SIGN) {
+ if (input.mType != RequiredInputType.SECURITY_TOKEN_SIGN) {
throw new AssertionError("operation types must match, this is a progrmming error!");
}
@@ -238,13 +238,13 @@ public class RequiredInputParcel implements Parcelable {
}
- public static class NfcKeyToCardOperationsBuilder {
+ public static class SecurityTokenKeyToCardOperationsBuilder {
ArrayList<byte[]> mSubkeysToExport = new ArrayList<>();
Long mMasterKeyId;
byte[] mPin;
byte[] mAdminPin;
- public NfcKeyToCardOperationsBuilder(Long masterKeyId) {
+ public SecurityTokenKeyToCardOperationsBuilder(Long masterKeyId) {
mMasterKeyId = masterKeyId;
}
@@ -264,7 +264,7 @@ public class RequiredInputParcel implements Parcelable {
ByteBuffer buf = ByteBuffer.wrap(mSubkeysToExport.get(0));
// We need to pass in a subkey here...
- return new RequiredInputParcel(RequiredInputType.NFC_MOVE_KEY_TO_CARD,
+ return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD,
inputData, null, null, mMasterKeyId, buf.getLong());
}
@@ -287,7 +287,7 @@ public class RequiredInputParcel implements Parcelable {
if (!mMasterKeyId.equals(input.mMasterKeyId)) {
throw new AssertionError("Master keys must match, this is a programming error!");
}
- if (input.mType != RequiredInputType.NFC_MOVE_KEY_TO_CARD) {
+ if (input.mType != RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD) {
throw new AssertionError("Operation types must match, this is a programming error!");
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java
index 5534403..fb33256 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java
@@ -18,14 +18,6 @@
package org.sufficientlysecure.keychain.ui;
-import java.io.File;
-import java.io.IOException;
-import java.security.SecureRandom;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.Random;
-
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -43,7 +35,7 @@ import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
import android.text.Editable;
-import android.text.InputType;
+import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -53,12 +45,9 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
-import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
-import com.github.pinball83.maskededittext.MaskedEditText;
-
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.ExportResult;
@@ -73,6 +62,14 @@ import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator;
import org.sufficientlysecure.keychain.util.FileHelper;
import org.sufficientlysecure.keychain.util.Passphrase;
+import java.io.File;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Random;
+
public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringParcel, ExportResult>
implements OnBackStackChangedListener {
@@ -100,7 +97,7 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
String mBackupCode;
private boolean mExecuteBackupOperation;
- private MaskedEditText mCodeEditText;
+ private EditText[] mCodeEditText;
private ToolableViewAnimator mStatusAnimator, mTitleAnimator, mCodeFieldsAnimator;
private Integer mBackStackLevel;
@@ -152,8 +149,13 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
boolean newCheckedState = !item.isChecked();
item.setChecked(newCheckedState);
mDebugModeAcceptAnyCode = newCheckedState;
- if (newCheckedState) {
- mCodeEditText.setText("ABCD-EFGH-IJKL-MNOP-QRST-UVWX");
+ if (newCheckedState && TextUtils.isEmpty(mCodeEditText[0].getText())) {
+ mCodeEditText[0].setText("ABCD");
+ mCodeEditText[1].setText("EFGH");
+ mCodeEditText[2].setText("IJKL");
+ mCodeEditText[3].setText("MNOP");
+ mCodeEditText[4].setText("QRST");
+ mCodeEditText[5].setText("UVWX");
Notify.create(getActivity(), "Actual backup code is all 'A's", Style.WARN).show();
}
return true;
@@ -177,11 +179,9 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
mTitleAnimator.setDisplayedChild(1, animate);
mStatusAnimator.setDisplayedChild(1, animate);
mCodeFieldsAnimator.setDisplayedChild(1, animate);
- // use non-breaking spaces to enlarge the empty EditText appropriately
- String empty = "\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0" +
- "-\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0" +
- "-\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0";
- mCodeEditText.setText(empty);
+ for (EditText editText : mCodeEditText) {
+ editText.setText("");
+ }
pushBackStackEntry();
@@ -195,7 +195,7 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
hideKeyboard();
if (animate) {
- @ColorInt int black = mCodeEditText.getCurrentTextColor();
+ @ColorInt int black = mCodeEditText[0].getCurrentTextColor();
@ColorInt int red = getResources().getColor(R.color.android_red_dark);
animateFlashText(mCodeEditText, black, red, false);
}
@@ -214,14 +214,18 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
hideKeyboard();
- mCodeEditText.setEnabled(false);
+ for (EditText editText : mCodeEditText) {
+ editText.setEnabled(false);
+ }
@ColorInt int green = getResources().getColor(R.color.android_green_dark);
if (animate) {
- @ColorInt int black = mCodeEditText.getCurrentTextColor();
+ @ColorInt int black = mCodeEditText[0].getCurrentTextColor();
animateFlashText(mCodeEditText, black, green, true);
} else {
- mCodeEditText.setTextColor(green);
+ for (TextView textView : mCodeEditText) {
+ textView.setTextColor(green);
+ }
}
popBackStackNoAction();
@@ -257,22 +261,38 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
mExportSecret = args.getBoolean(ARG_EXPORT_SECRET);
mExecuteBackupOperation = args.getBoolean(ARG_EXECUTE_BACKUP_OPERATION, true);
- // NOTE: order of these method calls matter, see setupAutomaticLinebreak()
- mCodeEditText = (MaskedEditText) view.findViewById(R.id.backup_code_input);
- mCodeEditText.setInputType(
- InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
- setupAutomaticLinebreak(mCodeEditText);
- mCodeEditText.setImeOptions(EditorInfo.IME_ACTION_DONE);
- setupEditTextSuccessListener(mCodeEditText);
-
- TextView codeDisplayText = (TextView) view.findViewById(R.id.backup_code_display);
- setupAutomaticLinebreak(codeDisplayText);
+ mCodeEditText = new EditText[6];
+ mCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1);
+ mCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2);
+ mCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3);
+ mCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4);
+ mCodeEditText[4] = (EditText) view.findViewById(R.id.backup_code_5);
+ mCodeEditText[5] = (EditText) view.findViewById(R.id.backup_code_6);
+
+ {
+ TextView[] codeDisplayText = new TextView[6];
+ codeDisplayText[0] = (TextView) view.findViewById(R.id.backup_code_display_1);
+ codeDisplayText[1] = (TextView) view.findViewById(R.id.backup_code_display_2);
+ codeDisplayText[2] = (TextView) view.findViewById(R.id.backup_code_display_3);
+ codeDisplayText[3] = (TextView) view.findViewById(R.id.backup_code_display_4);
+ codeDisplayText[4] = (TextView) view.findViewById(R.id.backup_code_display_5);
+ codeDisplayText[5] = (TextView) view.findViewById(R.id.backup_code_display_6);
+
+ // set backup code in code TextViews
+ char[] backupCode = mBackupCode.toCharArray();
+ for (int i = 0; i < codeDisplayText.length; i++) {
+ codeDisplayText[i].setText(backupCode, i * 5, 4);
+ }
- // set background to null in TextViews - this will retain padding from EditText style!
- // noinspection deprecation, setBackground(Drawable) is API level >=16
- codeDisplayText.setBackgroundDrawable(null);
+ // set background to null in TextViews - this will retain padding from EditText style!
+ for (TextView textView : codeDisplayText) {
+ // noinspection deprecation, setBackground(Drawable) is API level >=16
+ textView.setBackgroundDrawable(null);
+ }
+ }
- codeDisplayText.setText(mBackupCode);
+ setupEditTextFocusNext(mCodeEditText);
+ setupEditTextSuccessListener(mCodeEditText);
mStatusAnimator = (ToolableViewAnimator) view.findViewById(R.id.status_animator);
mTitleAnimator = (ToolableViewAnimator) view.findViewById(R.id.title_animator);
@@ -350,67 +370,76 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
outState.putInt(ARG_BACK_STACK, mBackStackLevel == null ? -1 : mBackStackLevel);
}
- /**
- * Automatic line break with max 6 lines for smaller displays
- * <p/>
- * NOTE: I was not able to get this behaviour using XML!
- * Looks like the order of these method calls matter, see http://stackoverflow.com/a/11171307
- */
- private void setupAutomaticLinebreak(TextView textview) {
- textview.setSingleLine(true);
- textview.setMaxLines(6);
- textview.setHorizontallyScrolling(false);
- }
+ private void setupEditTextSuccessListener(final EditText[] backupCodes) {
+ for (EditText backupCode : backupCodes) {
- private void setupEditTextSuccessListener(final MaskedEditText backupCode) {
- backupCode.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ backupCode.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 onTextChanged(CharSequence s, int start, int before, int count) {
+ }
- @Override
- public void afterTextChanged(Editable s) {
- boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT
- || mCurrentState == BackupCodeState.STATE_INPUT_ERROR;
- boolean partIsComplete = (backupCode.getText().toString().indexOf(' ') == -1)
- && (backupCode.getText().toString().indexOf('\u00a0') == -1);
- if (!inInputState || !partIsComplete) {
- return;
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (s.length() > 4) {
+ throw new AssertionError("max length of each field is 4!");
+ }
+
+ boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT
+ || mCurrentState == BackupCodeState.STATE_INPUT_ERROR;
+ boolean partIsComplete = s.length() == 4;
+ if (!inInputState || !partIsComplete) {
+ return;
+ }
+
+ checkIfCodeIsCorrect();
}
+ });
- checkIfCodeIsCorrect(backupCode);
- }
- });
+ }
}
- private void checkIfCodeIsCorrect(EditText backupCode) {
+ private void checkIfCodeIsCorrect() {
if (Constants.DEBUG && mDebugModeAcceptAnyCode) {
switchState(BackupCodeState.STATE_OK, true);
return;
}
- if (backupCode.toString().equals(mBackupCode)) {
+ StringBuilder backupCodeInput = new StringBuilder(26);
+ for (EditText editText : mCodeEditText) {
+ if (editText.getText().length() < 4) {
+ return;
+ }
+ backupCodeInput.append(editText.getText());
+ backupCodeInput.append('-');
+ }
+ backupCodeInput.deleteCharAt(backupCodeInput.length() - 1);
+
+ // if they don't match, do nothing
+ if (backupCodeInput.toString().equals(mBackupCode)) {
switchState(BackupCodeState.STATE_OK, true);
return;
}
switchState(BackupCodeState.STATE_INPUT_ERROR, true);
+
}
private static void animateFlashText(
- final TextView textView, int color1, int color2, boolean staySecondColor) {
+ final TextView[] textViews, int color1, int color2, boolean staySecondColor) {
ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), color1, color2);
anim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
- textView.setTextColor((Integer) animator.getAnimatedValue());
+ for (TextView textView : textViews) {
+ textView.setTextColor((Integer) animator.getAnimatedValue());
+ }
}
});
anim.setRepeatMode(ValueAnimator.REVERSE);
@@ -421,6 +450,34 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
}
+ private static void setupEditTextFocusNext(final EditText[] backupCodes) {
+ for (int i = 0; i < backupCodes.length - 1; i++) {
+
+ final int next = i + 1;
+
+ backupCodes[i].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) {
+ boolean inserting = before < count;
+ boolean cursorAtEnd = (start + count) == 4;
+
+ if (inserting && cursorAtEnd) {
+ backupCodes[next].requestFocus();
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ }
+ }
+
private void pushBackStackEntry() {
if (mBackStackLevel != null) {
return;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java
index 3845e07..0914971 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java
@@ -28,7 +28,8 @@ import org.sufficientlysecure.keychain.ui.base.BaseActivity;
public class CertifyKeyActivity extends BaseActivity {
public static final String EXTRA_RESULT = "operation_result";
- public static final String EXTRA_KEY_IDS = "extra_key_ids";
+ // For sending masterKeyIds to MultiUserIdsFragment to display list of keys
+ public static final String EXTRA_KEY_IDS = MultiUserIdsFragment.EXTRA_KEY_IDS ;
public static final String EXTRA_CERTIFY_KEY_ID = "certify_key_id";
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java
index 357b445..ad39ff4 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java
@@ -62,58 +62,26 @@ import java.util.ArrayList;
import java.util.Date;
public class CertifyKeyFragment
- extends CachingCryptoOperationFragment<CertifyActionsParcel, CertifyResult>
- implements LoaderManager.LoaderCallbacks<Cursor> {
-
- public static final String ARG_CHECK_STATES = "check_states";
+ extends CachingCryptoOperationFragment<CertifyActionsParcel, CertifyResult> {
private CheckBox mUploadKeyCheckbox;
- ListView mUserIds;
private CertifyKeySpinner mCertifyKeySpinner;
- private long[] mPubMasterKeyIds;
-
- public static final String[] USER_IDS_PROJECTION = new String[]{
- UserPackets._ID,
- UserPackets.MASTER_KEY_ID,
- UserPackets.USER_ID,
- UserPackets.IS_PRIMARY,
- UserPackets.IS_REVOKED
- };
- private static final int INDEX_MASTER_KEY_ID = 1;
- private static final int INDEX_USER_ID = 2;
- @SuppressWarnings("unused")
- private static final int INDEX_IS_PRIMARY = 3;
- @SuppressWarnings("unused")
- private static final int INDEX_IS_REVOKED = 4;
-
- private MultiUserIdsAdapter mUserIdsAdapter;
+ private MultiUserIdsFragment mMultiUserIdsFragment;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- mPubMasterKeyIds = getActivity().getIntent().getLongArrayExtra(CertifyKeyActivity.EXTRA_KEY_IDS);
- if (mPubMasterKeyIds == null) {
- Log.e(Constants.TAG, "List of key ids to certify missing!");
- getActivity().finish();
- return;
- }
-
- ArrayList<Boolean> checkedStates;
- if (savedInstanceState != null) {
- checkedStates = (ArrayList<Boolean>) savedInstanceState.getSerializable(ARG_CHECK_STATES);
- // key spinner and the checkbox keep their own state
- } else {
- checkedStates = null;
-
+ if (savedInstanceState == null) {
// preselect certify key id if given
long certifyKeyId = getActivity().getIntent()
.getLongExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, Constants.key.none);
if (certifyKeyId != Constants.key.none) {
try {
- CachedPublicKeyRing key = (new ProviderHelper(getActivity())).getCachedPublicKeyRing(certifyKeyId);
+ CachedPublicKeyRing key = (new ProviderHelper(getActivity()))
+ .getCachedPublicKeyRing(certifyKeyId);
if (key.canCertify()) {
mCertifyKeySpinner.setPreSelectedKeyId(certifyKeyId);
}
@@ -121,15 +89,8 @@ public class CertifyKeyFragment
Log.e(Constants.TAG, "certify certify check failed", e);
}
}
-
}
- mUserIdsAdapter = new MultiUserIdsAdapter(getActivity(), null, 0, checkedStates);
- mUserIds.setAdapter(mUserIdsAdapter);
- mUserIds.setDividerHeight(0);
-
- getLoaderManager().initLoader(0, null, this);
-
OperationResult result = getActivity().getIntent().getParcelableExtra(CertifyKeyActivity.EXTRA_RESULT);
if (result != null) {
// display result from import
@@ -138,21 +99,13 @@ public class CertifyKeyFragment
}
@Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- ArrayList<Boolean> states = mUserIdsAdapter.getCheckStates();
- // no proper parceling method available :(
- outState.putSerializable(ARG_CHECK_STATES, states);
- }
-
- @Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.certify_key_fragment, null);
mCertifyKeySpinner = (CertifyKeySpinner) view.findViewById(R.id.certify_key_spinner);
mUploadKeyCheckbox = (CheckBox) view.findViewById(R.id.sign_key_upload_checkbox);
- mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
+ mMultiUserIdsFragment = (MultiUserIdsFragment)
+ getChildFragmentManager().findFragmentById(R.id.multi_user_ids_fragment);
// make certify image gray, like action icons
ImageView vActionCertifyImage =
@@ -184,127 +137,10 @@ public class CertifyKeyFragment
}
@Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- Uri uri = UserPackets.buildUserIdsUri();
-
- String selection, ids[];
- {
- // generate placeholders and string selection args
- ids = new String[mPubMasterKeyIds.length];
- StringBuilder placeholders = new StringBuilder("?");
- for (int i = 0; i < mPubMasterKeyIds.length; i++) {
- ids[i] = Long.toString(mPubMasterKeyIds[i]);
- if (i != 0) {
- placeholders.append(",?");
- }
- }
- // put together selection string
- selection = UserPackets.IS_REVOKED + " = 0" + " AND "
- + Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID
- + " IN (" + placeholders + ")";
- }
-
- return new CursorLoader(getActivity(), uri,
- USER_IDS_PROJECTION, selection, ids,
- Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " ASC"
- + ", " + Tables.USER_PACKETS + "." + UserPackets.USER_ID + " ASC"
- );
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
-
- MatrixCursor matrix = new MatrixCursor(new String[]{
- "_id", "user_data", "grouped"
- }) {
- @Override
- public byte[] getBlob(int column) {
- return super.getBlob(column);
- }
- };
- data.moveToFirst();
-
- long lastMasterKeyId = 0;
- String lastName = "";
- ArrayList<String> uids = new ArrayList<>();
-
- boolean header = true;
-
- // Iterate over all rows
- while (!data.isAfterLast()) {
- long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
- String userId = data.getString(INDEX_USER_ID);
- KeyRing.UserId pieces = KeyRing.splitUserId(userId);
-
- // Two cases:
-
- boolean grouped = masterKeyId == lastMasterKeyId;
- boolean subGrouped = data.isFirst() || grouped && lastName.equals(pieces.name);
- // Remember for next loop
- lastName = pieces.name;
-
- Log.d(Constants.TAG, Long.toString(masterKeyId, 16) + (grouped ? "grouped" : "not grouped"));
-
- if (!subGrouped) {
- // 1. This name should NOT be grouped with the previous, so we flush the buffer
-
- Parcel p = Parcel.obtain();
- p.writeStringList(uids);
- byte[] d = p.marshall();
- p.recycle();
-
- matrix.addRow(new Object[]{
- lastMasterKeyId, d, header ? 1 : 0
- });
- // indicate that we have a header for this masterKeyId
- header = false;
-
- // Now clear the buffer, and add the new user id, for the next round
- uids.clear();
-
- }
-
- // 2. This name should be grouped with the previous, just add to buffer
- uids.add(userId);
- lastMasterKeyId = masterKeyId;
-
- // If this one wasn't grouped, the next one's gotta be a header
- if (!grouped) {
- header = true;
- }
-
- // Regardless of the outcome, move to next entry
- data.moveToNext();
-
- }
-
- // If there is anything left in the buffer, flush it one last time
- if (!uids.isEmpty()) {
-
- Parcel p = Parcel.obtain();
- p.writeStringList(uids);
- byte[] d = p.marshall();
- p.recycle();
-
- matrix.addRow(new Object[]{
- lastMasterKeyId, d, header ? 1 : 0
- });
-
- }
-
- mUserIdsAdapter.swapCursor(matrix);
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- mUserIdsAdapter.swapCursor(null);
- }
-
- @Override
public CertifyActionsParcel createOperationInput() {
// Bail out if there is not at least one user id selected
- ArrayList<CertifyAction> certifyActions = mUserIdsAdapter.getSelectedCertifyActions();
+ ArrayList<CertifyAction> certifyActions = mMultiUserIdsFragment.getSelectedCertifyActions();
if (certifyActions.isEmpty()) {
Notify.create(getActivity(), "No identities selected!",
Notify.Style.ERROR).show();
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java
index b1fec3a..b719173 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java
@@ -21,6 +21,7 @@ import android.content.Intent;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import org.sufficientlysecure.keychain.R;
@@ -28,7 +29,7 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
-import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
+import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.Preferences;
@@ -36,7 +37,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
import java.io.IOException;
import java.util.ArrayList;
-public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
+public class CreateKeyActivity extends BaseSecurityTokenActivity {
public static final String EXTRA_NAME = "name";
public static final String EXTRA_EMAIL = "email";
@@ -47,9 +48,9 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
public static final String EXTRA_SECURITY_TOKEN_PIN = "yubi_key_pin";
public static final String EXTRA_SECURITY_TOKEN_ADMIN_PIN = "yubi_key_admin_pin";
- public static final String EXTRA_NFC_USER_ID = "nfc_user_id";
- public static final String EXTRA_NFC_AID = "nfc_aid";
- public static final String EXTRA_NFC_FINGERPRINTS = "nfc_fingerprints";
+ public static final String EXTRA_SECURITY_TOKEN_USER_ID = "nfc_user_id";
+ public static final String EXTRA_SECURITY_TOKEN_AID = "nfc_aid";
+ public static final String EXTRA_SECURITY_FINGERPRINTS = "nfc_fingerprints";
public static final String FRAGMENT_TAG = "currentFragment";
@@ -66,8 +67,8 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
byte[] mScannedFingerprints;
- byte[] mNfcAid;
- String mNfcUserId;
+ byte[] mSecurityTokenAid;
+ String mSecurityTokenUserId;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -77,7 +78,7 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
// NOTE: ACTION_NDEF_DISCOVERED and not ACTION_TAG_DISCOVERED like in BaseNfcActivity
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
- mTagDispatcher.interceptIntent(getIntent());
+ mNfcTagDispatcher.interceptIntent(getIntent());
setTitle(R.string.title_manage_my_keys);
@@ -107,10 +108,10 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
mFirstTime = intent.getBooleanExtra(EXTRA_FIRST_TIME, false);
mCreateSecurityToken = intent.getBooleanExtra(EXTRA_CREATE_SECURITY_TOKEN, false);
- if (intent.hasExtra(EXTRA_NFC_FINGERPRINTS)) {
- byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_NFC_FINGERPRINTS);
- String nfcUserId = intent.getStringExtra(EXTRA_NFC_USER_ID);
- byte[] nfcAid = intent.getByteArrayExtra(EXTRA_NFC_AID);
+ if (intent.hasExtra(EXTRA_SECURITY_FINGERPRINTS)) {
+ byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_SECURITY_FINGERPRINTS);
+ String nfcUserId = intent.getStringExtra(EXTRA_SECURITY_TOKEN_USER_ID);
+ byte[] nfcAid = intent.getByteArrayExtra(EXTRA_SECURITY_TOKEN_AID);
if (containsKeys(nfcFingerprints)) {
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
@@ -143,24 +144,32 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
}
@Override
- protected void doNfcInBackground() throws IOException {
- if (mCurrentFragment instanceof NfcListenerFragment) {
- ((NfcListenerFragment) mCurrentFragment).doNfcInBackground();
+ protected void doSecurityTokenInBackground() throws IOException {
+ if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
+ ((SecurityTokenListenerFragment) mCurrentFragment).doSecurityTokenInBackground();
return;
}
- mScannedFingerprints = nfcGetFingerprints();
- mNfcAid = nfcGetAid();
- mNfcUserId = nfcGetUserId();
+ mScannedFingerprints = mSecurityTokenHelper.getFingerprints();
+ mSecurityTokenAid = mSecurityTokenHelper.getAid();
+ mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
}
@Override
- protected void onNfcPostExecute() {
- if (mCurrentFragment instanceof NfcListenerFragment) {
- ((NfcListenerFragment) mCurrentFragment).onNfcPostExecute();
+ protected void onSecurityTokenPostExecute() {
+ if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
+ ((SecurityTokenListenerFragment) mCurrentFragment).onSecurityTokenPostExecute();
return;
}
+ // We don't want get back to wait activity mainly because it looks weird with otg token
+ if (mCurrentFragment instanceof CreateSecurityTokenWaitFragment) {
+ // hack from http://stackoverflow.com/a/11253987
+ CreateSecurityTokenWaitFragment.sDisableFragmentAnimations = true;
+ getSupportFragmentManager().popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ CreateSecurityTokenWaitFragment.sDisableFragmentAnimations = false;
+ }
+
if (containsKeys(mScannedFingerprints)) {
try {
long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mScannedFingerprints);
@@ -169,15 +178,15 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
Intent intent = new Intent(this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mScannedFingerprints);
startActivity(intent);
finish();
} catch (PgpKeyNotFoundException e) {
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
- mScannedFingerprints, mNfcAid, mNfcUserId);
+ mScannedFingerprints, mSecurityTokenAid, mSecurityTokenUserId);
loadFragment(frag, FragAction.TO_RIGHT);
}
} else {
@@ -252,12 +261,11 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
// do it immediately!
getSupportFragmentManager().executePendingTransactions();
-
}
- interface NfcListenerFragment {
- void doNfcInBackground() throws IOException;
- void onNfcPostExecute();
+ interface SecurityTokenListenerFragment {
+ void doSecurityTokenInBackground() throws IOException;
+ void onSecurityTokenPostExecute();
}
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyEmailFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyEmailFragment.java
index b020a0d..b871f47 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyEmailFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyEmailFragment.java
@@ -44,7 +44,6 @@ import org.sufficientlysecure.keychain.ui.widget.EmailEditText;
import java.util.ArrayList;
import java.util.List;
-import java.util.regex.Pattern;
public class CreateKeyEmailFragment extends Fragment {
private CreateKeyActivity mCreateKeyActivity;
@@ -52,10 +51,6 @@ public class CreateKeyEmailFragment extends Fragment {
private ArrayList<EmailAdapter.ViewModel> mAdditionalEmailModels = new ArrayList<>();
private EmailAdapter mEmailAdapter;
- // NOTE: Do not use more complicated pattern like defined in android.util.Patterns.EMAIL_ADDRESS
- // EMAIL_ADDRESS fails for mails with umlauts for example
- private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\S]+@[\\S]+\\.[a-z]+$");
-
/**
* Creates new instance of this fragment
*/
@@ -76,16 +71,15 @@ public class CreateKeyEmailFragment extends Fragment {
* @return true if EditText is not empty
*/
private boolean isMainEmailValid(EditText editText) {
- boolean output = true;
- if (!checkEmail(editText.getText().toString(), false)) {
+ if (editText.getText().length() == 0) {
editText.setError(getString(R.string.create_key_empty));
editText.requestFocus();
- output = false;
- } else {
- editText.setError(null);
+ return false;
+ } else if (!checkEmail(editText.getText().toString(), false)){
+ return false;
}
-
- return output;
+ editText.setError(null);
+ return true;
}
@Override
@@ -146,10 +140,9 @@ public class CreateKeyEmailFragment extends Fragment {
* @return
*/
private boolean checkEmail(String email, boolean additionalEmail) {
- // check for email format or if the user did any input
- if (!isEmailFormatValid(email)) {
+ if (email.isEmpty()) {
Notify.create(getActivity(),
- getString(R.string.create_key_email_invalid_email),
+ getString(R.string.create_key_email_empty_email),
Notify.LENGTH_LONG, Notify.Style.ERROR).show(CreateKeyEmailFragment.this);
return false;
}
@@ -167,18 +160,6 @@ public class CreateKeyEmailFragment extends Fragment {
}
/**
- * Checks the email format
- * Uses the default Android Email Pattern
- *
- * @param email
- * @return
- */
- private boolean isEmailFormatValid(String email) {
- // check for email format or if the user did any input
- return !(email.length() == 0 || !EMAIL_PATTERN.matcher(email).matches());
- }
-
- /**
* Checks for duplicated emails inside the additional email adapter.
*
* @param email
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java
index b53bfc1..227d6fc 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java
@@ -44,9 +44,9 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
-import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
@@ -57,6 +57,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
import java.util.Date;
import java.util.Iterator;
+import java.util.regex.Pattern;
public class CreateKeyFinalFragment extends Fragment {
@@ -81,6 +82,10 @@ public class CreateKeyFinalFragment extends Fragment {
private OperationResult mQueuedFinishResult;
private EditKeyResult mQueuedDisplayResult;
+ // NOTE: Do not use more complicated pattern like defined in android.util.Patterns.EMAIL_ADDRESS
+ // EMAIL_ADDRESS fails for mails with umlauts for example
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\S]+@[\\S]+\\.[a-z]+$");
+
public static CreateKeyFinalFragment newInstance() {
CreateKeyFinalFragment frag = new CreateKeyFinalFragment();
frag.setRetainInstance(true);
@@ -106,7 +111,11 @@ public class CreateKeyFinalFragment extends Fragment {
CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity();
// set values
- mNameEdit.setText(createKeyActivity.mName);
+ if (createKeyActivity.mName != null) {
+ mNameEdit.setText(createKeyActivity.mName);
+ } else {
+ mNameEdit.setText(getString(R.string.user_id_no_name));
+ }
if (createKeyActivity.mAdditionalEmails != null && createKeyActivity.mAdditionalEmails.size() > 0) {
String emailText = createKeyActivity.mEmail + ", ";
Iterator<?> it = createKeyActivity.mAdditionalEmails.iterator();
@@ -122,6 +131,8 @@ public class CreateKeyFinalFragment extends Fragment {
mEmailEdit.setText(createKeyActivity.mEmail);
}
+ checkEmailValidity();
+
mCreateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -278,18 +289,20 @@ public class CreateKeyFinalFragment extends Fragment {
2048, null, KeyFlags.AUTHENTICATION, 0L));
// use empty passphrase
- saveKeyringParcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase(), null);
+ saveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(new Passphrase()));
} else {
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA,
- 4096, null, KeyFlags.CERTIFY_OTHER, 0L));
+ 3072, null, KeyFlags.CERTIFY_OTHER, 0L));
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA,
- 4096, null, KeyFlags.SIGN_DATA, 0L));
+ 3072, null, KeyFlags.SIGN_DATA, 0L));
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA,
- 4096, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L));
+ 3072, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L));
- saveKeyringParcel.mNewUnlock = createKeyActivity.mPassphrase != null
- ? new ChangeUnlockParcel(createKeyActivity.mPassphrase, null)
- : null;
+ if (createKeyActivity.mPassphrase != null) {
+ saveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(createKeyActivity.mPassphrase));
+ } else {
+ saveKeyringParcel.setNewUnlock(null);
+ }
}
String userId = KeyRing.createUserId(
new KeyRing.UserId(createKeyActivity.mName, createKeyActivity.mEmail, null)
@@ -309,6 +322,31 @@ public class CreateKeyFinalFragment extends Fragment {
return saveKeyringParcel;
}
+ private void checkEmailValidity() {
+ CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity();
+
+ boolean emailsValid = true;
+ if (!EMAIL_PATTERN.matcher(createKeyActivity.mEmail).matches()) {
+ emailsValid = false;
+ }
+ if (createKeyActivity.mAdditionalEmails != null && createKeyActivity.mAdditionalEmails.size() > 0) {
+ for (Iterator<?> it = createKeyActivity.mAdditionalEmails.iterator(); it.hasNext(); ) {
+ if (!EMAIL_PATTERN.matcher(it.next().toString()).matches()) {
+ emailsValid = false;
+ }
+ }
+ }
+ if (!emailsValid) {
+ mEmailEdit.setError(getString(R.string.create_key_final_email_valid_warning));
+ mEmailEdit.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mNameEdit.requestFocus(); // Workaround to remove focus from email
+ }
+ });
+ }
+ }
+
private void createKey() {
CreateKeyActivity activity = (CreateKeyActivity) getActivity();
if (activity == null) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyNameFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyNameFragment.java
index 7480367..3332b9c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyNameFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyNameFragment.java
@@ -18,13 +18,11 @@
package org.sufficientlysecure.keychain.ui;
import android.app.Activity;
-import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.EditText;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
@@ -50,27 +48,6 @@ public class CreateKeyNameFragment extends Fragment {
return frag;
}
- /**
- * Checks if text of given EditText is not empty. If it is empty an error is
- * set and the EditText gets the focus.
- *
- * @param context
- * @param editText
- * @return true if EditText is not empty
- */
- private static boolean isEditTextNotEmpty(Context context, EditText editText) {
- boolean output = true;
- if (editText.getText().length() == 0) {
- editText.setError(context.getString(R.string.create_key_empty));
- editText.requestFocus();
- output = false;
- } else {
- editText.setError(null);
- }
-
- return output;
- }
-
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.create_key_name_fragment, container, false);
@@ -109,13 +86,11 @@ public class CreateKeyNameFragment extends Fragment {
}
private void nextClicked() {
- if (isEditTextNotEmpty(getActivity(), mNameEdit)) {
- // save state
- mCreateKeyActivity.mName = mNameEdit.getText().toString();
+ // save state
+ mCreateKeyActivity.mName = mNameEdit.getText().length() == 0 ? null : mNameEdit.getText().toString();
- CreateKeyEmailFragment frag = CreateKeyEmailFragment.newInstance();
- mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
- }
+ CreateKeyEmailFragment frag = CreateKeyEmailFragment.newInstance();
+ mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java
index ea57fe5..6f35fdd 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenImportResetFragment.java
@@ -25,7 +25,6 @@ import java.util.ArrayList;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
-import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
@@ -43,7 +42,7 @@ import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
-import org.sufficientlysecure.keychain.ui.CreateKeyActivity.NfcListenerFragment;
+import org.sufficientlysecure.keychain.ui.CreateKeyActivity.SecurityTokenListenerFragment;
import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Preferences;
@@ -51,7 +50,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
public class CreateSecurityTokenImportResetFragment
extends QueueingCryptoOperationFragment<ImportKeyringParcel, ImportKeyResult>
- implements NfcListenerFragment {
+ implements SecurityTokenListenerFragment {
private static final int REQUEST_CODE_RESET = 0x00005001;
@@ -231,7 +230,7 @@ public class CreateSecurityTokenImportResetFragment
public void resetCard() {
Intent intent = new Intent(getActivity(), SecurityTokenOperationActivity.class);
- RequiredInputParcel resetP = RequiredInputParcel.createNfcReset();
+ RequiredInputParcel resetP = RequiredInputParcel.createSecurityTokenReset();
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, resetP);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, new CryptoInputParcel());
startActivityForResult(intent, REQUEST_CODE_RESET);
@@ -248,11 +247,11 @@ public class CreateSecurityTokenImportResetFragment
}
@Override
- public void doNfcInBackground() throws IOException {
+ public void doSecurityTokenInBackground() throws IOException {
- mTokenFingerprints = mCreateKeyActivity.nfcGetFingerprints();
- mTokenAid = mCreateKeyActivity.nfcGetAid();
- mTokenUserId = mCreateKeyActivity.nfcGetUserId();
+ mTokenFingerprints = mCreateKeyActivity.getSecurityTokenHelper().getFingerprints();
+ mTokenAid = mCreateKeyActivity.getSecurityTokenHelper().getAid();
+ mTokenUserId = mCreateKeyActivity.getSecurityTokenHelper().getUserId();
byte[] fp = new byte[20];
ByteBuffer.wrap(fp).put(mTokenFingerprints, 0, 20);
@@ -260,7 +259,7 @@ public class CreateSecurityTokenImportResetFragment
}
@Override
- public void onNfcPostExecute() {
+ public void onSecurityTokenPostExecute() {
setData();
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java
index a3ea38e..7825027 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateSecurityTokenWaitFragment.java
@@ -17,23 +17,36 @@
package org.sufficientlysecure.keychain.ui;
-import android.app.Activity;
+import android.content.Context;
import android.os.Bundle;
+import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.animation.Animation;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
-
+import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
public class CreateSecurityTokenWaitFragment extends Fragment {
+ public static boolean sDisableFragmentAnimations = false;
+
CreateKeyActivity mCreateKeyActivity;
View mBackButton;
@Override
+ public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ if (this.getActivity() instanceof BaseSecurityTokenActivity) {
+ ((BaseSecurityTokenActivity) this.getActivity()).checkDeviceConnection();
+ }
+ }
+
+ @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.create_security_token_wait_fragment, container, false);
@@ -50,9 +63,22 @@ public class CreateSecurityTokenWaitFragment extends Fragment {
}
@Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
+ public void onAttach(Context context) {
+ super.onAttach(context);
mCreateKeyActivity = (CreateKeyActivity) getActivity();
}
+ /**
+ * hack from http://stackoverflow.com/a/11253987
+ */
+ @Override
+ public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
+ if (sDisableFragmentAnimations) {
+ Animation a = new Animation() {};
+ a.setDuration(0);
+ return a;
+ }
+ return super.onCreateAnimation(transit, enter, nextAnim);
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
index 2d94d0d..80fea7b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java
@@ -35,6 +35,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
@@ -49,8 +50,8 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
+import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
-import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.adapter.SubkeysAdapter;
@@ -128,7 +129,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.edit_key_fragment, null);
+ View view = inflater.inflate(R.layout.edit_key_fragment, superContainer, false);
mUserIdsList = (ListView) view.findViewById(R.id.edit_key_user_ids);
mSubkeysList = (ListView) view.findViewById(R.id.edit_key_keys);
@@ -338,10 +339,8 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
Bundle data = message.getData();
// cache new returned passphrase!
- mSaveKeyringParcel.mNewUnlock = new ChangeUnlockParcel(
- (Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE),
- null
- );
+ mSaveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(
+ (Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE)));
}
}
};
@@ -441,50 +440,45 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
}
break;
}
- case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: {
- // TODO: enable later when Admin PIN handling is resolved
- Notify.create(getActivity(),
- "This feature will be available in an upcoming OpenKeychain version.",
- Notify.Style.WARN).show();
- break;
+ case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN: {
+ SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
+ if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
+ secretKeyType == SecretKeyType.GNU_DUMMY) {
+ Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_stripped, Notify.Style.ERROR)
+ .show();
+ break;
+ }
+
+ int algorithm = mSubkeysAdapter.getAlgorithm(position);
+ if (algorithm != PublicKeyAlgorithmTags.RSA_GENERAL
+ && algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT
+ && algorithm != PublicKeyAlgorithmTags.RSA_SIGN) {
+ Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_algo, Notify.Style.ERROR)
+ .show();
+ break;
+ }
+
+ if (mSubkeysAdapter.getKeySize(position) != 2048) {
+ Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_size, Notify.Style.ERROR)
+ .show();
+ break;
+ }
-// Activity activity = EditKeyFragment.this.getActivity();
-// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
-// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
-// secretKeyType == SecretKeyType.GNU_DUMMY) {
-// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR)
-// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
-// break;
-// }
-// int algorithm = mSubkeysAdapter.getAlgorithm(position);
-// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN
-// if (algorithm != 1 && algorithm != 2 && algorithm != 3) {
-// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR)
-// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
-// break;
-// }
-// if (mSubkeysAdapter.getKeySize(position) != 2048) {
-// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR)
-// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
-// break;
-// }
-//
-//
-// SubkeyChange change;
-// change = mSaveKeyringParcel.getSubkeyChange(keyId);
-// if (change == null) {
-// mSaveKeyringParcel.mChangeSubKeys.add(
-// new SubkeyChange(keyId, false, true)
-// );
-// break;
-// }
-// // toggle
-// change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
-// if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
-// // User had chosen to strip key, but now wants to divert it.
-// change.mDummyStrip = false;
-// }
-// break;
+ SubkeyChange change;
+ change = mSaveKeyringParcel.getSubkeyChange(keyId);
+ if (change == null) {
+ mSaveKeyringParcel.mChangeSubKeys.add(
+ new SubkeyChange(keyId, false, true)
+ );
+ break;
+ }
+ // toggle
+ change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
+ if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
+ // User had chosen to strip key, but now wants to divert it.
+ change.mDummyStrip = false;
+ }
+ break;
}
}
getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad();
@@ -562,15 +556,9 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
}
private void addSubkey() {
- boolean willBeMasterKey;
- if (mSubkeysAdapter != null) {
- willBeMasterKey = mSubkeysAdapter.getCount() == 0 && mSubkeysAddedAdapter.getCount() == 0;
- } else {
- willBeMasterKey = mSubkeysAddedAdapter.getCount() == 0;
- }
-
+ // new subkey will never be a masterkey, as masterkey cannot be removed
AddSubkeyDialogFragment addSubkeyDialogFragment =
- AddSubkeyDialogFragment.newInstance(willBeMasterKey);
+ AddSubkeyDialogFragment.newInstance(false);
addSubkeyDialogFragment
.setOnAlgorithmSelectedListener(
new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java
index be08f6a..d5c5408 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java
@@ -247,10 +247,10 @@ public class EncryptFilesFragment
try {
mFilesAdapter.add(inputUri);
} catch (IOException e) {
+ String fileName = FileHelper.getFilename(getActivity(), inputUri);
Notify.create(getActivity(),
- getActivity().getString(R.string.error_file_added_already, FileHelper.getFilename(getActivity(), inputUri)),
+ getActivity().getString(R.string.error_file_added_already, fileName),
Notify.Style.ERROR).show(this);
- return;
}
// remove from pending input uris
@@ -729,6 +729,8 @@ public class EncryptFilesFragment
// make sure this is correct at this point
mAfterEncryptAction = AfterEncryptAction.SAVE;
cryptoOperation(new CryptoInputParcel(new Date()));
+ } else if (resultCode == Activity.RESULT_CANCELED) {
+ onCryptoOperationCancelled();
}
return;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java
index ca5d20f..5102209 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptModeAsymmetricFragment.java
@@ -21,6 +21,7 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ImageView;
import android.widget.ViewAnimator;
import com.tokenautocomplete.TokenCompleteTextView;
@@ -79,9 +80,6 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment {
mSignKeySpinner = (KeySpinner) view.findViewById(R.id.sign);
mEncryptKeyView = (EncryptKeyCompletionView) view.findViewById(R.id.recipient_list);
mEncryptKeyView.setThreshold(1); // Start working from first character
- // TODO: workaround for bug in TokenAutoComplete,
- // see https://github.com/open-keychain/open-keychain/issues/1636
- mEncryptKeyView.setDeletionStyle(TokenCompleteTextView.TokenDeleteStyle.ToString);
final ViewAnimator vSignatureIcon = (ViewAnimator) view.findViewById(R.id.result_signature_icon);
mSignKeySpinner.setOnKeyChangedListener(new OnKeyChangedListener() {
@@ -112,6 +110,14 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment {
}
});
+ ImageView addRecipientImgView = (ImageView) view.findViewById(R.id.add_recipient);
+ addRecipientImgView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mEncryptKeyView.showAllKeys();
+ }
+ });
+
return view;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
index f67c6a7..7d2d30c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
@@ -131,8 +131,11 @@ public class ImportKeysActivity extends BaseActivity
if (Intent.ACTION_VIEW.equals(action)) {
if (FacebookKeyserver.isFacebookHost(dataUri)) {
action = ACTION_IMPORT_KEY_FROM_FACEBOOK;
- } else if ("http".equals(scheme) || "https".equals(scheme)) {
+ } else if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
action = ACTION_SEARCH_KEYSERVER_FROM_URL;
+ } else if ("openpgp4fpr".equalsIgnoreCase(scheme)) {
+ action = ACTION_IMPORT_KEY_FROM_KEYSERVER;
+ extras.putString(EXTRA_FINGERPRINT, dataUri.getSchemeSpecificPart());
} else {
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
// delegate action to ACTION_IMPORT_KEY
@@ -413,11 +416,18 @@ public class ImportKeysActivity extends BaseActivity
intent.putExtra(ImportKeyResult.EXTRA_RESULT, result);
setResult(RESULT_OK, intent);
finish();
- return;
+ } else if (result.isOkNew() || result.isOkUpdated()) {
+ // User has successfully imported a key, hide first time dialog
+ Preferences.getPreferences(this).setFirstTime(false);
+
+ // Close activities opened for importing keys and go to the list of keys
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ } else {
+ result.createNotify(ImportKeysActivity.this)
+ .show((ViewGroup) findViewById(R.id.import_snackbar));
}
-
- result.createNotify(ImportKeysActivity.this)
- .show((ViewGroup) findViewById(R.id.import_snackbar));
}
// methods from CryptoOperationHelper.Callback
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java
index 8de60df..133cf29 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java
@@ -17,29 +17,46 @@
package org.sufficientlysecure.keychain.ui;
+import android.Manifest;
import android.app.Activity;
+import android.content.ContentResolver;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
+import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Toast;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
+import org.sufficientlysecure.keychain.ui.ImportKeysListFragment.BytesLoaderState;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.FileHelper;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
public class ImportKeysFileFragment extends Fragment {
private ImportKeysActivity mImportActivity;
private View mBrowse;
private View mClipboardButton;
- public static final int REQUEST_CODE_FILE = 0x00007003;
+ private Uri mCurrentUri;
+
+ private static final int REQUEST_CODE_FILE = 0x00007003;
+ private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12;
/**
* Creates new instance of this fragment
@@ -83,10 +100,10 @@ public class ImportKeysFileFragment extends Fragment {
sendText = clipboardText.toString();
sendText = PgpHelper.getPgpKeyContent(sendText);
if (sendText == null) {
- Notify.create(mImportActivity, "Bad data!", Style.ERROR).show();
+ Notify.create(mImportActivity, R.string.error_bad_data, Style.ERROR).show();
return;
}
- mImportActivity.loadCallback(new ImportKeysListFragment.BytesLoaderState(sendText.getBytes(), null));
+ mImportActivity.loadCallback(new BytesLoaderState(sendText.getBytes(), null));
}
}
});
@@ -106,11 +123,12 @@ public class ImportKeysFileFragment extends Fragment {
switch (requestCode) {
case REQUEST_CODE_FILE: {
if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
+ mCurrentUri = data.getData();
- // load data
- mImportActivity.loadCallback(new ImportKeysListFragment.BytesLoaderState(null, data.getData()));
+ if (checkAndRequestReadPermission(mCurrentUri)) {
+ startImportingKeys();
+ }
}
-
break;
}
@@ -121,4 +139,77 @@ public class ImportKeysFileFragment extends Fragment {
}
}
+ private void startImportingKeys() {
+ boolean isEncrypted;
+ try {
+ isEncrypted = FileHelper.isEncryptedFile(mImportActivity, mCurrentUri);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error opening file", e);
+
+ Notify.create(mImportActivity, R.string.error_bad_data, Style.ERROR).show();
+ return;
+ }
+
+ if (isEncrypted) {
+ Intent intent = new Intent(mImportActivity, DecryptActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setData(mCurrentUri);
+ startActivity(intent);
+ } else {
+ mImportActivity.loadCallback(new BytesLoaderState(null, mCurrentUri));
+ }
+ }
+
+ /**
+ * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris.
+ * <p/>
+ * This method returns true on Android < 6, or if permission is already granted. It
+ * requests the permission and returns false otherwise.
+ * <p/>
+ * see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html
+ */
+ private boolean checkAndRequestReadPermission(final Uri uri) {
+ if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ return true;
+ }
+
+ // Additional check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ return true;
+ }
+
+ if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+
+ requestPermissions(
+ new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
+ REQUEST_PERMISSION_READ_EXTERNAL_STORAGE);
+
+ return false;
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+
+ if (requestCode != REQUEST_PERMISSION_READ_EXTERNAL_STORAGE) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ return;
+ }
+
+ boolean permissionWasGranted = grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED;
+
+ if (permissionWasGranted) {
+ startImportingKeys();
+ } else {
+ Toast.makeText(getActivity(), R.string.error_denied_storage_permission, Toast.LENGTH_LONG).show();
+ getActivity().setResult(Activity.RESULT_CANCELED);
+ getActivity().finish();
+ }
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java
index b399af9..4d4219f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java
@@ -330,9 +330,16 @@ public class ImportKeysListFragment extends ListFragment implements
}
public void loadNew(LoaderState loaderState) {
-
mLoaderState = loaderState;
+ if (mLoaderState instanceof BytesLoaderState) {
+ BytesLoaderState ls = (BytesLoaderState) mLoaderState;
+
+ if ( ls.mDataUri != null && ! checkAndRequestReadPermission(ls.mDataUri)) {
+ return;
+ }
+ }
+
restartLoaders();
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java
index af60a1d..13df0b5 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java
@@ -40,11 +40,11 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment;
-import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
+import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Preferences;
-public class MainActivity extends BaseSecurityTokenNfcActivity implements FabContainer, OnBackStackChangedListener {
+public class MainActivity extends BaseSecurityTokenActivity implements FabContainer, OnBackStackChangedListener {
static final int ID_KEYS = 1;
static final int ID_ENCRYPT_DECRYPT = 2;
@@ -90,8 +90,9 @@ public class MainActivity extends BaseSecurityTokenNfcActivity implements FabCon
@Override
public boolean onItemClick(View view, int position, IDrawerItem drawerItem) {
if (drawerItem != null) {
+ PrimaryDrawerItem item = (PrimaryDrawerItem) drawerItem;
Intent intent = null;
- switch (drawerItem.getIdentifier()) {
+ switch ((int) item.getIdentifier()) {
case ID_KEYS:
onKeysSelected();
break;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MultiUserIdsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MultiUserIdsFragment.java
new file mode 100644
index 0000000..8ba695c
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MultiUserIdsFragment.java
@@ -0,0 +1,223 @@
+package org.sufficientlysecure.keychain.ui;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.support.annotation.Nullable;
+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.ViewGroup;
+import android.widget.ListView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.KeyRing;
+import org.sufficientlysecure.keychain.provider.KeychainContract;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase;
+import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
+import org.sufficientlysecure.keychain.ui.adapter.MultiUserIdsAdapter;
+import org.sufficientlysecure.keychain.util.Log;
+
+import java.util.ArrayList;
+
+public class MultiUserIdsFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>{
+ public static final String ARG_CHECK_STATES = "check_states";
+ public static final String EXTRA_KEY_IDS = "extra_key_ids";
+ private boolean checkboxVisibility = true;
+
+ ListView mUserIds;
+ private MultiUserIdsAdapter mUserIdsAdapter;
+
+ private long[] mPubMasterKeyIds;
+
+ public static final String[] USER_IDS_PROJECTION = new String[]{
+ KeychainContract.UserPackets._ID,
+ KeychainContract.UserPackets.MASTER_KEY_ID,
+ KeychainContract.UserPackets.USER_ID,
+ KeychainContract.UserPackets.IS_PRIMARY,
+ KeychainContract.UserPackets.IS_REVOKED
+ };
+ private static final int INDEX_MASTER_KEY_ID = 1;
+ private static final int INDEX_USER_ID = 2;
+ @SuppressWarnings("unused")
+ private static final int INDEX_IS_PRIMARY = 3;
+ @SuppressWarnings("unused")
+ private static final int INDEX_IS_REVOKED = 4;
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.multi_user_ids_fragment, null);
+
+ mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mPubMasterKeyIds = getActivity().getIntent().getLongArrayExtra(EXTRA_KEY_IDS);
+ if (mPubMasterKeyIds == null) {
+ Log.e(Constants.TAG, "List of key ids to certify missing!");
+ getActivity().finish();
+ return;
+ }
+
+ ArrayList<Boolean> checkedStates = null;
+ if (savedInstanceState != null) {
+ checkedStates = (ArrayList<Boolean>) savedInstanceState.getSerializable(ARG_CHECK_STATES);
+ }
+
+ mUserIdsAdapter = new MultiUserIdsAdapter(getActivity(), null, 0, checkedStates, checkboxVisibility);
+ mUserIds.setAdapter(mUserIdsAdapter);
+ mUserIds.setDividerHeight(0);
+
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ ArrayList<Boolean> states = mUserIdsAdapter.getCheckStates();
+ // no proper parceling method available :(
+ outState.putSerializable(ARG_CHECK_STATES, states);
+ }
+
+ public ArrayList<CertifyActionsParcel.CertifyAction> getSelectedCertifyActions() {
+ if (!checkboxVisibility) {
+ throw new AssertionError("Item selection not allowed");
+ }
+
+ return mUserIdsAdapter.getSelectedCertifyActions();
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ Uri uri = KeychainContract.UserPackets.buildUserIdsUri();
+
+ String selection, ids[];
+ {
+ // generate placeholders and string selection args
+ ids = new String[mPubMasterKeyIds.length];
+ StringBuilder placeholders = new StringBuilder("?");
+ for (int i = 0; i < mPubMasterKeyIds.length; i++) {
+ ids[i] = Long.toString(mPubMasterKeyIds[i]);
+ if (i != 0) {
+ placeholders.append(",?");
+ }
+ }
+ // put together selection string
+ selection = KeychainContract.UserPackets.IS_REVOKED + " = 0" + " AND "
+ + KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.MASTER_KEY_ID
+ + " IN (" + placeholders + ")";
+ }
+
+ return new CursorLoader(getActivity(), uri,
+ USER_IDS_PROJECTION, selection, ids,
+ KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.MASTER_KEY_ID + " ASC"
+ + ", " + KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.USER_ID + " ASC"
+ );
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+
+ MatrixCursor matrix = new MatrixCursor(new String[]{
+ "_id", "user_data", "grouped"
+ }) {
+ @Override
+ public byte[] getBlob(int column) {
+ return super.getBlob(column);
+ }
+ };
+ data.moveToFirst();
+
+ long lastMasterKeyId = 0;
+ String lastName = "";
+ ArrayList<String> uids = new ArrayList<>();
+
+ boolean header = true;
+
+ // Iterate over all rows
+ while (!data.isAfterLast()) {
+ long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
+ String userId = data.getString(INDEX_USER_ID);
+ KeyRing.UserId pieces = KeyRing.splitUserId(userId);
+
+ // Two cases:
+
+ boolean grouped = masterKeyId == lastMasterKeyId;
+ boolean subGrouped = data.isFirst() || grouped && lastName.equals(pieces.name);
+ // Remember for next loop
+ lastName = pieces.name;
+
+ Log.d(Constants.TAG, Long.toString(masterKeyId, 16) + (grouped ? "grouped" : "not grouped"));
+
+ if (!subGrouped) {
+ // 1. This name should NOT be grouped with the previous, so we flush the buffer
+
+ Parcel p = Parcel.obtain();
+ p.writeStringList(uids);
+ byte[] d = p.marshall();
+ p.recycle();
+
+ matrix.addRow(new Object[]{
+ lastMasterKeyId, d, header ? 1 : 0
+ });
+ // indicate that we have a header for this masterKeyId
+ header = false;
+
+ // Now clear the buffer, and add the new user id, for the next round
+ uids.clear();
+
+ }
+
+ // 2. This name should be grouped with the previous, just add to buffer
+ uids.add(userId);
+ lastMasterKeyId = masterKeyId;
+
+ // If this one wasn't grouped, the next one's gotta be a header
+ if (!grouped) {
+ header = true;
+ }
+
+ // Regardless of the outcome, move to next entry
+ data.moveToNext();
+
+ }
+
+ // If there is anything left in the buffer, flush it one last time
+ if (!uids.isEmpty()) {
+
+ Parcel p = Parcel.obtain();
+ p.writeStringList(uids);
+ byte[] d = p.marshall();
+ p.recycle();
+
+ matrix.addRow(new Object[]{
+ lastMasterKeyId, d, header ? 1 : 0
+ });
+
+ }
+
+ mUserIdsAdapter.swapCursor(matrix);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ mUserIdsAdapter.swapCursor(null);
+ }
+
+ public void setCheckboxVisibility(boolean checkboxVisibility) {
+ this.checkboxVisibility = checkboxVisibility;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
index fd4f271..2c562c3 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java
@@ -46,8 +46,6 @@ import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewAnimator;
-import com.github.pinball83.maskededittext.MaskedEditText;
-
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
@@ -60,7 +58,6 @@ import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
-import org.sufficientlysecure.keychain.remote.CryptoInputParcelCacheService;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
@@ -157,7 +154,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
public static class PassphraseDialogFragment extends DialogFragment implements TextView.OnEditorActionListener {
private EditText mPassphraseEditText;
private TextView mPassphraseText;
- private MaskedEditText mBackupCodeEditText;
+ private EditText[] mBackupCodeEditText;
private boolean mIsCancelled = false;
private RequiredInputParcel mRequiredInput;
@@ -184,13 +181,15 @@ public class PassphraseDialogActivity extends FragmentActivity {
View view = inflater.inflate(R.layout.passphrase_dialog_backup_code, null);
alert.setView(view);
- mBackupCodeEditText = (MaskedEditText) view.findViewById(R.id.backup_code);
- // NOTE: order of these method calls matter, see setupAutomaticLinebreak()
- mBackupCodeEditText.setInputType(
- InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
- setupAutomaticLinebreak(mBackupCodeEditText);
- mBackupCodeEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
- mBackupCodeEditText.setOnEditorActionListener(this);
+ mBackupCodeEditText = new EditText[6];
+ mBackupCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1);
+ mBackupCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2);
+ mBackupCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3);
+ mBackupCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4);
+ mBackupCodeEditText[4] = (EditText) view.findViewById(R.id.backup_code_5);
+ mBackupCodeEditText[5] = (EditText) view.findViewById(R.id.backup_code_6);
+
+ setupEditTextFocusNext(mBackupCodeEditText);
AlertDialog dialog = alert.create();
dialog.setButton(DialogInterface.BUTTON_POSITIVE,
@@ -281,28 +280,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
mPassphraseText.setText(message);
mPassphraseEditText.setHint(hint);
- // Hack to open keyboard.
- // This is the only method that I found to work across all Android versions
- // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/
- // Notes: * onCreateView can't be used because we want to add buttons to the dialog
- // * opening in onActivityCreated does not work on Android 4.4
- mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- mPassphraseEditText.post(new Runnable() {
- @Override
- public void run() {
- if (getActivity() == null || mPassphraseEditText == null) {
- return;
- }
- InputMethodManager imm = (InputMethodManager) getActivity()
- .getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT);
- }
- });
- }
- });
- mPassphraseEditText.requestFocus();
+ openKeyboard(mPassphraseEditText);
mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
mPassphraseEditText.setOnEditorActionListener(this);
@@ -325,17 +303,62 @@ public class PassphraseDialogActivity extends FragmentActivity {
}
/**
- * Automatic line break with max 6 lines for smaller displays
- * <p/>
- * NOTE: I was not able to get this behaviour using XML!
- * Looks like the order of these method calls matter, see http://stackoverflow.com/a/11171307
+ * Hack to open keyboard.
+ * This is the only method that I found to work across all Android versions
+ * http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/
+ * Notes:
+ * * onCreateView can't be used because we want to add buttons to the dialog
+ * * opening in onActivityCreated does not work on Android 4.4
*/
- private void setupAutomaticLinebreak(TextView textview) {
- textview.setSingleLine(true);
- textview.setMaxLines(6);
- textview.setHorizontallyScrolling(false);
+ private void openKeyboard(final TextView textView) {
+ textView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ textView.post(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null || textView == null) {
+ return;
+ }
+ InputMethodManager imm = (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(textView, InputMethodManager.SHOW_IMPLICIT);
+ }
+ });
+ }
+ });
+ textView.requestFocus();
+ }
+
+ private static void setupEditTextFocusNext(final EditText[] backupCodes) {
+ for (int i = 0; i < backupCodes.length - 1; i++) {
+
+ final int next = i + 1;
+
+ backupCodes[i].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) {
+ boolean inserting = before < count;
+ boolean cursorAtEnd = (start + count) == 4;
+
+ if (inserting && cursorAtEnd) {
+ backupCodes[next].requestFocus();
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ }
}
+
@Override
public void onStart() {
super.onStart();
@@ -347,8 +370,17 @@ public class PassphraseDialogActivity extends FragmentActivity {
public void onClick(View v) {
if (mRequiredInput.mType == RequiredInputType.BACKUP_CODE) {
- Passphrase passphrase =
- new Passphrase(mBackupCodeEditText.getText().toString());
+ StringBuilder backupCodeInput = new StringBuilder(26);
+ for (EditText editText : mBackupCodeEditText) {
+ if (editText.getText().length() < 4) {
+ return;
+ }
+ backupCodeInput.append(editText.getText());
+ backupCodeInput.append('-');
+ }
+ backupCodeInput.deleteCharAt(backupCodeInput.length() - 1);
+
+ Passphrase passphrase = new Passphrase(backupCodeInput.toString());
finishCaching(passphrase);
return;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/RedirectImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/RedirectImportKeysActivity.java
new file mode 100644
index 0000000..5cb680a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/RedirectImportKeysActivity.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.ui.base.BaseActivity;
+
+public class RedirectImportKeysActivity extends BaseActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ startQrCodeCaptureActivity();
+ }
+
+ private void startQrCodeCaptureActivity() {
+ final Intent scanQrCode = new Intent(this, ImportKeysProxyActivity.class);
+ scanQrCode.setAction(ImportKeysProxyActivity.ACTION_QR_CODE_API);
+
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.redirect_import_key_title)
+ .setMessage(R.string.redirect_import_key_message)
+ .setPositiveButton(R.string.redirect_import_key_yes, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // directly scan with OpenKeychain
+ startActivity(scanQrCode);
+ finish();
+ }
+ })
+ .setNegativeButton(R.string.redirect_import_key_no, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // close window
+ finish();
+ }
+ })
+ .show();
+ }
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java
index 78d82d4..4d07025 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SecurityTokenOperationActivity.java
@@ -3,6 +3,7 @@
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* Copyright (C) 2013-2014 Signe Rüsch
* Copyright (C) 2013-2014 Philipp Jakubeit
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,10 +36,12 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.securitytoken.KeyType;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
-import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
+import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.OrientationUtils;
@@ -55,7 +58,7 @@ import nordpol.android.NfcGuideView;
* NFC devices.
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
*/
-public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity {
+public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity {
public static final String EXTRA_REQUIRED_INPUT = "required_input";
public static final String EXTRA_CRYPTO_INPUT = "crypto_input";
@@ -69,8 +72,6 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
private RequiredInputParcel mRequiredInput;
- private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
-
private CryptoInputParcel mInputParcel;
@Override
@@ -137,9 +138,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
private void obtainPassphraseIfRequired() {
// obtain passphrase for this subkey
- if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD
- && mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_RESET_CARD) {
+ if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD
+ && mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_RESET_CARD) {
obtainSecurityTokenPin(mRequiredInput);
+ checkPinAvailability();
+ } else {
+ checkDeviceConnection();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (REQUEST_CODE_PIN == requestCode) {
+ checkPinAvailability();
+ }
+ }
+
+ private void checkPinAvailability() {
+ try {
+ Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
+ mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
+ if (passphrase != null) {
+ checkDeviceConnection();
+ }
+ } catch (PassphraseCacheService.KeyNotFoundException e) {
+ throw new AssertionError(
+ "tried to find passphrase for non-existing key. this is a programming error!");
}
}
@@ -149,39 +174,53 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
- public void onNfcPreExecute() {
+ public void onSecurityTokenPreExecute() {
// start with indeterminate progress
vAnimator.setDisplayedChild(1);
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.TRANSFERRING);
}
@Override
- protected void doNfcInBackground() throws IOException {
+ protected void doSecurityTokenInBackground() throws IOException {
switch (mRequiredInput.mType) {
- case NFC_DECRYPT: {
+ case SECURITY_TOKEN_DECRYPT: {
+ long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(
+ mSecurityTokenHelper.getKeyFingerprint(KeyType.ENCRYPT));
+
+ if (tokenKeyId != mRequiredInput.getSubKeyId()) {
+ throw new IOException(getString(R.string.error_wrong_security_token));
+ }
+
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] encryptedSessionKey = mRequiredInput.mInputData[i];
- byte[] decryptedSessionKey = nfcDecryptSessionKey(encryptedSessionKey);
+ byte[] decryptedSessionKey = mSecurityTokenHelper.decryptSessionKey(encryptedSessionKey);
mInputParcel.addCryptoData(encryptedSessionKey, decryptedSessionKey);
}
break;
}
- case NFC_SIGN: {
+ case SECURITY_TOKEN_SIGN: {
+ long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(
+ mSecurityTokenHelper.getKeyFingerprint(KeyType.SIGN));
+
+ if (tokenKeyId != mRequiredInput.getSubKeyId()) {
+ throw new IOException(getString(R.string.error_wrong_security_token));
+ }
+
mInputParcel.addSignatureTime(mRequiredInput.mSignatureTime);
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] hash = mRequiredInput.mInputData[i];
int algo = mRequiredInput.mSignAlgos[i];
- byte[] signedHash = nfcCalculateSignature(hash, algo);
+ byte[] signedHash = mSecurityTokenHelper.calculateSignature(hash, algo);
mInputParcel.addCryptoData(hash, signedHash);
}
break;
}
- case NFC_MOVE_KEY_TO_CARD: {
+ case SECURITY_TOKEN_MOVE_KEY_TO_CARD: {
// TODO: assume PIN and Admin PIN to be default for this operation
- mPin = new Passphrase("123456");
- mAdminPin = new Passphrase("12345678");
+ mSecurityTokenHelper.setPin(new Passphrase("123456"));
+ mSecurityTokenHelper.setAdminPin(new Passphrase("12345678"));
ProviderHelper providerHelper = new ProviderHelper(this);
CanonicalizedSecretKeyRing secretKeyRing;
@@ -202,11 +241,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
long subkeyId = buf.getLong();
CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId);
-
- long keyGenerationTimestampMillis = key.getCreationTime().getTime();
- long keyGenerationTimestamp = keyGenerationTimestampMillis / 1000;
- byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
- byte[] tokenSerialNumber = Arrays.copyOf(nfcGetAid(), 16);
+ byte[] tokenSerialNumber = Arrays.copyOf(mSecurityTokenHelper.getAid(), 16);
Passphrase passphrase;
try {
@@ -216,46 +251,20 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
throw new IOException("Unable to get cached passphrase!");
}
- if (key.canSign() || key.canCertify()) {
- if (shouldPutKey(key.getFingerprint(), 0)) {
- nfcPutKey(0xB6, key, passphrase);
- nfcPutData(0xCE, timestampBytes);
- nfcPutData(0xC7, key.getFingerprint());
- } else {
- throw new IOException("Key slot occupied; token must be reset to put new signature key.");
- }
- } else if (key.canEncrypt()) {
- if (shouldPutKey(key.getFingerprint(), 1)) {
- nfcPutKey(0xB8, key, passphrase);
- nfcPutData(0xCF, timestampBytes);
- nfcPutData(0xC8, key.getFingerprint());
- } else {
- throw new IOException("Key slot occupied; token must be reset to put new decryption key.");
- }
- } else if (key.canAuthenticate()) {
- if (shouldPutKey(key.getFingerprint(), 2)) {
- nfcPutKey(0xA4, key, passphrase);
- nfcPutData(0xD0, timestampBytes);
- nfcPutData(0xC9, key.getFingerprint());
- } else {
- throw new IOException("Key slot occupied; token must be reset to put new authentication key.");
- }
- } else {
- throw new IOException("Inappropriate key flags for Security Token key.");
- }
+ mSecurityTokenHelper.changeKey(key, passphrase);
// TODO: Is this really used anywhere?
mInputParcel.addCryptoData(subkeyBytes, tokenSerialNumber);
}
// change PINs afterwards
- nfcModifyPIN(0x81, newPin);
- nfcModifyPIN(0x83, newAdminPin);
+ mSecurityTokenHelper.modifyPin(0x81, newPin);
+ mSecurityTokenHelper.modifyPin(0x83, newAdminPin);
break;
}
- case NFC_RESET_CARD: {
- nfcResetCard();
+ case SECURITY_TOKEN_RESET_CARD: {
+ mSecurityTokenHelper.resetAndWipeToken();
break;
}
@@ -267,7 +276,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
- protected final void onNfcPostExecute() {
+ protected final void onSecurityTokenPostExecute() {
handleResult(mInputParcel);
// show finish
@@ -275,28 +284,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.DONE);
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- // check all 200ms if Security Token has been taken away
- while (true) {
- if (isNfcConnected()) {
- try {
- Thread.sleep(200);
- } catch (InterruptedException ignored) {
+ if (mSecurityTokenHelper.isPersistentConnectionAllowed()) {
+ // Just close
+ finish();
+ } else {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ // check all 200ms if Security Token has been taken away
+ while (true) {
+ if (isSecurityTokenConnected()) {
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException ignored) {
+ }
+ } else {
+ return null;
}
- } else {
- return null;
}
}
- }
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
- finish();
- }
- }.execute();
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ finish();
+ }
+ }.execute();
+ }
}
/**
@@ -311,7 +325,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
- protected void onNfcError(String error) {
+ protected void onSecurityTokenError(String error) {
pauseTagHandling();
vErrorText.setText(error + "\n\n" + getString(R.string.security_token_nfc_try_again_text));
@@ -321,31 +335,11 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
- public void onNfcPinError(String error) {
- onNfcError(error);
+ public void onSecurityTokenPinError(String error) {
+ onSecurityTokenError(error);
// clear (invalid) passphrase
PassphraseCacheService.clearCachedPassphrase(
this, mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
}
-
- private boolean shouldPutKey(byte[] fingerprint, int idx) throws IOException {
- byte[] tokenFingerprint = nfcGetMasterKeyFingerprint(idx);
-
- // Note: special case: This should not happen, but happens with
- // https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true
- if (tokenFingerprint == null) {
- return true;
- }
-
- // Slot is empty, or contains this key already. PUT KEY operation is safe
- if (Arrays.equals(tokenFingerprint, BLANK_FINGERPRINT) ||
- Arrays.equals(tokenFingerprint, fingerprint)) {
- return true;
- }
-
- // Slot already contains a different key; don't overwrite it.
- return false;
- }
-
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
index ea70cde..4fd327c 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java
@@ -19,8 +19,6 @@
package org.sufficientlysecure.keychain.ui;
-import java.util.List;
-
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
@@ -49,6 +47,7 @@ import android.view.ViewGroup;
import android.widget.LinearLayout;
import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.KeychainApplication;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.AppCompatPreferenceActivity;
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
@@ -59,6 +58,8 @@ import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
+import java.util.List;
+
public class SettingsActivity extends AppCompatPreferenceActivity {
public static final int REQUEST_CODE_KEYSERVER_PREF = 0x00007005;
@@ -405,7 +406,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
/**
- * This fragment shows the keyserver/contacts sync preferences
+ * This fragment shows the keyserver/wifi-only-sync/contacts sync preferences
*/
public static class SyncPrefsFragment extends PresetPreferenceFragment {
@@ -422,8 +423,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
super.onResume();
// this needs to be done in onResume since the user can change sync values from Android
// settings and we need to reflect that change when the user navigates back
- AccountManager manager = AccountManager.get(getActivity());
- final Account account = manager.getAccountsByType(Constants.ACCOUNT_TYPE)[0];
+ final Account account = KeychainApplication.createAccountIfNecessary(getActivity());
// for keyserver sync
initializeSyncCheckBox(
(SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER),
@@ -441,8 +441,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
private void initializeSyncCheckBox(final SwitchPreference syncCheckBox,
final Account account,
final String authority) {
- boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority)
- && checkContactsPermission(authority);
+ // account is null if it could not be created for some reason
+ boolean syncEnabled =
+ account != null
+ && ContentResolver.getSyncAutomatically(account, authority)
+ && checkContactsPermission(authority);
syncCheckBox.setChecked(syncEnabled);
setSummary(syncCheckBox, authority, syncEnabled);
@@ -464,6 +467,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
return false;
}
} else {
+ if (account == null) {
+ // if account could not be created for some reason,
+ // we can't have our sync
+ return false;
+ }
// disable syncs
ContentResolver.setSyncAutomatically(account, authority, false);
// immediately delete any linked contacts
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java
index 5a8ab36..488558a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java
@@ -40,6 +40,7 @@ import android.widget.TextView;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.dialog.AddEditKeyserverDialogFragment;
+import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperAdapter;
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperViewHolder;
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperDragCallback;
@@ -312,19 +313,19 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC
public void showAsSelectedKeyserver() {
isSelectedKeyserver = true;
selectedServerLabel.setVisibility(View.VISIBLE);
- outerLayout.setBackgroundColor(getResources().getColor(R.color.android_green_dark));
+ outerLayout.setBackgroundColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorPrimaryDark));
}
public void showAsUnselectedKeyserver() {
isSelectedKeyserver = false;
selectedServerLabel.setVisibility(View.GONE);
- outerLayout.setBackgroundColor(Color.WHITE);
+ outerLayout.setBackgroundColor(0);
}
@Override
public void onItemSelected() {
selectedServerLabel.setVisibility(View.GONE);
- itemView.setBackgroundColor(Color.LTGRAY);
+ itemView.setBackgroundColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorBrightToolbar));
}
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java
index f38e492..306b022 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java
@@ -31,10 +31,7 @@ import android.widget.Spinner;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.UploadResult;
-import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.KeychainContract;
-import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
-import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
@@ -53,7 +50,6 @@ public class UploadKeyActivity extends BaseActivity
// CryptoOperationHelper.Callback vars
private String mKeyserver;
- private long mMasterKeyId;
private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper;
@Override
@@ -63,6 +59,10 @@ public class UploadKeyActivity extends BaseActivity
mUploadButton = findViewById(R.id.upload_key_action_upload);
mKeyServerSpinner = (Spinner) findViewById(R.id.upload_key_keyserver);
+ MultiUserIdsFragment mMultiUserIdsFragment = (MultiUserIdsFragment)
+ getSupportFragmentManager().findFragmentById(R.id.multi_user_ids_fragment);
+ mMultiUserIdsFragment.setCheckboxVisibility(false);
+
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
.getKeyServers()
@@ -89,15 +89,6 @@ public class UploadKeyActivity extends BaseActivity
return;
}
- try {
- mMasterKeyId = new ProviderHelper(this).getCachedPublicKeyRing(
- KeyRings.buildUnifiedKeyRingUri(mDataUri)).getMasterKeyId();
- } catch (PgpKeyNotFoundException e) {
- Log.e(Constants.TAG, "Intent data pointed to bad key!");
- finish();
- return;
- }
-
}
@Override
@@ -136,7 +127,9 @@ public class UploadKeyActivity extends BaseActivity
@Override
public UploadKeyringParcel createOperationInput() {
- return new UploadKeyringParcel(mKeyserver, mMasterKeyId);
+ long[] masterKeyIds = getIntent().getLongArrayExtra(MultiUserIdsFragment.EXTRA_KEY_IDS);
+
+ return new UploadKeyringParcel(mKeyserver, masterKeyIds[0]);
}
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UsbEventReceiverActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UsbEventReceiverActivity.java
new file mode 100644
index 0000000..05b30b1
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UsbEventReceiverActivity.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+public class UsbEventReceiverActivity extends Activity {
+ public static final String ACTION_USB_PERMISSION =
+ "org.sufficientlysecure.keychain.ui.USB_PERMISSION";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ final UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
+
+ Intent intent = getIntent();
+ if (intent != null) {
+ if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
+ UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+
+ Log.d(Constants.TAG, "Requesting permission for " + usbDevice.getDeviceName());
+ usbManager.requestPermission(usbDevice,
+ PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0));
+ }
+ }
+
+ // Close the activity
+ finish();
+ }
+} \ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
index 03fc079..ca4a339 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java
@@ -32,6 +32,7 @@ import android.app.ActivityOptions;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
import android.net.Uri;
import android.nfc.NfcAdapter;
import android.os.AsyncTask;
@@ -80,11 +81,11 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
+import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
-import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType;
-import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
+import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
@@ -102,7 +103,7 @@ import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.Preferences;
-public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
+public class ViewKeyActivity extends BaseSecurityTokenActivity implements
LoaderManager.LoaderCallbacks<Cursor>,
CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> {
@@ -129,8 +130,8 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
private String mKeyserver;
private ArrayList<ParcelableKeyRing> mKeyList;
private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper;
- private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mEditOpHelper;
- private SaveKeyringParcel mSaveKeyringParcel;
+ private CryptoOperationHelper<ChangeUnlockParcel, EditKeyResult> mEditOpHelper;
+ private ChangeUnlockParcel mChangeUnlockParcel;
private TextView mStatusText;
private ImageView mStatusImage;
@@ -170,9 +171,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
private byte[] mFingerprint;
private String mFingerprintString;
- private byte[] mNfcFingerprints;
- private String mNfcUserId;
- private byte[] mNfcAid;
+ private byte[] mSecurityTokenFingerprints;
+ private String mSecurityTokenUserId;
+ private byte[] mSecurityTokenAid;
@SuppressLint("InflateParams")
@Override
@@ -428,13 +429,11 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
}
private void changePassword() {
- mSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint);
-
- CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> editKeyCallback
- = new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() {
+ CryptoOperationHelper.Callback<ChangeUnlockParcel, EditKeyResult> editKeyCallback
+ = new CryptoOperationHelper.Callback<ChangeUnlockParcel, EditKeyResult>() {
@Override
- public SaveKeyringParcel createOperationInput() {
- return mSaveKeyringParcel;
+ public ChangeUnlockParcel createOperationInput() {
+ return mChangeUnlockParcel;
}
@Override
@@ -468,9 +467,10 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
Bundle data = message.getData();
// use new passphrase!
- mSaveKeyringParcel.mNewUnlock = new SaveKeyringParcel.ChangeUnlockParcel(
- (Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE),
- null
+ mChangeUnlockParcel = new ChangeUnlockParcel(
+ mMasterKeyId,
+ mFingerprint,
+ (Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE)
);
mEditOpHelper.cryptoOperation();
@@ -646,17 +646,17 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
}
@Override
- protected void doNfcInBackground() throws IOException {
+ protected void doSecurityTokenInBackground() throws IOException {
- mNfcFingerprints = nfcGetFingerprints();
- mNfcUserId = nfcGetUserId();
- mNfcAid = nfcGetAid();
+ mSecurityTokenFingerprints = mSecurityTokenHelper.getFingerprints();
+ mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
+ mSecurityTokenAid = mSecurityTokenHelper.getAid();
}
@Override
- protected void onNfcPostExecute() {
+ protected void onSecurityTokenPostExecute() {
- long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints);
+ long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mSecurityTokenFingerprints);
try {
@@ -667,7 +667,7 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
// if the master key of that key matches this one, just show the token dialog
if (KeyFormattingUtils.convertFingerprintToHex(candidateFp).equals(mFingerprintString)) {
- showSecurityTokenFragment(mNfcFingerprints, mNfcUserId, mNfcAid);
+ showSecurityTokenFragment(mSecurityTokenFingerprints, mSecurityTokenUserId, mSecurityTokenAid);
return;
}
@@ -680,9 +680,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
Intent intent = new Intent(
ViewKeyActivity.this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
finish();
}
@@ -695,9 +695,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
public void onAction() {
Intent intent = new Intent(
ViewKeyActivity.this, CreateKeyActivity.class);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
finish();
}
@@ -924,6 +924,7 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
}
mPhoto.setImageBitmap(photo);
+ mPhoto.setColorFilter(getResources().getColor(R.color.toolbar_photo_tint), PorterDuff.Mode.SRC_ATOP);
mPhotoLayout.setVisibility(View.VISIBLE);
}
};
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java
index ce2f2de..02eae1b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java
@@ -238,7 +238,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
// let user choose application
Intent sendIntent = new Intent(Intent.ACTION_SEND);
- sendIntent.setType("text/plain");
+ sendIntent.setType(Constants.MIME_TYPE_KEYS);
// NOTE: Don't use Intent.EXTRA_TEXT to send the key
// better send it via a Uri!
@@ -455,8 +455,19 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
}
private void uploadToKeyserver() {
+ long keyId;
+ try {
+ keyId = new ProviderHelper(getActivity())
+ .getCachedPublicKeyRing(mDataUri)
+ .extractOrGetMasterKeyId();
+ } catch (PgpKeyNotFoundException e) {
+ Log.e(Constants.TAG, "key not found!", e);
+ Notify.create(getActivity(), "key not found", Style.ERROR).show();
+ return;
+ }
Intent uploadIntent = new Intent(getActivity(), UploadKeyActivity.class);
uploadIntent.setData(mDataUri);
+ uploadIntent.putExtra(MultiUserIdsFragment.EXTRA_KEY_IDS, new long[]{keyId});
startActivityForResult(uploadIntent, 0);
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java
index fc6db1b..93b38af 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvSubkeysFragment.java
@@ -39,6 +39,7 @@ import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.ViewAnimator;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
@@ -346,50 +347,45 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements
}
break;
}
- case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: {
- // TODO: enable later when Admin PIN handling is resolved
- Notify.create(getActivity(),
- "This feature will be available in an upcoming OpenKeychain version.",
- Notify.Style.WARN).show();
- break;
+ case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN: {
+ SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
+ if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
+ secretKeyType == SecretKeyType.GNU_DUMMY) {
+ Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_stripped, Notify.Style.ERROR)
+ .show();
+ break;
+ }
+
+ int algorithm = mSubkeysAdapter.getAlgorithm(position);
+ if (algorithm != PublicKeyAlgorithmTags.RSA_GENERAL
+ && algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT
+ && algorithm != PublicKeyAlgorithmTags.RSA_SIGN) {
+ Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_algo, Notify.Style.ERROR)
+ .show();
+ break;
+ }
+
+ if (mSubkeysAdapter.getKeySize(position) != 2048) {
+ Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_size, Notify.Style.ERROR)
+ .show();
+ break;
+ }
-// Activity activity = EditKeyFragment.this.getActivity();
-// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
-// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
-// secretKeyType == SecretKeyType.GNU_DUMMY) {
-// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR)
-// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
-// break;
-// }
-// int algorithm = mSubkeysAdapter.getAlgorithm(position);
-// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN
-// if (algorithm != 1 && algorithm != 2 && algorithm != 3) {
-// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR)
-// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
-// break;
-// }
-// if (mSubkeysAdapter.getKeySize(position) != 2048) {
-// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR)
-// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
-// break;
-// }
-//
-//
-// SubkeyChange change;
-// change = mSaveKeyringParcel.getSubkeyChange(keyId);
-// if (change == null) {
-// mSaveKeyringParcel.mChangeSubKeys.add(
-// new SubkeyChange(keyId, false, true)
-// );
-// break;
-// }
-// // toggle
-// change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
-// if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
-// // User had chosen to strip key, but now wants to divert it.
-// change.mDummyStrip = false;
-// }
-// break;
+ SubkeyChange change;
+ change = mEditModeSaveKeyringParcel.getSubkeyChange(keyId);
+ if (change == null) {
+ mEditModeSaveKeyringParcel.mChangeSubKeys.add(
+ new SubkeyChange(keyId, false, true)
+ );
+ break;
+ }
+ // toggle
+ change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
+ if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
+ // User had chosen to strip key, but now wants to divert it.
+ change.mDummyStrip = false;
+ }
+ break;
}
}
getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad();
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
index 0201318..df24e98 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java
@@ -141,6 +141,7 @@ public class ImportKeysListLoader
OperationResult.OperationLog log = new OperationResult.OperationLog();
log.add(OperationResult.LogType.MSG_GET_NO_VALID_KEYS, 0);
GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_ERROR_NO_VALID_KEYS, log);
+ mData.clear();
mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult);
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java
index fb72a26..78aaeca 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java
@@ -66,6 +66,7 @@ public class KeyAdapter extends CursorAdapter {
KeyRings.HAS_DUPLICATE_USER_ID,
KeyRings.FINGERPRINT,
KeyRings.CREATION,
+ KeyRings.HAS_ENCRYPT
};
public static final int INDEX_MASTER_KEY_ID = 1;
@@ -77,6 +78,7 @@ public class KeyAdapter extends CursorAdapter {
public static final int INDEX_HAS_DUPLICATE_USER_ID = 7;
public static final int INDEX_FINGERPRINT = 8;
public static final int INDEX_CREATION = 9;
+ public static final int INDEX_HAS_ENCRYPT = 10;
public KeyAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
@@ -289,6 +291,7 @@ public class KeyAdapter extends CursorAdapter {
public final KeyRing.UserId mUserId;
public final long mKeyId;
public final boolean mHasDuplicate;
+ public final boolean mHasEncrypt;
public final Date mCreation;
public final String mFingerprint;
public final boolean mIsSecret, mIsRevoked, mIsExpired, mIsVerified;
@@ -299,6 +302,7 @@ public class KeyAdapter extends CursorAdapter {
mUserIdFull = userId;
mKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
mHasDuplicate = cursor.getLong(INDEX_HAS_DUPLICATE_USER_ID) > 0;
+ mHasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0;
mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000);
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
cursor.getBlob(INDEX_FINGERPRINT));
@@ -315,6 +319,7 @@ public class KeyAdapter extends CursorAdapter {
mUserIdFull = userId;
mKeyId = ring.getMasterKeyId();
mHasDuplicate = false;
+ mHasEncrypt = key.getKeyRing().getEncryptIds().size() > 0;
mCreation = key.getCreationTime();
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
ring.getFingerprint());
@@ -333,14 +338,6 @@ public class KeyAdapter extends CursorAdapter {
return mUserId.email;
}
}
-
- // TODO: workaround for bug in TokenAutoComplete,
- // see https://github.com/open-keychain/open-keychain/issues/1636
- @Override
- public String toString() {
- return " ";
- }
-
}
public static String[] getProjectionWith(String[] projection) {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java
index b91abf0..d247fad 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java
@@ -39,6 +39,7 @@ import java.util.ArrayList;
public class MultiUserIdsAdapter extends CursorAdapter {
private LayoutInflater mInflater;
private final ArrayList<Boolean> mCheckStates;
+ private boolean checkboxVisibility = true;
public MultiUserIdsAdapter(Context context, Cursor c, int flags, ArrayList<Boolean> preselectStates) {
super(context, c, flags);
@@ -46,6 +47,11 @@ public class MultiUserIdsAdapter extends CursorAdapter {
mCheckStates = preselectStates == null ? new ArrayList<Boolean>() : preselectStates;
}
+ public MultiUserIdsAdapter(Context context, Cursor c, int flags, ArrayList<Boolean> preselectStates, boolean checkboxVisibility) {
+ this(context,c,flags,preselectStates);
+ this.checkboxVisibility = checkboxVisibility;
+ }
+
@Override
public Cursor swapCursor(Cursor newCursor) {
if (newCursor != null) {
@@ -138,6 +144,7 @@ public class MultiUserIdsAdapter extends CursorAdapter {
}
});
vCheckBox.setClickable(false);
+ vCheckBox.setVisibility(checkboxVisibility?View.VISIBLE:View.GONE);
View vUidBody = view.findViewById(R.id.user_id_body);
vUidBody.setClickable(true);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAddedAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAddedAdapter.java
index 8b2481c..717299b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAddedAdapter.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAddedAdapter.java
@@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.ui.adapter;
import android.app.Activity;
import android.content.Context;
import android.graphics.Typeface;
+import android.support.v4.app.FragmentActivity;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
@@ -32,6 +33,7 @@ import android.widget.TextView;
import org.bouncycastle.bcpg.sig.KeyFlags;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.ui.dialog.AddSubkeyDialogFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import java.util.Calendar;
@@ -65,10 +67,11 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
public SaveKeyringParcel.SubkeyAdd mModel;
}
+
public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) {
// Not recycled, inflate a new view
- convertView = mInflater.inflate(R.layout.view_key_adv_subkey_item, null);
+ convertView = mInflater.inflate(R.layout.view_key_adv_subkey_item, parent, false);
final ViewHolder holder = new ViewHolder();
holder.vKeyId = (TextView) convertView.findViewById(R.id.subkey_item_key_id);
holder.vKeyDetails = (TextView) convertView.findViewById(R.id.subkey_item_details);
@@ -88,16 +91,8 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
vStatus.setVisibility(View.GONE);
convertView.setTag(holder);
-
- holder.vDelete.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- // remove reference model item from adapter (data and notify about change)
- SubkeysAddedAdapter.this.remove(holder.mModel);
- }
- });
-
}
+
final ViewHolder holder = (ViewHolder) convertView.getTag();
// save reference to model item
@@ -113,8 +108,41 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
boolean isMasterKey = mNewKeyring && position == 0;
if (isMasterKey) {
holder.vKeyId.setTypeface(null, Typeface.BOLD);
+ holder.vDelete.setImageResource(R.drawable.ic_change_grey_24dp);
+ holder.vDelete.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // swapping out the old master key with newly set master key
+ AddSubkeyDialogFragment addSubkeyDialogFragment =
+ AddSubkeyDialogFragment.newInstance(true);
+ addSubkeyDialogFragment
+ .setOnAlgorithmSelectedListener(
+ new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() {
+ @Override
+ public void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey) {
+ // calculate manually as the provided position variable
+ // is not always accurate
+ int pos = SubkeysAddedAdapter.this.getPosition(holder.mModel);
+ SubkeysAddedAdapter.this.remove(holder.mModel);
+ SubkeysAddedAdapter.this.insert(newSubkey, pos);
+ }
+ }
+ );
+ addSubkeyDialogFragment.show(
+ ((FragmentActivity)mActivity).getSupportFragmentManager()
+ , "addSubkeyDialog");
+ }
+ });
} else {
holder.vKeyId.setTypeface(null, Typeface.NORMAL);
+ holder.vDelete.setImageResource(R.drawable.ic_close_grey_24dp);
+ holder.vDelete.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // remove reference model item from adapter (data and notify about change)
+ SubkeysAddedAdapter.this.remove(holder.mModel);
+ }
+ });
}
holder.vKeyId.setText(R.string.edit_key_new_subkey);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java
index 107c63e..063181d 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java
@@ -26,6 +26,7 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Gravity;
import android.view.LayoutInflater;
+import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -45,8 +46,8 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
- initTheme();
super.onCreate(savedInstanceState);
+ initTheme();
initLayout();
initToolbar();
}
@@ -65,6 +66,16 @@ public abstract class BaseActivity extends AppCompatActivity {
}
}
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home :
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
public static void onResumeChecks(Context context) {
KeyserverSyncAdapterService.cancelUpdates(context);
// in case user has disabled sync from Android account settings
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java
new file mode 100644
index 0000000..6806135
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenActivity.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ * Copyright (C) 2013-2014 Signe Rüsch
+ * Copyright (C) 2013-2014 Philipp Jakubeit
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.base;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.TagLostException;
+import android.os.AsyncTask;
+import android.os.Bundle;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
+import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
+import org.sufficientlysecure.keychain.securitytoken.CardException;
+import org.sufficientlysecure.keychain.securitytoken.NfcTransport;
+import org.sufficientlysecure.keychain.securitytoken.SecurityTokenHelper;
+import org.sufficientlysecure.keychain.securitytoken.Transport;
+import org.sufficientlysecure.keychain.util.UsbConnectionDispatcher;
+import org.sufficientlysecure.keychain.securitytoken.UsbTransport;
+import org.sufficientlysecure.keychain.ui.CreateKeyActivity;
+import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
+import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
+import org.sufficientlysecure.keychain.ui.dialog.FidesmoInstallDialog;
+import org.sufficientlysecure.keychain.ui.dialog.FidesmoPgpInstallDialog;
+import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
+import org.sufficientlysecure.keychain.ui.util.Notify;
+import org.sufficientlysecure.keychain.ui.util.Notify.Style;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
+import java.io.IOException;
+
+import nordpol.android.OnDiscoveredTagListener;
+import nordpol.android.TagDispatcher;
+
+public abstract class BaseSecurityTokenActivity extends BaseActivity
+ implements OnDiscoveredTagListener, UsbConnectionDispatcher.OnDiscoveredUsbDeviceListener {
+ public static final int REQUEST_CODE_PIN = 1;
+
+ public static final String EXTRA_TAG_HANDLING_ENABLED = "tag_handling_enabled";
+
+ private static final String FIDESMO_APP_PACKAGE = "com.fidesmo.sec.android";
+
+ protected SecurityTokenHelper mSecurityTokenHelper = SecurityTokenHelper.getInstance();
+ protected TagDispatcher mNfcTagDispatcher;
+ protected UsbConnectionDispatcher mUsbDispatcher;
+ private boolean mTagHandlingEnabled;
+
+ private byte[] mSecurityTokenFingerprints;
+ private String mSecurityTokenUserId;
+ private byte[] mSecurityTokenAid;
+
+ /**
+ * Override to change UI before SecurityToken handling (UI thread)
+ */
+ protected void onSecurityTokenPreExecute() {
+ }
+
+ /**
+ * Override to implement SecurityToken operations (background thread)
+ */
+ protected void doSecurityTokenInBackground() throws IOException {
+ mSecurityTokenFingerprints = mSecurityTokenHelper.getFingerprints();
+ mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
+ mSecurityTokenAid = mSecurityTokenHelper.getAid();
+ }
+
+ /**
+ * Override to handle result of SecurityToken operations (UI thread)
+ */
+ protected void onSecurityTokenPostExecute() {
+
+ final long subKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mSecurityTokenFingerprints);
+
+ try {
+ CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(
+ KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId));
+ long masterKeyId = ring.getMasterKeyId();
+
+ Intent intent = new Intent(this, ViewKeyActivity.class);
+ intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
+ intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
+ startActivity(intent);
+ } catch (PgpKeyNotFoundException e) {
+ Intent intent = new Intent(this, CreateKeyActivity.class);
+ intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
+ intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
+ intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_FINGERPRINTS, mSecurityTokenFingerprints);
+ startActivity(intent);
+ }
+ }
+
+ /**
+ * Override to use something different than Notify (UI thread)
+ */
+ protected void onSecurityTokenError(String error) {
+ Notify.create(this, error, Style.WARN).show();
+ }
+
+ /**
+ * Override to do something when PIN is wrong, e.g., clear passphrases (UI thread)
+ */
+ protected void onSecurityTokenPinError(String error) {
+ onSecurityTokenError(error);
+ }
+
+ public void tagDiscovered(final Tag tag) {
+ // Actual NFC operations are executed in doInBackground to not block the UI thread
+ if (!mTagHandlingEnabled)
+ return;
+
+ securityTokenDiscovered(new NfcTransport(tag));
+ }
+
+ public void usbDeviceDiscovered(final UsbDevice usbDevice) {
+ // Actual USB operations are executed in doInBackground to not block the UI thread
+ if (!mTagHandlingEnabled)
+ return;
+
+ UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
+ securityTokenDiscovered(new UsbTransport(usbDevice, usbManager));
+ }
+
+ public void securityTokenDiscovered(final Transport transport) {
+ // Actual Security Token operations are executed in doInBackground to not block the UI thread
+ if (!mTagHandlingEnabled)
+ return;
+ new AsyncTask<Void, Void, IOException>() {
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ onSecurityTokenPreExecute();
+ }
+
+ @Override
+ protected IOException doInBackground(Void... params) {
+ try {
+ handleSecurityToken(transport);
+ } catch (IOException e) {
+ return e;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(IOException exception) {
+ super.onPostExecute(exception);
+
+ if (exception != null) {
+ handleSecurityTokenError(exception);
+ return;
+ }
+
+ onSecurityTokenPostExecute();
+ }
+ }.execute();
+ }
+
+ protected void pauseTagHandling() {
+ mTagHandlingEnabled = false;
+ }
+
+ protected void resumeTagHandling() {
+ mTagHandlingEnabled = true;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mNfcTagDispatcher = TagDispatcher.get(this, this, false, false, true, false);
+ mUsbDispatcher = new UsbConnectionDispatcher(this, this);
+
+ // Check whether we're recreating a previously destroyed instance
+ if (savedInstanceState != null) {
+ // Restore value of members from saved state
+ mTagHandlingEnabled = savedInstanceState.getBoolean(EXTRA_TAG_HANDLING_ENABLED);
+ } else {
+ mTagHandlingEnabled = true;
+ }
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
+ throw new AssertionError("should not happen: NfcOperationActivity.onCreate is called instead of onNewIntent!");
+ }
+
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putBoolean(EXTRA_TAG_HANDLING_ENABLED, mTagHandlingEnabled);
+ }
+
+ /**
+ * This activity is started as a singleTop activity.
+ * All new NFC Intents which are delivered to this activity are handled here
+ */
+ @Override
+ public void onNewIntent(final Intent intent) {
+ mNfcTagDispatcher.interceptIntent(intent);
+ }
+
+ private void handleSecurityTokenError(IOException e) {
+
+ if (e instanceof TagLostException) {
+ onSecurityTokenError(getString(R.string.security_token_error_tag_lost));
+ return;
+ }
+
+ if (e instanceof IsoDepNotSupportedException) {
+ onSecurityTokenError(getString(R.string.security_token_error_iso_dep_not_supported));
+ return;
+ }
+
+ short status;
+ if (e instanceof CardException) {
+ status = ((CardException) e).getResponseCode();
+ } else {
+ status = -1;
+ }
+
+ // Wrong PIN, a status of 63CX indicates X attempts remaining.
+ // NOTE: Used in ykneo-openpgp version < 1.0.10, changed to 0x6982 in 1.0.11
+ // https://github.com/Yubico/ykneo-openpgp/commit/90c2b91e86fb0e43ee234dd258834e75e3416410
+ if ((status & (short) 0xFFF0) == 0x63C0) {
+ int tries = status & 0x000F;
+ // hook to do something different when PIN is wrong
+ onSecurityTokenPinError(getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries));
+ return;
+ }
+
+ // Otherwise, all status codes are fixed values.
+ switch (status) {
+
+ // These error conditions are likely to be experienced by an end user.
+
+ /* OpenPGP Card Spec: Security status not satisfied, PW wrong,
+ PW not checked (command not allowed), Secure messaging incorrect (checksum and/or cryptogram) */
+ // NOTE: Used in ykneo-openpgp >= 1.0.11 for wrong PIN
+ case 0x6982: {
+ // hook to do something different when PIN is wrong
+ onSecurityTokenPinError(getString(R.string.security_token_error_security_not_satisfied));
+ break;
+ }
+ /* OpenPGP Card Spec: Selected file in termination state */
+ case 0x6285: {
+ onSecurityTokenError(getString(R.string.security_token_error_terminated));
+ break;
+ }
+ /* OpenPGP Card Spec: Wrong length (Lc and/or Le) */
+ // NOTE: Used in ykneo-openpgp < 1.0.10 for too short PIN, changed in 1.0.11 to 0x6A80 for too short PIN
+ // https://github.com/Yubico/ykneo-openpgp/commit/b49ce8241917e7c087a4dab7b2c755420ff4500f
+ case 0x6700: {
+ // hook to do something different when PIN is wrong
+ onSecurityTokenPinError(getString(R.string.security_token_error_wrong_length));
+ break;
+ }
+ /* OpenPGP Card Spec: Incorrect parameters in the data field */
+ // NOTE: Used in ykneo-openpgp >= 1.0.11 for too short PIN
+ case 0x6A80: {
+ // hook to do something different when PIN is wrong
+ onSecurityTokenPinError(getString(R.string.security_token_error_bad_data));
+ break;
+ }
+ /* OpenPGP Card Spec: Authentication method blocked, PW blocked (error counter zero) */
+ case 0x6983: {
+ onSecurityTokenError(getString(R.string.security_token_error_authentication_blocked));
+ break;
+ }
+ /* OpenPGP Card Spec: Condition of use not satisfied */
+ case 0x6985: {
+ onSecurityTokenError(getString(R.string.security_token_error_conditions_not_satisfied));
+ break;
+ }
+ /* OpenPGP Card Spec: SM data objects incorrect (e.g. wrong TLV-structure in command data) */
+ // NOTE: 6A88 is "Not Found" in the spec, but ykneo-openpgp also returns 6A83 for this in some cases.
+ case 0x6A88:
+ case 0x6A83: {
+ onSecurityTokenError(getString(R.string.security_token_error_data_not_found));
+ break;
+ }
+ // 6F00 is a JavaCard proprietary status code, SW_UNKNOWN, and usually represents an
+ // unhandled exception on the security token.
+ case 0x6F00: {
+ onSecurityTokenError(getString(R.string.security_token_error_unknown));
+ break;
+ }
+ // 6A82 app not installed on security token!
+ case 0x6A82: {
+ if (mSecurityTokenHelper.isFidesmoToken()) {
+ // Check if the Fidesmo app is installed
+ if (isAndroidAppInstalled(FIDESMO_APP_PACKAGE)) {
+ promptFidesmoPgpInstall();
+ } else {
+ promptFidesmoAppInstall();
+ }
+ } else { // Other (possibly) compatible hardware
+ onSecurityTokenError(getString(R.string.security_token_error_pgp_app_not_installed));
+ }
+ break;
+ }
+
+ // These errors should not occur in everyday use; if they are returned, it means we
+ // made a mistake sending data to the token, or the token is misbehaving.
+
+ /* OpenPGP Card Spec: Last command of the chain expected */
+ case 0x6883: {
+ onSecurityTokenError(getString(R.string.security_token_error_chaining_error));
+ break;
+ }
+ /* OpenPGP Card Spec: Wrong parameters P1-P2 */
+ case 0x6B00: {
+ onSecurityTokenError(getString(R.string.security_token_error_header, "P1/P2"));
+ break;
+ }
+ /* OpenPGP Card Spec: Instruction (INS) not supported */
+ case 0x6D00: {
+ onSecurityTokenError(getString(R.string.security_token_error_header, "INS"));
+ break;
+ }
+ /* OpenPGP Card Spec: Class (CLA) not supported */
+ case 0x6E00: {
+ onSecurityTokenError(getString(R.string.security_token_error_header, "CLA"));
+ break;
+ }
+ default: {
+ onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
+ break;
+ }
+ }
+
+ }
+
+ /**
+ * Called when the system is about to start resuming a previous activity,
+ * disables NFC Foreground Dispatch
+ */
+ public void onPause() {
+ super.onPause();
+ Log.d(Constants.TAG, "BaseNfcActivity.onPause");
+
+ mNfcTagDispatcher.disableExclusiveNfc();
+ }
+
+ /**
+ * Called when the activity will start interacting with the user,
+ * enables NFC Foreground Dispatch
+ */
+ public void onResume() {
+ super.onResume();
+ Log.d(Constants.TAG, "BaseNfcActivity.onResume");
+ mNfcTagDispatcher.enableExclusiveNfc();
+ }
+
+ protected void obtainSecurityTokenPin(RequiredInputParcel requiredInput) {
+
+ try {
+ Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
+ requiredInput.getMasterKeyId(), requiredInput.getSubKeyId());
+ if (passphrase != null) {
+ mSecurityTokenHelper.setPin(passphrase);
+ return;
+ }
+
+ Intent intent = new Intent(this, PassphraseDialogActivity.class);
+ intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT,
+ RequiredInputParcel.createRequiredPassphrase(requiredInput));
+ startActivityForResult(intent, REQUEST_CODE_PIN);
+ } catch (PassphraseCacheService.KeyNotFoundException e) {
+ throw new AssertionError(
+ "tried to find passphrase for non-existing key. this is a programming error!");
+ }
+
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CODE_PIN: {
+ if (resultCode != Activity.RESULT_OK) {
+ setResult(resultCode);
+ finish();
+ return;
+ }
+ CryptoInputParcel input = data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT);
+ mSecurityTokenHelper.setPin(input.getPassphrase());
+ break;
+ }
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ protected void handleSecurityToken(Transport transport) throws IOException {
+ // Don't reconnect if device was already connected
+ if (!(mSecurityTokenHelper.isPersistentConnectionAllowed()
+ && mSecurityTokenHelper.isConnected()
+ && mSecurityTokenHelper.getTransport().equals(transport))) {
+ mSecurityTokenHelper.setTransport(transport);
+ mSecurityTokenHelper.connectToDevice();
+ }
+ doSecurityTokenInBackground();
+ }
+
+ public boolean isSecurityTokenConnected() {
+ return mSecurityTokenHelper.isConnected();
+ }
+
+ public static class IsoDepNotSupportedException extends IOException {
+
+ public IsoDepNotSupportedException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ }
+
+ /**
+ * Ask user if she wants to install PGP onto her Fidesmo token
+ */
+ private void promptFidesmoPgpInstall() {
+ FidesmoPgpInstallDialog fidesmoPgpInstallDialog = new FidesmoPgpInstallDialog();
+ fidesmoPgpInstallDialog.show(getSupportFragmentManager(), "fidesmoPgpInstallDialog");
+ }
+
+ /**
+ * Show a Dialog to the user informing that Fidesmo App must be installed and with option
+ * to launch the Google Play store.
+ */
+ private void promptFidesmoAppInstall() {
+ FidesmoInstallDialog fidesmoInstallDialog = new FidesmoInstallDialog();
+ fidesmoInstallDialog.show(getSupportFragmentManager(), "fidesmoInstallDialog");
+ }
+
+ /**
+ * Use the package manager to detect if an application is installed on the phone
+ *
+ * @param uri an URI identifying the application's package
+ * @return 'true' if the app is installed
+ */
+ private boolean isAndroidAppInstalled(String uri) {
+ PackageManager mPackageManager = getPackageManager();
+ boolean mAppInstalled;
+ try {
+ mPackageManager.getPackageInfo(uri, PackageManager.GET_ACTIVITIES);
+ mAppInstalled = true;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(Constants.TAG, "App not installed on Android device");
+ mAppInstalled = false;
+ }
+ return mAppInstalled;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mUsbDispatcher.onStop();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mUsbDispatcher.onStart();
+ }
+
+ public SecurityTokenHelper getSecurityTokenHelper() {
+ return mSecurityTokenHelper;
+ }
+
+ /**
+ * Run Security Token routines if last used token is connected and supports
+ * persistent connections
+ */
+ public void checkDeviceConnection() {
+ mUsbDispatcher.rescanDevices();
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java
deleted file mode 100644
index ae1944c..0000000
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseSecurityTokenNfcActivity.java
+++ /dev/null
@@ -1,1057 +0,0 @@
-/*
- * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
- * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
- * Copyright (C) 2013-2014 Signe Rüsch
- * Copyright (C) 2013-2014 Philipp Jakubeit
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package org.sufficientlysecure.keychain.ui.base;
-
-import java.io.IOException;
-import java.math.BigInteger;
-import java.nio.ByteBuffer;
-import java.security.interfaces.RSAPrivateCrtKey;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.nfc.NfcAdapter;
-import android.nfc.Tag;
-import android.nfc.TagLostException;
-import android.nfc.tech.IsoDep;
-import android.os.AsyncTask;
-import android.os.Bundle;
-
-import nordpol.Apdu;
-import nordpol.android.TagDispatcher;
-import nordpol.android.AndroidCard;
-import nordpol.android.OnDiscoveredTagListener;
-import nordpol.IsoCard;
-
-import org.bouncycastle.bcpg.HashAlgorithmTags;
-import org.bouncycastle.util.Arrays;
-import org.bouncycastle.util.encoders.Hex;
-import org.sufficientlysecure.keychain.Constants;
-import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
-import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
-import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
-import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
-import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
-import org.sufficientlysecure.keychain.provider.ProviderHelper;
-import org.sufficientlysecure.keychain.service.PassphraseCacheService;
-import org.sufficientlysecure.keychain.service.PassphraseCacheService.KeyNotFoundException;
-import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
-import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
-import org.sufficientlysecure.keychain.ui.CreateKeyActivity;
-import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
-import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
-import org.sufficientlysecure.keychain.ui.dialog.FidesmoInstallDialog;
-import org.sufficientlysecure.keychain.ui.dialog.FidesmoPgpInstallDialog;
-import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
-import org.sufficientlysecure.keychain.ui.util.Notify;
-import org.sufficientlysecure.keychain.ui.util.Notify.Style;
-import org.sufficientlysecure.keychain.util.Iso7816TLV;
-import org.sufficientlysecure.keychain.util.Log;
-import org.sufficientlysecure.keychain.util.Passphrase;
-
-public abstract class BaseSecurityTokenNfcActivity extends BaseActivity implements OnDiscoveredTagListener {
- public static final int REQUEST_CODE_PIN = 1;
-
- public static final String EXTRA_TAG_HANDLING_ENABLED = "tag_handling_enabled";
-
- // Fidesmo constants
- private static final String FIDESMO_APPS_AID_PREFIX = "A000000617";
- private static final String FIDESMO_APP_PACKAGE = "com.fidesmo.sec.android";
-
- protected Passphrase mPin;
- protected Passphrase mAdminPin;
- protected boolean mPw1ValidForMultipleSignatures;
- protected boolean mPw1ValidatedForSignature;
- protected boolean mPw1ValidatedForDecrypt; // Mode 82 does other things; consider renaming?
- protected boolean mPw3Validated;
- protected TagDispatcher mTagDispatcher;
- private IsoCard mIsoCard;
- private boolean mTagHandlingEnabled;
-
- private static final int TIMEOUT = 100000;
-
- private byte[] mNfcFingerprints;
- private String mNfcUserId;
- private byte[] mNfcAid;
-
- /**
- * Override to change UI before NFC handling (UI thread)
- */
- protected void onNfcPreExecute() {
- }
-
- /**
- * Override to implement NFC operations (background thread)
- */
- protected void doNfcInBackground() throws IOException {
- mNfcFingerprints = nfcGetFingerprints();
- mNfcUserId = nfcGetUserId();
- mNfcAid = nfcGetAid();
- }
-
- /**
- * Override to handle result of NFC operations (UI thread)
- */
- protected void onNfcPostExecute() {
-
- final long subKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints);
-
- try {
- CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(
- KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId));
- long masterKeyId = ring.getMasterKeyId();
-
- Intent intent = new Intent(this, ViewKeyActivity.class);
- intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
- intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
- startActivity(intent);
- } catch (PgpKeyNotFoundException e) {
- Intent intent = new Intent(this, CreateKeyActivity.class);
- intent.putExtra(CreateKeyActivity.EXTRA_NFC_AID, mNfcAid);
- intent.putExtra(CreateKeyActivity.EXTRA_NFC_USER_ID, mNfcUserId);
- intent.putExtra(CreateKeyActivity.EXTRA_NFC_FINGERPRINTS, mNfcFingerprints);
- startActivity(intent);
- }
- }
-
- /**
- * Override to use something different than Notify (UI thread)
- */
- protected void onNfcError(String error) {
- Notify.create(this, error, Style.WARN).show();
- }
-
- /**
- * Override to do something when PIN is wrong, e.g., clear passphrases (UI thread)
- */
- protected void onNfcPinError(String error) {
- onNfcError(error);
- }
-
- public void tagDiscovered(final Tag tag) {
- // Actual NFC operations are executed in doInBackground to not block the UI thread
- if(!mTagHandlingEnabled)
- return;
- new AsyncTask<Void, Void, IOException>() {
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- onNfcPreExecute();
- }
-
- @Override
- protected IOException doInBackground(Void... params) {
- try {
- handleTagDiscovered(tag);
- } catch (IOException e) {
- return e;
- }
-
- return null;
- }
-
- @Override
- protected void onPostExecute(IOException exception) {
- super.onPostExecute(exception);
-
- if (exception != null) {
- handleNfcError(exception);
- return;
- }
-
- onNfcPostExecute();
- }
- }.execute();
- }
-
- protected void pauseTagHandling() {
- mTagHandlingEnabled = false;
- }
-
- protected void resumeTagHandling() {
- mTagHandlingEnabled = true;
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mTagDispatcher = TagDispatcher.get(this, this, false, false);
-
- // Check whether we're recreating a previously destroyed instance
- if (savedInstanceState != null) {
- // Restore value of members from saved state
- mTagHandlingEnabled = savedInstanceState.getBoolean(EXTRA_TAG_HANDLING_ENABLED);
- } else {
- mTagHandlingEnabled = true;
- }
-
- Intent intent = getIntent();
- String action = intent.getAction();
- if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
- throw new AssertionError("should not happen: NfcOperationActivity.onCreate is called instead of onNewIntent!");
- }
-
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putBoolean(EXTRA_TAG_HANDLING_ENABLED, mTagHandlingEnabled);
- }
-
- /**
- * This activity is started as a singleTop activity.
- * All new NFC Intents which are delivered to this activity are handled here
- */
- @Override
- public void onNewIntent(final Intent intent) {
- mTagDispatcher.interceptIntent(intent);
- }
-
- private void handleNfcError(IOException e) {
-
- if (e instanceof TagLostException) {
- onNfcError(getString(R.string.security_token_error_tag_lost));
- return;
- }
-
- if (e instanceof IsoDepNotSupportedException) {
- onNfcError(getString(R.string.security_token_error_iso_dep_not_supported));
- return;
- }
-
- short status;
- if (e instanceof CardException) {
- status = ((CardException) e).getResponseCode();
- } else {
- status = -1;
- }
-
- // Wrong PIN, a status of 63CX indicates X attempts remaining.
- if ((status & (short) 0xFFF0) == 0x63C0) {
- int tries = status & 0x000F;
- // hook to do something different when PIN is wrong
- onNfcPinError(getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries));
- return;
- }
-
- // Otherwise, all status codes are fixed values.
- switch (status) {
- // These errors should not occur in everyday use; if they are returned, it means we
- // made a mistake sending data to the token, or the token is misbehaving.
- case 0x6A80: {
- onNfcError(getString(R.string.security_token_error_bad_data));
- break;
- }
- case 0x6883: {
- onNfcError(getString(R.string.security_token_error_chaining_error));
- break;
- }
- case 0x6B00: {
- onNfcError(getString(R.string.security_token_error_header, "P1/P2"));
- break;
- }
- case 0x6D00: {
- onNfcError(getString(R.string.security_token_error_header, "INS"));
- break;
- }
- case 0x6E00: {
- onNfcError(getString(R.string.security_token_error_header, "CLA"));
- break;
- }
- // These error conditions are more likely to be experienced by an end user.
- case 0x6285: {
- onNfcError(getString(R.string.security_token_error_terminated));
- break;
- }
- case 0x6700: {
- onNfcPinError(getString(R.string.security_token_error_wrong_length));
- break;
- }
- case 0x6982: {
- onNfcError(getString(R.string.security_token_error_security_not_satisfied));
- break;
- }
- case 0x6983: {
- onNfcError(getString(R.string.security_token_error_authentication_blocked));
- break;
- }
- case 0x6985: {
- onNfcError(getString(R.string.security_token_error_conditions_not_satisfied));
- break;
- }
- // 6A88 is "Not Found" in the spec, but Yubikey also returns 6A83 for this in some cases.
- case 0x6A88:
- case 0x6A83: {
- onNfcError(getString(R.string.security_token_error_data_not_found));
- break;
- }
- // 6F00 is a JavaCard proprietary status code, SW_UNKNOWN, and usually represents an
- // unhandled exception on the security token.
- case 0x6F00: {
- onNfcError(getString(R.string.security_token_error_unknown));
- break;
- }
- // 6A82 app not installed on security token!
- case 0x6A82: {
- if (isFidesmoDevice()) {
- // Check if the Fidesmo app is installed
- if (isAndroidAppInstalled(FIDESMO_APP_PACKAGE)) {
- promptFidesmoPgpInstall();
- } else {
- promptFidesmoAppInstall();
- }
- } else { // Other (possibly) compatible hardware
- onNfcError(getString(R.string.security_token_error_pgp_app_not_installed));
- }
- break;
- }
- default: {
- onNfcError(getString(R.string.security_token_error, e.getMessage()));
- break;
- }
- }
-
- }
-
- /**
- * Called when the system is about to start resuming a previous activity,
- * disables NFC Foreground Dispatch
- */
- public void onPause() {
- super.onPause();
- Log.d(Constants.TAG, "BaseNfcActivity.onPause");
-
- mTagDispatcher.disableExclusiveNfc();
- }
-
- /**
- * Called when the activity will start interacting with the user,
- * enables NFC Foreground Dispatch
- */
- public void onResume() {
- super.onResume();
- Log.d(Constants.TAG, "BaseNfcActivity.onResume");
- mTagDispatcher.enableExclusiveNfc();
- }
-
- protected void obtainSecurityTokenPin(RequiredInputParcel requiredInput) {
-
- try {
- Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
- requiredInput.getMasterKeyId(), requiredInput.getSubKeyId());
- if (passphrase != null) {
- mPin = passphrase;
- return;
- }
-
- Intent intent = new Intent(this, PassphraseDialogActivity.class);
- intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT,
- RequiredInputParcel.createRequiredPassphrase(requiredInput));
- startActivityForResult(intent, REQUEST_CODE_PIN);
- } catch (KeyNotFoundException e) {
- throw new AssertionError(
- "tried to find passphrase for non-existing key. this is a programming error!");
- }
-
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- switch (requestCode) {
- case REQUEST_CODE_PIN: {
- if (resultCode != Activity.RESULT_OK) {
- setResult(resultCode);
- finish();
- return;
- }
- CryptoInputParcel input = data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT);
- mPin = input.getPassphrase();
- break;
- }
- default:
- super.onActivityResult(requestCode, resultCode, data);
- }
- }
-
- /** Handle NFC communication and return a result.
- *
- * This method is called by onNewIntent above upon discovery of an NFC tag.
- * It handles initialization and login to the application, subsequently
- * calls either nfcCalculateSignature() or nfcDecryptSessionKey(), then
- * finishes the activity with an appropriate result.
- *
- * On general communication, see also
- * http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-a.aspx
- *
- * References to pages are generally related to the OpenPGP Application
- * on ISO SmartCard Systems specification.
- *
- */
- protected void handleTagDiscovered(Tag tag) throws IOException {
-
- // Connect to the detected tag, setting a couple of settings
- mIsoCard = AndroidCard.get(tag);
- if (mIsoCard == null) {
- throw new IsoDepNotSupportedException("Tag does not support ISO-DEP (ISO 14443-4)");
- }
- mIsoCard.setTimeout(TIMEOUT); // timeout is set to 100 seconds to avoid cancellation during calculation
- mIsoCard.connect();
-
- // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
- // See specification, page 51
- String accepted = "9000";
-
- // Command APDU (page 51) for SELECT FILE command (page 29)
- String opening =
- "00" // CLA
- + "A4" // INS
- + "04" // P1
- + "00" // P2
- + "06" // Lc (number of bytes)
- + "D27600012401" // Data (6 bytes)
- + "00"; // Le
- String response = nfcCommunicate(opening); // activate connection
- if ( ! response.endsWith(accepted) ) {
- throw new CardException("Initialization failed!", parseCardStatus(response));
- }
-
- byte[] pwStatusBytes = nfcGetPwStatusBytes();
- mPw1ValidForMultipleSignatures = (pwStatusBytes[0] == 1);
- mPw1ValidatedForSignature = false;
- mPw1ValidatedForDecrypt = false;
- mPw3Validated = false;
-
- doNfcInBackground();
-
- }
-
- public boolean isNfcConnected() {
- return mIsoCard.isConnected();
- }
-
- /** Return the key id from application specific data stored on tag, or null
- * if it doesn't exist.
- *
- * @param idx Index of the key to return the fingerprint from.
- * @return The long key id of the requested key, or null if not found.
- */
- public Long nfcGetKeyId(int idx) throws IOException {
- byte[] fp = nfcGetMasterKeyFingerprint(idx);
- if (fp == null) {
- return null;
- }
- ByteBuffer buf = ByteBuffer.wrap(fp);
- // skip first 12 bytes of the fingerprint
- buf.position(12);
- // the last eight bytes are the key id (big endian, which is default order in ByteBuffer)
- return buf.getLong();
- }
-
- /** Return fingerprints of all keys from application specific data stored
- * on tag, or null if data not available.
- *
- * @return The fingerprints of all subkeys in a contiguous byte array.
- */
- public byte[] nfcGetFingerprints() throws IOException {
- String data = "00CA006E00";
- byte[] buf = mIsoCard.transceive(Hex.decode(data));
-
- Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true);
- Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint());
-
- Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5);
- if (fptlv == null) {
- return null;
- }
-
- return fptlv.mV;
- }
-
- /** Return the PW Status Bytes from the token. This is a simple DO; no TLV decoding needed.
- *
- * @return Seven bytes in fixed format, plus 0x9000 status word at the end.
- */
- public byte[] nfcGetPwStatusBytes() throws IOException {
- String data = "00CA00C400";
- return mIsoCard.transceive(Hex.decode(data));
- }
-
- /** Return the fingerprint from application specific data stored on tag, or
- * null if it doesn't exist.
- *
- * @param idx Index of the key to return the fingerprint from.
- * @return The fingerprint of the requested key, or null if not found.
- */
- public byte[] nfcGetMasterKeyFingerprint(int idx) throws IOException {
- byte[] data = nfcGetFingerprints();
- if (data == null) {
- return null;
- }
-
- // return the master key fingerprint
- ByteBuffer fpbuf = ByteBuffer.wrap(data);
- byte[] fp = new byte[20];
- fpbuf.position(idx * 20);
- fpbuf.get(fp, 0, 20);
-
- return fp;
- }
-
- public byte[] nfcGetAid() throws IOException {
- String info = "00CA004F00";
- return mIsoCard.transceive(Hex.decode(info));
- }
-
- public String nfcGetUserId() throws IOException {
- String info = "00CA006500";
- return nfcGetHolderName(nfcCommunicate(info));
- }
-
- /**
- * Calls to calculate the signature and returns the MPI value
- *
- * @param hash the hash for signing
- * @return a big integer representing the MPI for the given hash
- */
- public byte[] nfcCalculateSignature(byte[] hash, int hashAlgo) throws IOException {
- if (!mPw1ValidatedForSignature) {
- nfcVerifyPIN(0x81); // (Verify PW1 with mode 81 for signing)
- }
-
- // dsi, including Lc
- String dsi;
-
- Log.i(Constants.TAG, "Hash: " + hashAlgo);
- switch (hashAlgo) {
- case HashAlgorithmTags.SHA1:
- if (hash.length != 20) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 10!");
- }
- dsi = "23" // Lc
- + "3021" // Tag/Length of Sequence, the 0x21 includes all following 33 bytes
- + "3009" // Tag/Length of Sequence, the 0x09 are the following header bytes
- + "0605" + "2B0E03021A" // OID of SHA1
- + "0500" // TLV coding of ZERO
- + "0414" + getHex(hash); // 0x14 are 20 hash bytes
- break;
- case HashAlgorithmTags.RIPEMD160:
- if (hash.length != 20) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 20!");
- }
- dsi = "233021300906052B2403020105000414" + getHex(hash);
- break;
- case HashAlgorithmTags.SHA224:
- if (hash.length != 28) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 28!");
- }
- dsi = "2F302D300D06096086480165030402040500041C" + getHex(hash);
- break;
- case HashAlgorithmTags.SHA256:
- if (hash.length != 32) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 32!");
- }
- dsi = "333031300D060960864801650304020105000420" + getHex(hash);
- break;
- case HashAlgorithmTags.SHA384:
- if (hash.length != 48) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 48!");
- }
- dsi = "433041300D060960864801650304020205000430" + getHex(hash);
- break;
- case HashAlgorithmTags.SHA512:
- if (hash.length != 64) {
- throw new IOException("Bad hash length (" + hash.length + ", expected 64!");
- }
- dsi = "533051300D060960864801650304020305000440" + getHex(hash);
- break;
- default:
- throw new IOException("Not supported hash algo!");
- }
-
- // Command APDU for PERFORM SECURITY OPERATION: COMPUTE DIGITAL SIGNATURE (page 37)
- String apdu =
- "002A9E9A" // CLA, INS, P1, P2
- + dsi // digital signature input
- + "00"; // Le
-
- String response = nfcCommunicate(apdu);
-
- // split up response into signature and status
- String status = response.substring(response.length()-4);
- String signature = response.substring(0, response.length() - 4);
-
- // while we are getting 0x61 status codes, retrieve more data
- while (status.substring(0, 2).equals("61")) {
- Log.d(Constants.TAG, "requesting more data, status " + status);
- // Send GET RESPONSE command
- response = nfcCommunicate("00C00000" + status.substring(2));
- status = response.substring(response.length()-4);
- signature += response.substring(0, response.length()-4);
- }
-
- Log.d(Constants.TAG, "final response:" + status);
-
- if (!mPw1ValidForMultipleSignatures) {
- mPw1ValidatedForSignature = false;
- }
-
- if ( ! "9000".equals(status)) {
- throw new CardException("Bad NFC response code: " + status, parseCardStatus(response));
- }
-
- // Make sure the signature we received is actually the expected number of bytes long!
- if (signature.length() != 256 && signature.length() != 512) {
- throw new IOException("Bad signature length! Expected 128 or 256 bytes, got " + signature.length() / 2);
- }
-
- return Hex.decode(signature);
- }
-
- /**
- * Calls to calculate the signature and returns the MPI value
- *
- * @param encryptedSessionKey the encoded session key
- * @return the decoded session key
- */
- public byte[] nfcDecryptSessionKey(byte[] encryptedSessionKey) throws IOException {
- if (!mPw1ValidatedForDecrypt) {
- nfcVerifyPIN(0x82); // (Verify PW1 with mode 82 for decryption)
- }
-
- String firstApdu = "102a8086fe";
- String secondApdu = "002a808603";
- String le = "00";
-
- byte[] one = new byte[254];
- // leave out first byte:
- System.arraycopy(encryptedSessionKey, 1, one, 0, one.length);
-
- byte[] two = new byte[encryptedSessionKey.length - 1 - one.length];
- for (int i = 0; i < two.length; i++) {
- two[i] = encryptedSessionKey[i + one.length + 1];
- }
-
- String first = nfcCommunicate(firstApdu + getHex(one));
- String second = nfcCommunicate(secondApdu + getHex(two) + le);
-
- String decryptedSessionKey = nfcGetDataField(second);
-
- return Hex.decode(decryptedSessionKey);
- }
-
- /** Verifies the user's PW1 or PW3 with the appropriate mode.
- *
- * @param mode For PW1, this is 0x81 for signing, 0x82 for everything else.
- * For PW3 (Admin PIN), mode is 0x83.
- */
- public void nfcVerifyPIN(int mode) throws IOException {
- if (mPin != null || mode == 0x83) {
-
- byte[] pin;
- if (mode == 0x83) {
- pin = mAdminPin.toStringUnsafe().getBytes();
- } else {
- pin = mPin.toStringUnsafe().getBytes();
- }
-
- // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time.
- // See specification, page 51
- String accepted = "9000";
- String response = tryPin(mode, pin); // login
- if (!response.equals(accepted)) {
- throw new CardException("Bad PIN!", parseCardStatus(response));
- }
-
- if (mode == 0x81) {
- mPw1ValidatedForSignature = true;
- } else if (mode == 0x82) {
- mPw1ValidatedForDecrypt = true;
- } else if (mode == 0x83) {
- mPw3Validated = true;
- }
- }
- }
-
- public void nfcResetCard() throws IOException {
- String accepted = "9000";
-
- // try wrong PIN 4 times until counter goes to C0
- byte[] pin = "XXXXXX".getBytes();
- for (int i = 0; i <= 4; i++) {
- String response = tryPin(0x81, pin);
- if (response.equals(accepted)) { // Should NOT accept!
- throw new CardException("Should never happen, XXXXXX has been accepted!", parseCardStatus(response));
- }
- }
-
- // try wrong Admin PIN 4 times until counter goes to C0
- byte[] adminPin = "XXXXXXXX".getBytes();
- for (int i = 0; i <= 4; i++) {
- String response = tryPin(0x83, adminPin);
- if (response.equals(accepted)) { // Should NOT accept!
- throw new CardException("Should never happen, XXXXXXXX has been accepted", parseCardStatus(response));
- }
- }
-
- // reactivate token!
- String reactivate1 = "00" + "e6" + "00" + "00";
- String reactivate2 = "00" + "44" + "00" + "00";
- String response1 = nfcCommunicate(reactivate1);
- String response2 = nfcCommunicate(reactivate2);
- if (!response1.equals(accepted) || !response2.equals(accepted)) {
- throw new CardException("Reactivating failed!", parseCardStatus(response1));
- }
-
- }
-
- private String tryPin(int mode, byte[] pin) throws IOException {
- // Command APDU for VERIFY command (page 32)
- String login =
- "00" // CLA
- + "20" // INS
- + "00" // P1
- + String.format("%02x", mode) // P2
- + String.format("%02x", pin.length) // Lc
- + Hex.toHexString(pin);
-
- return nfcCommunicate(login);
- }
-
- /** Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for
- * conformance to the token's requirements for key length.
- *
- * @param pw For PW1, this is 0x81. For PW3 (Admin PIN), mode is 0x83.
- * @param newPin The new PW1 or PW3.
- */
- public void nfcModifyPIN(int pw, byte[] newPin) throws IOException {
- final int MAX_PW1_LENGTH_INDEX = 1;
- final int MAX_PW3_LENGTH_INDEX = 3;
-
- byte[] pwStatusBytes = nfcGetPwStatusBytes();
-
- if (pw == 0x81) {
- if (newPin.length < 6 || newPin.length > pwStatusBytes[MAX_PW1_LENGTH_INDEX]) {
- throw new IOException("Invalid PIN length");
- }
- } else if (pw == 0x83) {
- if (newPin.length < 8 || newPin.length > pwStatusBytes[MAX_PW3_LENGTH_INDEX]) {
- throw new IOException("Invalid PIN length");
- }
- } else {
- throw new IOException("Invalid PW index for modify PIN operation");
- }
-
- byte[] pin;
- if (pw == 0x83) {
- pin = mAdminPin.toStringUnsafe().getBytes();
- } else {
- pin = mPin.toStringUnsafe().getBytes();
- }
-
- // Command APDU for CHANGE REFERENCE DATA command (page 32)
- String changeReferenceDataApdu = "00" // CLA
- + "24" // INS
- + "00" // P1
- + String.format("%02x", pw) // P2
- + String.format("%02x", pin.length + newPin.length) // Lc
- + getHex(pin)
- + getHex(newPin);
- String response = nfcCommunicate(changeReferenceDataApdu); // change PIN
- if (!response.equals("9000")) {
- throw new CardException("Failed to change PIN", parseCardStatus(response));
- }
- }
-
- /**
- * Stores a data object on the token. Automatically validates the proper PIN for the operation.
- * Supported for all data objects < 255 bytes in length. Only the cardholder certificate
- * (0x7F21) can exceed this length.
- *
- * @param dataObject The data object to be stored.
- * @param data The data to store in the object
- */
- public void nfcPutData(int dataObject, byte[] data) throws IOException {
- if (data.length > 254) {
- throw new IOException("Cannot PUT DATA with length > 254");
- }
- if (dataObject == 0x0101 || dataObject == 0x0103) {
- if (!mPw1ValidatedForDecrypt) {
- nfcVerifyPIN(0x82); // (Verify PW1 for non-signing operations)
- }
- } else if (!mPw3Validated) {
- nfcVerifyPIN(0x83); // (Verify PW3)
- }
-
- String putDataApdu = "00" // CLA
- + "DA" // INS
- + String.format("%02x", (dataObject & 0xFF00) >> 8) // P1
- + String.format("%02x", dataObject & 0xFF) // P2
- + String.format("%02x", data.length) // Lc
- + getHex(data);
-
- String response = nfcCommunicate(putDataApdu); // put data
- if (!response.equals("9000")) {
- throw new CardException("Failed to put data.", parseCardStatus(response));
- }
- }
-
- /**
- * Puts a key on the token in the given slot.
- *
- * @param slot The slot on the token where the key should be stored:
- * 0xB6: Signature Key
- * 0xB8: Decipherment Key
- * 0xA4: Authentication Key
- */
- public void nfcPutKey(int slot, CanonicalizedSecretKey secretKey, Passphrase passphrase)
- throws IOException {
- if (slot != 0xB6 && slot != 0xB8 && slot != 0xA4) {
- throw new IOException("Invalid key slot");
- }
-
- RSAPrivateCrtKey crtSecretKey;
- try {
- secretKey.unlock(passphrase);
- crtSecretKey = secretKey.getCrtSecretKey();
- } catch (PgpGeneralException e) {
- throw new IOException(e.getMessage());
- }
-
- // Shouldn't happen; the UI should block the user from getting an incompatible key this far.
- if (crtSecretKey.getModulus().bitLength() > 2048) {
- throw new IOException("Key too large to export to Security Token.");
- }
-
- // Should happen only rarely; all GnuPG keys since 2006 use public exponent 65537.
- if (!crtSecretKey.getPublicExponent().equals(new BigInteger("65537"))) {
- throw new IOException("Invalid public exponent for smart Security Token.");
- }
-
- if (!mPw3Validated) {
- nfcVerifyPIN(0x83); // (Verify PW3 with mode 83)
- }
-
- byte[] header= Hex.decode(
- "4D82" + "03A2" // Extended header list 4D82, length of 930 bytes. (page 23)
- + String.format("%02x", slot) + "00" // CRT to indicate targeted key, no length
- + "7F48" + "15" // Private key template 0x7F48, length 21 (decimal, 0x15 hex)
- + "9103" // Public modulus, length 3
- + "928180" // Prime P, length 128
- + "938180" // Prime Q, length 128
- + "948180" // Coefficient (1/q mod p), length 128
- + "958180" // Prime exponent P (d mod (p - 1)), length 128
- + "968180" // Prime exponent Q (d mod (1 - 1)), length 128
- + "97820100" // Modulus, length 256, last item in private key template
- + "5F48" + "820383");// DO 5F48; 899 bytes of concatenated key data will follow
- byte[] dataToSend = new byte[934];
- byte[] currentKeyObject;
- int offset = 0;
-
- System.arraycopy(header, 0, dataToSend, offset, header.length);
- offset += header.length;
- currentKeyObject = crtSecretKey.getPublicExponent().toByteArray();
- System.arraycopy(currentKeyObject, 0, dataToSend, offset, 3);
- offset += 3;
- // NOTE: For a 2048-bit key, these lengths are fixed. However, bigint includes a leading 0
- // in the array to represent sign, so we take care to set the offset to 1 if necessary.
- currentKeyObject = crtSecretKey.getPrimeP().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getPrimeQ().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getCrtCoefficient().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getPrimeExponentP().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getPrimeExponentQ().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 128, dataToSend, offset, 128);
- Arrays.fill(currentKeyObject, (byte)0);
- offset += 128;
- currentKeyObject = crtSecretKey.getModulus().toByteArray();
- System.arraycopy(currentKeyObject, currentKeyObject.length - 256, dataToSend, offset, 256);
-
- String putKeyCommand = "10DB3FFF";
- String lastPutKeyCommand = "00DB3FFF";
-
- // Now we're ready to communicate with the token.
- offset = 0;
- String response;
- while(offset < dataToSend.length) {
- int dataRemaining = dataToSend.length - offset;
- if (dataRemaining > 254) {
- response = nfcCommunicate(
- putKeyCommand + "FE" + Hex.toHexString(dataToSend, offset, 254)
- );
- offset += 254;
- } else {
- int length = dataToSend.length - offset;
- response = nfcCommunicate(
- lastPutKeyCommand + String.format("%02x", length)
- + Hex.toHexString(dataToSend, offset, length));
- offset += length;
- }
-
- if (!response.endsWith("9000")) {
- throw new CardException("Key export to Security Token failed", parseCardStatus(response));
- }
- }
-
- // Clear array with secret data before we return.
- Arrays.fill(dataToSend, (byte) 0);
- }
-
- /**
- * Parses out the status word from a JavaCard response string.
- *
- * @param response A hex string with the response from the token
- * @return A short indicating the SW1/SW2, or 0 if a status could not be determined.
- */
- short parseCardStatus(String response) {
- if (response.length() < 4) {
- return 0; // invalid input
- }
-
- try {
- return Short.parseShort(response.substring(response.length() - 4), 16);
- } catch (NumberFormatException e) {
- return 0;
- }
- }
-
- public String nfcGetHolderName(String name) {
- try {
- String slength;
- int ilength;
- name = name.substring(6);
- slength = name.substring(0, 2);
- ilength = Integer.parseInt(slength, 16) * 2;
- name = name.substring(2, ilength + 2);
- name = (new String(Hex.decode(name))).replace('<', ' ');
- return name;
- } catch (IndexOutOfBoundsException e) {
- // try-catch for https://github.com/FluffyKaon/OpenPGP-Card
- // Note: This should not happen, but happens with
- // https://github.com/FluffyKaon/OpenPGP-Card, thus return an empty string for now!
-
- Log.e(Constants.TAG, "Couldn't get holder name, returning empty string!", e);
- return "";
- }
- }
-
- private String nfcGetDataField(String output) {
- return output.substring(0, output.length() - 4);
- }
-
- public String nfcCommunicate(String apdu) throws IOException {
- return getHex(mIsoCard.transceive(Hex.decode(apdu)));
- }
-
- public static String getHex(byte[] raw) {
- return new String(Hex.encode(raw));
- }
-
- public class IsoDepNotSupportedException extends IOException {
-
- public IsoDepNotSupportedException(String detailMessage) {
- super(detailMessage);
- }
-
- }
-
- public class CardException extends IOException {
- private short mResponseCode;
-
- public CardException(String detailMessage, short responseCode) {
- super(detailMessage);
- mResponseCode = responseCode;
- }
-
- public short getResponseCode() {
- return mResponseCode;
- }
-
- }
-
- private boolean isFidesmoDevice() {
- if (isNfcConnected()) { // Check if we can still talk to the card
- try {
- // By trying to select any apps that have the Fidesmo AID prefix we can
- // see if it is a Fidesmo device or not
- byte[] mSelectResponse = mIsoCard.transceive(Apdu.select(FIDESMO_APPS_AID_PREFIX));
- // Compare the status returned by our select with the OK status code
- return Apdu.hasStatus(mSelectResponse, Apdu.OK_APDU);
- } catch (IOException e) {
- Log.e(Constants.TAG, "Card communication failed!", e);
- }
- }
- return false;
- }
-
- /**
- * Ask user if she wants to install PGP onto her Fidesmo device
- */
- private void promptFidesmoPgpInstall() {
- FidesmoPgpInstallDialog mFidesmoPgpInstallDialog = new FidesmoPgpInstallDialog();
- mFidesmoPgpInstallDialog.show(getSupportFragmentManager(), "mFidesmoPgpInstallDialog");
- }
-
- /**
- * Show a Dialog to the user informing that Fidesmo App must be installed and with option
- * to launch the Google Play store.
- */
- private void promptFidesmoAppInstall() {
- FidesmoInstallDialog mFidesmoInstallDialog = new FidesmoInstallDialog();
- mFidesmoInstallDialog.show(getSupportFragmentManager(), "mFidesmoInstallDialog");
- }
-
- /**
- * Use the package manager to detect if an application is installed on the phone
- * @param uri an URI identifying the application's package
- * @return 'true' if the app is installed
- */
- private boolean isAndroidAppInstalled(String uri) {
- PackageManager mPackageManager = getPackageManager();
- boolean mAppInstalled = false;
- try {
- mPackageManager.getPackageInfo(uri, PackageManager.GET_ACTIVITIES);
- mAppInstalled = true;
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(Constants.TAG, "App not installed on Android device");
- mAppInstalled = false;
- }
- return mAppInstalled;
- }
-}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
index 451065d..ad15c8f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java
@@ -130,9 +130,9 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
switch (requiredInput.mType) {
// always use CryptoOperationHelper.startActivityForResult!
- case NFC_MOVE_KEY_TO_CARD:
- case NFC_DECRYPT:
- case NFC_SIGN: {
+ case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
+ case SECURITY_TOKEN_DECRYPT:
+ case SECURITY_TOKEN_SIGN: {
Intent intent = new Intent(activity, SecurityTokenOperationActivity.class);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel);
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java
index 3d96f3c..3dcc2f5 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java
@@ -50,14 +50,13 @@ import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Request;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
-import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
-import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.TlsHelper;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
@@ -354,19 +353,15 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
Log.d("Converted URL", newKeyserver.toString());
- OkHttpClient client = HkpKeyserver.getClient(newKeyserver.toURL(), proxy);
-
- // don't follow any redirects
- client.setFollowRedirects(false);
- client.setFollowSslRedirects(false);
-
if (onlyTrustedKeyserver
- && !TlsHelper.usePinnedCertificateIfAvailable(client, newKeyserver.toURL())) {
+ && TlsHelper.getPinnedSslSocketFactory(newKeyserver.toURL()) == null) {
Log.w(Constants.TAG, "No pinned certificate for this host in OpenKeychain's assets.");
reason = FailureReason.NO_PINNED_CERTIFICATE;
return reason;
}
+ OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(newKeyserver.toURL(), proxy);
+
client.newCall(new Request.Builder().url(newKeyserver.toURL()).build()).execute();
} catch (TlsHelper.TlsHelperException e) {
reason = FailureReason.CONNECTION_FAILED;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java
index 5b75723..ce16653 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java
@@ -17,25 +17,26 @@
package org.sufficientlysecure.keychain.ui.dialog;
-import android.annotation.TargetApi;
+import android.annotation.SuppressLint;
import android.app.Dialog;
-import android.os.Build;
+import android.content.Context;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentActivity;
import android.support.v7.app.AlertDialog;
-import android.text.Editable;
-import android.text.TextWatcher;
+import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
-import android.view.inputmethod.InputMethodManager;
+import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.DatePicker;
-import android.widget.EditText;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
import android.widget.Spinner;
import android.widget.TableRow;
import android.widget.TextView;
@@ -49,14 +50,18 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve;
import org.sufficientlysecure.keychain.util.Choice;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Calendar;
+import java.util.List;
import java.util.TimeZone;
public class AddSubkeyDialogFragment extends DialogFragment {
public interface OnAlgorithmSelectedListener {
- public void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey);
+ void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey);
+ }
+
+ public enum SupportedKeyType {
+ RSA_2048, RSA_3072, RSA_4096, ECC_P256, ECC_P521
}
private static final String ARG_WILL_BE_MASTER_KEY = "will_be_master_key";
@@ -66,18 +71,12 @@ public class AddSubkeyDialogFragment extends DialogFragment {
private CheckBox mNoExpiryCheckBox;
private TableRow mExpiryRow;
private DatePicker mExpiryDatePicker;
- private Spinner mAlgorithmSpinner;
- private View mKeySizeRow;
- private Spinner mKeySizeSpinner;
- private View mCurveRow;
- private Spinner mCurveSpinner;
- private TextView mCustomKeyTextView;
- private EditText mCustomKeyEditText;
- private TextView mCustomKeyInfoTextView;
- private CheckBox mFlagCertify;
- private CheckBox mFlagSign;
- private CheckBox mFlagEncrypt;
- private CheckBox mFlagAuthenticate;
+ private Spinner mKeyTypeSpinner;
+ private RadioGroup mUsageRadioGroup;
+ private RadioButton mUsageNone;
+ private RadioButton mUsageSign;
+ private RadioButton mUsageEncrypt;
+ private RadioButton mUsageSignAndEncrypt;
private boolean mWillBeMasterKey;
@@ -96,6 +95,8 @@ public class AddSubkeyDialogFragment extends DialogFragment {
return frag;
}
+
+ @NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final FragmentActivity context = getActivity();
@@ -106,25 +107,27 @@ public class AddSubkeyDialogFragment extends DialogFragment {
CustomAlertDialogBuilder dialog = new CustomAlertDialogBuilder(context);
+ @SuppressLint("InflateParams")
View view = mInflater.inflate(R.layout.add_subkey_dialog, null);
dialog.setView(view);
- dialog.setTitle(R.string.title_add_subkey);
mNoExpiryCheckBox = (CheckBox) view.findViewById(R.id.add_subkey_no_expiry);
mExpiryRow = (TableRow) view.findViewById(R.id.add_subkey_expiry_row);
mExpiryDatePicker = (DatePicker) view.findViewById(R.id.add_subkey_expiry_date_picker);
- mAlgorithmSpinner = (Spinner) view.findViewById(R.id.add_subkey_algorithm);
- mKeySizeSpinner = (Spinner) view.findViewById(R.id.add_subkey_size);
- mCurveSpinner = (Spinner) view.findViewById(R.id.add_subkey_curve);
- mKeySizeRow = view.findViewById(R.id.add_subkey_row_size);
- mCurveRow = view.findViewById(R.id.add_subkey_row_curve);
- mCustomKeyTextView = (TextView) view.findViewById(R.id.add_subkey_custom_key_size_label);
- mCustomKeyEditText = (EditText) view.findViewById(R.id.add_subkey_custom_key_size_input);
- mCustomKeyInfoTextView = (TextView) view.findViewById(R.id.add_subkey_custom_key_size_info);
- mFlagCertify = (CheckBox) view.findViewById(R.id.add_subkey_flag_certify);
- mFlagSign = (CheckBox) view.findViewById(R.id.add_subkey_flag_sign);
- mFlagEncrypt = (CheckBox) view.findViewById(R.id.add_subkey_flag_encrypt);
- mFlagAuthenticate = (CheckBox) view.findViewById(R.id.add_subkey_flag_authenticate);
+ mKeyTypeSpinner = (Spinner) view.findViewById(R.id.add_subkey_type);
+ mUsageRadioGroup = (RadioGroup) view.findViewById(R.id.add_subkey_usage_group);
+ mUsageNone = (RadioButton) view.findViewById(R.id.add_subkey_usage_none);
+ mUsageSign = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign);
+ mUsageEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_encrypt);
+ mUsageSignAndEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign_and_encrypt);
+
+ if(mWillBeMasterKey) {
+ dialog.setTitle(R.string.title_change_master_key);
+ mUsageNone.setVisibility(View.VISIBLE);
+ mUsageNone.setChecked(true);
+ } else {
+ dialog.setTitle(R.string.title_add_subkey);
+ }
mNoExpiryCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
@@ -143,65 +146,24 @@ public class AddSubkeyDialogFragment extends DialogFragment {
mExpiryDatePicker.setMinDate(minDateCal.getTime().getTime());
{
- ArrayList<Choice<Algorithm>> choices = new ArrayList<>();
- choices.add(new Choice<>(Algorithm.DSA, getResources().getString(
- R.string.dsa)));
- if (!mWillBeMasterKey) {
- choices.add(new Choice<>(Algorithm.ELGAMAL, getResources().getString(
- R.string.elgamal)));
- }
- choices.add(new Choice<>(Algorithm.RSA, getResources().getString(
- R.string.rsa)));
- choices.add(new Choice<>(Algorithm.ECDSA, getResources().getString(
- R.string.ecdsa)));
- choices.add(new Choice<>(Algorithm.ECDH, getResources().getString(
- R.string.ecdh)));
- ArrayAdapter<Choice<Algorithm>> adapter = new ArrayAdapter<>(context,
+ ArrayList<Choice<SupportedKeyType>> choices = new ArrayList<>();
+ choices.add(new Choice<>(SupportedKeyType.RSA_2048, getResources().getString(
+ R.string.rsa_2048), getResources().getString(R.string.rsa_2048_description_html)));
+ choices.add(new Choice<>(SupportedKeyType.RSA_3072, getResources().getString(
+ R.string.rsa_3072), getResources().getString(R.string.rsa_3072_description_html)));
+ choices.add(new Choice<>(SupportedKeyType.RSA_4096, getResources().getString(
+ R.string.rsa_4096), getResources().getString(R.string.rsa_4096_description_html)));
+ choices.add(new Choice<>(SupportedKeyType.ECC_P256, getResources().getString(
+ R.string.ecc_p256), getResources().getString(R.string.ecc_p256_description_html)));
+ choices.add(new Choice<>(SupportedKeyType.ECC_P521, getResources().getString(
+ R.string.ecc_p521), getResources().getString(R.string.ecc_p521_description_html)));
+ TwoLineArrayAdapter adapter = new TwoLineArrayAdapter(context,
android.R.layout.simple_spinner_item, choices);
- adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- mAlgorithmSpinner.setAdapter(adapter);
- // make RSA the default
+ mKeyTypeSpinner.setAdapter(adapter);
+ // make RSA 3072 the default
for (int i = 0; i < choices.size(); ++i) {
- if (choices.get(i).getId() == Algorithm.RSA) {
- mAlgorithmSpinner.setSelection(i);
- break;
- }
- }
- }
-
- // dynamic ArrayAdapter must be created (instead of ArrayAdapter.getFromResource), because it's content may change
- ArrayAdapter<CharSequence> keySizeAdapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item,
- new ArrayList<CharSequence>(Arrays.asList(getResources().getStringArray(R.array.rsa_key_size_spinner_values))));
- keySizeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- mKeySizeSpinner.setAdapter(keySizeAdapter);
- mKeySizeSpinner.setSelection(1); // Default to 4096 for the key length
-
- {
- ArrayList<Choice<Curve>> choices = new ArrayList<>();
-
- choices.add(new Choice<>(Curve.NIST_P256, getResources().getString(
- R.string.key_curve_nist_p256)));
- choices.add(new Choice<>(Curve.NIST_P384, getResources().getString(
- R.string.key_curve_nist_p384)));
- choices.add(new Choice<>(Curve.NIST_P521, getResources().getString(
- R.string.key_curve_nist_p521)));
-
- /* @see SaveKeyringParcel
- choices.add(new Choice<Curve>(Curve.BRAINPOOL_P256, getResources().getString(
- R.string.key_curve_bp_p256)));
- choices.add(new Choice<Curve>(Curve.BRAINPOOL_P384, getResources().getString(
- R.string.key_curve_bp_p384)));
- choices.add(new Choice<Curve>(Curve.BRAINPOOL_P512, getResources().getString(
- R.string.key_curve_bp_p512)));
- */
-
- ArrayAdapter<Choice<Curve>> adapter = new ArrayAdapter<>(context,
- android.R.layout.simple_spinner_item, choices);
- mCurveSpinner.setAdapter(adapter);
- // make NIST P-256 the default
- for (int i = 0; i < choices.size(); ++i) {
- if (choices.get(i).getId() == Curve.NIST_P256) {
- mCurveSpinner.setSelection(i);
+ if (choices.get(i).getId() == SupportedKeyType.RSA_3072) {
+ mKeyTypeSpinner.setSelection(i);
break;
}
}
@@ -215,45 +177,35 @@ public class AddSubkeyDialogFragment extends DialogFragment {
final AlertDialog alertDialog = dialog.show();
- mCustomKeyEditText.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) {
- }
+ mKeyTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
- public void afterTextChanged(Editable s) {
- setOkButtonAvailability(alertDialog);
- }
- });
-
- mKeySizeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
- setCustomKeyVisibility();
- setOkButtonAvailability(alertDialog);
- }
+ // noinspection unchecked
+ SupportedKeyType keyType = ((Choice<SupportedKeyType>) parent.getSelectedItem()).getId();
- @Override
- public void onNothingSelected(AdapterView<?> parent) {
- }
- });
+ // RadioGroup.getCheckedRadioButtonId() gives the wrong RadioButton checked
+ // when programmatically unchecking children radio buttons. Clearing all is the only option.
+ mUsageRadioGroup.clearCheck();
- mAlgorithmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
- updateUiForAlgorithm(((Choice<Algorithm>) parent.getSelectedItem()).getId());
+ if(mWillBeMasterKey) {
+ mUsageNone.setChecked(true);
+ }
- setCustomKeyVisibility();
- setOkButtonAvailability(alertDialog);
+ if (keyType == SupportedKeyType.ECC_P521 || keyType == SupportedKeyType.ECC_P256) {
+ mUsageSignAndEncrypt.setEnabled(false);
+ if (mWillBeMasterKey) {
+ mUsageEncrypt.setEnabled(false);
+ }
+ } else {
+ // need to enable if previously disabled for ECC masterkey
+ mUsageEncrypt.setEnabled(true);
+ mUsageSignAndEncrypt.setEnabled(true);
+ }
}
@Override
- public void onNothingSelected(AdapterView<?> parent) {
- }
+ public void onNothingSelected(AdapterView<?> parent) {}
});
return alertDialog;
@@ -269,36 +221,74 @@ public class AddSubkeyDialogFragment extends DialogFragment {
positiveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- if (!mFlagCertify.isChecked() && !mFlagSign.isChecked()
- && !mFlagEncrypt.isChecked() && !mFlagAuthenticate.isChecked()) {
- Toast.makeText(getActivity(), R.string.edit_key_select_flag, Toast.LENGTH_LONG).show();
+ if (mUsageRadioGroup.getCheckedRadioButtonId() == -1) {
+ Toast.makeText(getActivity(), R.string.edit_key_select_usage, Toast.LENGTH_LONG).show();
return;
}
- Algorithm algorithm = ((Choice<Algorithm>) mAlgorithmSpinner.getSelectedItem()).getId();
+ // noinspection unchecked
+ SupportedKeyType keyType = ((Choice<SupportedKeyType>) mKeyTypeSpinner.getSelectedItem()).getId();
Curve curve = null;
Integer keySize = null;
- // For EC keys, add a curve
- if (algorithm == Algorithm.ECDH || algorithm == Algorithm.ECDSA) {
- curve = ((Choice<Curve>) mCurveSpinner.getSelectedItem()).getId();
- // Otherwise, get a keysize
- } else {
- keySize = getProperKeyLength(algorithm, getSelectedKeyLength());
+ Algorithm algorithm = null;
+
+ // set keysize & curve, for RSA & ECC respectively
+ switch (keyType) {
+ case RSA_2048: {
+ keySize = 2048;
+ break;
+ }
+ case RSA_3072: {
+ keySize = 3072;
+ break;
+ }
+ case RSA_4096: {
+ keySize = 4096;
+ break;
+ }
+ case ECC_P256: {
+ curve = Curve.NIST_P256;
+ break;
+ }
+ case ECC_P521: {
+ curve = Curve.NIST_P521;
+ break;
+ }
}
+ // set algorithm
+ switch (keyType) {
+ case RSA_2048:
+ case RSA_3072:
+ case RSA_4096: {
+ algorithm = Algorithm.RSA;
+ break;
+ }
+
+ case ECC_P256:
+ case ECC_P521: {
+ if(mUsageEncrypt.isChecked()) {
+ algorithm = Algorithm.ECDH;
+ } else {
+ algorithm = Algorithm.ECDSA;
+ }
+ break;
+ }
+ }
+
+ // set flags
int flags = 0;
- if (mFlagCertify.isChecked()) {
+ if (mWillBeMasterKey) {
flags |= KeyFlags.CERTIFY_OTHER;
}
- if (mFlagSign.isChecked()) {
+ if (mUsageSign.isChecked()) {
flags |= KeyFlags.SIGN_DATA;
- }
- if (mFlagEncrypt.isChecked()) {
+ } else if (mUsageEncrypt.isChecked()) {
flags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
+ } else if (mUsageSignAndEncrypt.isChecked()) {
+ flags |= KeyFlags.SIGN_DATA | KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
}
- if (mFlagAuthenticate.isChecked()) {
- flags |= KeyFlags.AUTHENTICATION;
- }
+
long expiry;
if (mNoExpiryCheckBox.isChecked()) {
@@ -332,206 +322,29 @@ public class AddSubkeyDialogFragment extends DialogFragment {
}
}
- private int getSelectedKeyLength() {
- final String selectedItemString = (String) mKeySizeSpinner.getSelectedItem();
- final String customLengthString = getResources().getString(R.string.key_size_custom);
- final boolean customSelected = customLengthString.equals(selectedItemString);
- String keyLengthString = customSelected ? mCustomKeyEditText.getText().toString() : selectedItemString;
- int keySize;
- try {
- keySize = Integer.parseInt(keyLengthString);
- } catch (NumberFormatException e) {
- keySize = 0;
- }
- return keySize;
- }
-
- /**
- * <h3>RSA</h3>
- * <p>for RSA algorithm, key length must be greater than 2048. Possibility to generate keys bigger
- * than 8192 bits is currently disabled, because it's almost impossible to generate them on a mobile device (check
- * <a href="http://www.javamex.com/tutorials/cryptography/rsa_key_length.shtml">RSA key length plot</a> and
- * <a href="http://www.keylength.com/">Cryptographic Key Length Recommendation</a>). Also, key length must be a
- * multiplicity of 8.</p>
- * <h3>ElGamal</h3>
- * <p>For ElGamal algorithm, supported key lengths are 2048, 3072, 4096 or 8192 bits.</p>
- * <h3>DSA</h3>
- * <p>For DSA algorithm key length must be between 2048 and 3072. Also, it must me dividable by 64.</p>
- *
- * @return correct key length, according to BouncyCastle specification. Returns <code>-1</code>, if key length is
- * inappropriate.
- */
- private int getProperKeyLength(Algorithm algorithm, int currentKeyLength) {
- final int[] elGamalSupportedLengths = {2048, 3072, 4096, 8192};
- int properKeyLength = -1;
- switch (algorithm) {
- case RSA: {
- if (currentKeyLength >= 2048 && currentKeyLength <= 16384) {
- properKeyLength = currentKeyLength + ((8 - (currentKeyLength % 8)) % 8);
- }
- break;
- }
- case ELGAMAL: {
- int[] elGammalKeyDiff = new int[elGamalSupportedLengths.length];
- for (int i = 0; i < elGamalSupportedLengths.length; i++) {
- elGammalKeyDiff[i] = Math.abs(elGamalSupportedLengths[i] - currentKeyLength);
- }
- int minimalValue = Integer.MAX_VALUE;
- int minimalIndex = -1;
- for (int i = 0; i < elGammalKeyDiff.length; i++) {
- if (elGammalKeyDiff[i] <= minimalValue) {
- minimalValue = elGammalKeyDiff[i];
- minimalIndex = i;
- }
- }
- properKeyLength = elGamalSupportedLengths[minimalIndex];
- break;
- }
- case DSA: {
- // Bouncy Castle supports 4096 maximum
- if (currentKeyLength >= 2048 && currentKeyLength <= 4096) {
- properKeyLength = currentKeyLength + ((64 - (currentKeyLength % 64)) % 64);
- }
- break;
- }
+ private class TwoLineArrayAdapter extends ArrayAdapter<Choice<SupportedKeyType>> {
+ public TwoLineArrayAdapter(Context context, int resource, List<Choice<SupportedKeyType>> objects) {
+ super(context, resource, objects);
}
- return properKeyLength;
- }
- private void setOkButtonAvailability(AlertDialog alertDialog) {
- Algorithm algorithm = ((Choice<Algorithm>) mAlgorithmSpinner.getSelectedItem()).getId();
- boolean enabled = algorithm == Algorithm.ECDSA || algorithm == Algorithm.ECDH
- || getProperKeyLength(algorithm, getSelectedKeyLength()) > 0;
- alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
- }
- private void setCustomKeyVisibility() {
- final String selectedItemString = (String) mKeySizeSpinner.getSelectedItem();
- final String customLengthString = getResources().getString(R.string.key_size_custom);
- final boolean customSelected = customLengthString.equals(selectedItemString);
- final int visibility = customSelected ? View.VISIBLE : View.GONE;
-
- mCustomKeyEditText.setVisibility(visibility);
- mCustomKeyTextView.setVisibility(visibility);
- mCustomKeyInfoTextView.setVisibility(visibility);
-
- // hide keyboard after setting visibility to gone
- if (visibility == View.GONE) {
- InputMethodManager imm = (InputMethodManager)
- getActivity().getSystemService(FragmentActivity.INPUT_METHOD_SERVICE);
- imm.hideSoftInputFromWindow(mCustomKeyEditText.getWindowToken(), 0);
- }
- }
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ // inflate view if not given one
+ if (convertView == null) {
+ convertView = getActivity().getLayoutInflater()
+ .inflate(R.layout.two_line_spinner_dropdown_item, parent, false);
+ }
- private void updateUiForAlgorithm(Algorithm algorithm) {
- final ArrayAdapter<CharSequence> keySizeAdapter = (ArrayAdapter<CharSequence>) mKeySizeSpinner.getAdapter();
- keySizeAdapter.clear();
- switch (algorithm) {
- case RSA: {
- replaceArrayAdapterContent(keySizeAdapter, R.array.rsa_key_size_spinner_values);
- mKeySizeSpinner.setSelection(1);
- mKeySizeRow.setVisibility(View.VISIBLE);
- mCurveRow.setVisibility(View.GONE);
- mCustomKeyInfoTextView.setText(getResources().getString(R.string.key_size_custom_info_rsa));
- // allowed flags:
- mFlagSign.setEnabled(true);
- mFlagEncrypt.setEnabled(true);
- mFlagAuthenticate.setEnabled(true);
-
- if (mWillBeMasterKey) {
- mFlagCertify.setEnabled(true);
-
- mFlagCertify.setChecked(true);
- mFlagSign.setChecked(false);
- mFlagEncrypt.setChecked(false);
- } else {
- mFlagCertify.setEnabled(false);
+ Choice c = this.getItem(position);
- mFlagCertify.setChecked(false);
- mFlagSign.setChecked(true);
- mFlagEncrypt.setChecked(true);
- }
- mFlagAuthenticate.setChecked(false);
- break;
- }
- case ELGAMAL: {
- replaceArrayAdapterContent(keySizeAdapter, R.array.elgamal_key_size_spinner_values);
- mKeySizeSpinner.setSelection(3);
- mKeySizeRow.setVisibility(View.VISIBLE);
- mCurveRow.setVisibility(View.GONE);
- mCustomKeyInfoTextView.setText(""); // ElGamal does not support custom key length
- // allowed flags:
- mFlagCertify.setChecked(false);
- mFlagCertify.setEnabled(false);
- mFlagSign.setChecked(false);
- mFlagSign.setEnabled(false);
- mFlagEncrypt.setChecked(true);
- mFlagEncrypt.setEnabled(true);
- mFlagAuthenticate.setChecked(false);
- mFlagAuthenticate.setEnabled(false);
- break;
- }
- case DSA: {
- replaceArrayAdapterContent(keySizeAdapter, R.array.dsa_key_size_spinner_values);
- mKeySizeSpinner.setSelection(2);
- mKeySizeRow.setVisibility(View.VISIBLE);
- mCurveRow.setVisibility(View.GONE);
- mCustomKeyInfoTextView.setText(getResources().getString(R.string.key_size_custom_info_dsa));
- // allowed flags:
- mFlagCertify.setChecked(false);
- mFlagCertify.setEnabled(false);
- mFlagSign.setChecked(true);
- mFlagSign.setEnabled(true);
- mFlagEncrypt.setChecked(false);
- mFlagEncrypt.setEnabled(false);
- mFlagAuthenticate.setChecked(false);
- mFlagAuthenticate.setEnabled(false);
- break;
- }
- case ECDSA: {
- mKeySizeRow.setVisibility(View.GONE);
- mCurveRow.setVisibility(View.VISIBLE);
- mCustomKeyInfoTextView.setText("");
- // allowed flags:
- mFlagCertify.setEnabled(mWillBeMasterKey);
- mFlagCertify.setChecked(mWillBeMasterKey);
- mFlagSign.setEnabled(true);
- mFlagSign.setChecked(!mWillBeMasterKey);
- mFlagEncrypt.setEnabled(false);
- mFlagEncrypt.setChecked(false);
- mFlagAuthenticate.setEnabled(true);
- mFlagAuthenticate.setChecked(false);
- break;
- }
- case ECDH: {
- mKeySizeRow.setVisibility(View.GONE);
- mCurveRow.setVisibility(View.VISIBLE);
- mCustomKeyInfoTextView.setText("");
- // allowed flags:
- mFlagCertify.setChecked(false);
- mFlagCertify.setEnabled(false);
- mFlagSign.setChecked(false);
- mFlagSign.setEnabled(false);
- mFlagEncrypt.setChecked(true);
- mFlagEncrypt.setEnabled(true);
- mFlagAuthenticate.setChecked(false);
- mFlagAuthenticate.setEnabled(false);
- break;
- }
- }
- keySizeAdapter.notifyDataSetChanged();
+ TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
+ TextView text2 = (TextView) convertView.findViewById(android.R.id.text2);
- }
+ text1.setText(c.getName());
+ text2.setText(Html.fromHtml(c.getDescription()));
- @TargetApi(Build.VERSION_CODES.HONEYCOMB)
- private void replaceArrayAdapterContent(ArrayAdapter<CharSequence> arrayAdapter, int stringArrayResourceId) {
- final String[] spinnerValuesStringArray = getResources().getStringArray(stringArrayResourceId);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
- arrayAdapter.addAll(spinnerValuesStringArray);
- } else {
- for (final String value : spinnerValuesStringArray) {
- arrayAdapter.add(value);
- }
+ return convertView;
}
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyDialogFragment.java
index b516487..0c0877b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyDialogFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/EditSubkeyDialogFragment.java
@@ -35,7 +35,11 @@ public class EditSubkeyDialogFragment extends DialogFragment {
public static final int MESSAGE_CHANGE_EXPIRY = 1;
public static final int MESSAGE_REVOKE = 2;
public static final int MESSAGE_STRIP = 3;
- public static final int MESSAGE_MOVE_KEY_TO_CARD = 4;
+ public static final int MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN = 4;
+ public static final int SUBKEY_MENU_CHANGE_EXPIRY = 0;
+ public static final int SUBKEY_MENU_REVOKE_SUBKEY = 1;
+ public static final int SUBKEY_MENU_STRIP_SUBKEY = 2;
+ public static final int SUBKEY_MENU_MOVE_TO_SECURITY_TOKEN = 3;
private Messenger mMessenger;
@@ -68,17 +72,17 @@ public class EditSubkeyDialogFragment extends DialogFragment {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
- case 0:
+ case SUBKEY_MENU_CHANGE_EXPIRY:
sendMessageToHandler(MESSAGE_CHANGE_EXPIRY, null);
break;
- case 1:
+ case SUBKEY_MENU_REVOKE_SUBKEY:
sendMessageToHandler(MESSAGE_REVOKE, null);
break;
- case 2:
- sendMessageToHandler(MESSAGE_STRIP, null);
+ case SUBKEY_MENU_STRIP_SUBKEY:
+ showAlertDialog();
break;
- case 3:
- sendMessageToHandler(MESSAGE_MOVE_KEY_TO_CARD, null);
+ case SUBKEY_MENU_MOVE_TO_SECURITY_TOKEN:
+ sendMessageToHandler(MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN, null);
break;
default:
break;
@@ -95,6 +99,25 @@ public class EditSubkeyDialogFragment extends DialogFragment {
return builder.show();
}
+ private void showAlertDialog() {
+ CustomAlertDialogBuilder stripAlertDialog = new CustomAlertDialogBuilder(getActivity());
+ stripAlertDialog.setTitle(R.string.title_alert_strip).
+ setMessage(R.string.alert_strip).setCancelable(true);
+ stripAlertDialog.setPositiveButton(R.string.strip, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ sendMessageToHandler(MESSAGE_STRIP, null);
+ }
+ });
+ stripAlertDialog.setNegativeButton(R.string.btn_do_not_save, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dismiss();
+ }
+ });
+ stripAlertDialog.show();
+ }
+
/**
* Send message back to handler which is initialized in a activity
*
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java
index c25f775..334a736 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java
@@ -119,7 +119,7 @@ public class LinkedIdCreateTwitterStep1Fragment extends Fragment {
private static Boolean checkHandle(String handle) {
try {
HttpURLConnection nection =
- (HttpURLConnection) new URL("https://twitter.com/" + handle).openConnection();
+ (HttpURLConnection) new URL("https://twitter.com/" + handle).getUrlResponse();
nection.setRequestMethod("HEAD");
nection.setRequestProperty("User-Agent", "OpenKeychain");
return nection.getResponseCode() == 200;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
index 5fcc3d5..4dc0eba 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java
@@ -64,8 +64,8 @@ public class KeyFormattingUtils {
String algorithmStr;
switch (algorithm) {
- case PublicKeyAlgorithmTags.RSA_ENCRYPT:
case PublicKeyAlgorithmTags.RSA_GENERAL:
+ case PublicKeyAlgorithmTags.RSA_ENCRYPT:
case PublicKeyAlgorithmTags.RSA_SIGN: {
algorithmStr = "RSA";
break;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/spinner/FocusFirstItemSpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/spinner/FocusFirstItemSpinner.java
new file mode 100644
index 0000000..7919a09
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/spinner/FocusFirstItemSpinner.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 Alex Fong Jie Wen <alexfongg@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.ui.util.spinner;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Spinner;
+
+/**
+ * Custom spinner which uses a hack to
+ * always set focus on first item in list
+ *
+ */
+public class FocusFirstItemSpinner extends Spinner {
+ /**
+ * Spinner is originally designed to set focus on the currently selected item.
+ * When Spinner is selected to show dropdown, 'performClick()' is called internally.
+ * 'getSelectedItemPosition()' is then called to obtain the item to focus on.
+ * We use a toggle to have 'getSelectedItemPosition()' return the 0th index
+ * for this particular case.
+ */
+
+ private boolean mToggleFlag = true;
+
+ public FocusFirstItemSpinner(Context context, AttributeSet attrs,
+ int defStyle, int mode) {
+ super(context, attrs, defStyle, mode);
+ }
+
+ public FocusFirstItemSpinner(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public FocusFirstItemSpinner(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FocusFirstItemSpinner(Context context, int mode) {
+ super(context, mode);
+ }
+
+ public FocusFirstItemSpinner(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int getSelectedItemPosition() {
+ if (!mToggleFlag) {
+ return 0;
+ }
+ return super.getSelectedItemPosition();
+ }
+
+ @Override
+ public boolean performClick() {
+ mToggleFlag = false;
+ boolean result = super.performClick();
+ mToggleFlag = true;
+ return result;
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java
index 6a51085..63afb1e 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java
@@ -113,11 +113,6 @@ public class CertifyKeySpinner extends KeySpinner {
@Override
boolean isItemEnabled(Cursor cursor) {
- // "none" entry is always enabled!
- if (cursor.getPosition() == 0) {
- return true;
- }
-
if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) {
return false;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EmailEditText.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EmailEditText.java
index 49b3769..55d5aec 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EmailEditText.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EmailEditText.java
@@ -23,15 +23,11 @@ import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
-import android.util.Patterns;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
-import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.util.ContactHelper;
-import java.util.regex.Matcher;
-
public class EmailEditText extends AppCompatAutoCompleteTextView {
public EmailEditText(Context context) {
@@ -70,20 +66,7 @@ public class EmailEditText extends AppCompatAutoCompleteTextView {
@Override
public void afterTextChanged(Editable editable) {
- String email = editable.toString();
- if (email.length() > 0) {
- Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email);
- if (emailMatcher.matches()) {
- EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0,
- R.drawable.ic_stat_retyped_ok, 0);
- } else {
- EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0,
- R.drawable.ic_stat_retyped_bad, 0);
- }
- } else {
- // remove drawable if email is empty
- EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
- }
+
}
};
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java
index f98fda5..4074d39 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java
@@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.ui.widget;
import android.content.Context;
import android.database.Cursor;
+import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
@@ -83,6 +84,11 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView<KeyItem>
LayoutInflater l = LayoutInflater.from(getContext());
View view = l.inflate(R.layout.recipient_box_entry, null);
((TextView) view.findViewById(android.R.id.text1)).setText(keyItem.getReadableName());
+
+ if (keyItem.mIsRevoked || !keyItem.mHasEncrypt || keyItem.mIsExpired) {
+ ((TextView) view.findViewById(android.R.id.text1)).setTextColor(Color.RED);
+ }
+
return view;
}
@@ -171,4 +177,22 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView<KeyItem>
mLoaderManager.restartLoader(0, args, this);
}
+ @Override
+ public boolean enoughToFilter() {
+ return true;
+ }
+
+ public void showAllKeys(){
+ Bundle args = new Bundle();
+ args.putString(ARG_QUERY, "");
+ mLoaderManager.restartLoader(0, args, this);
+ super.showDropDown();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ // increase width to include add button
+ this.setDropDownWidth(this.getRight());
+ }
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java
index 8fb9e38..0c2d93a 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/SignKeySpinner.java
@@ -72,11 +72,6 @@ public class SignKeySpinner extends KeySpinner {
@Override
boolean isItemEnabled(Cursor cursor) {
- // "none" entry is always enabled!
- if (cursor.getPosition() == 0) {
- return true;
- }
-
if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) {
return false;
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Choice.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Choice.java
index 48f10d4..5ffce9f 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Choice.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Choice.java
@@ -18,12 +18,15 @@
package org.sufficientlysecure.keychain.util;
public class Choice <E> {
+
private String mName;
private E mId;
+ private String mDescription;
- public Choice(E id, String name) {
+ public Choice(E id, String name, String description) {
mId = id;
mName = name;
+ mDescription = description;
}
public E getId() {
@@ -34,6 +37,10 @@ public class Choice <E> {
return mName;
}
+ public String getDescription() {
+ return mDescription;
+ }
+
@Override
public String toString() {
return mName;
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
index 1067752..62dd87b 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java
@@ -20,11 +20,13 @@ package org.sufficientlysecure.keychain.util;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
@@ -223,6 +225,31 @@ public class FileHelper {
}
}
+ public static boolean isEncryptedFile(Context context, Uri uri) throws IOException {
+ boolean isEncrypted = false;
+
+ BufferedReader br = null;
+ try {
+ InputStream is = context.getContentResolver().openInputStream(uri);
+ br = new BufferedReader(new InputStreamReader(is));
+
+ String header = "-----BEGIN PGP MESSAGE-----";
+ int length = header.length();
+ char[] buffer = new char[length];
+ if (br.read(buffer, 0, length) == length) {
+ isEncrypted = new String(buffer).equals(header);
+ }
+ } finally {
+ try {
+ if (br != null)
+ br.close();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error closing file", e);
+ }
+ }
+ return isEncrypted;
+ }
+
public static String readableFileSize(long size) {
if (size <= 0) return "0";
final String[] units = new String[]{"B", "KB", "MB", "GB", "TB"};
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpClientFactory.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpClientFactory.java
new file mode 100644
index 0000000..ea2ae83
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpClientFactory.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 Michał Kępkowski
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.CertificatePinner;
+import okhttp3.OkHttpClient;
+
+public class OkHttpClientFactory {
+ private static OkHttpClient client;
+
+ public static OkHttpClient getSimpleClient() {
+ if (client == null) {
+ client = new OkHttpClient.Builder()
+ .connectTimeout(5000, TimeUnit.MILLISECONDS)
+ .readTimeout(25000, TimeUnit.MILLISECONDS)
+ .build();
+ }
+ return client;
+ }
+
+ public static OkHttpClient getSimpleClientPinned(CertificatePinner pinner) {
+ return new OkHttpClient.Builder()
+ .connectTimeout(5000, TimeUnit.MILLISECONDS)
+ .readTimeout(25000, TimeUnit.MILLISECONDS)
+ .certificatePinner(pinner)
+ .build();
+ }
+
+ public static OkHttpClient getClientPinnedIfAvailable(URL url, Proxy proxy) throws IOException,
+ TlsHelper.TlsHelperException {
+ OkHttpClient.Builder builder = new OkHttpClient.Builder();
+
+ // don't follow any redirects for keyservers, as discussed in the security audit
+ builder.followRedirects(false)
+ .followSslRedirects(false);
+
+ if (proxy != null) {
+ // set proxy and higher timeouts for Tor
+ builder.proxy(proxy);
+ builder.connectTimeout(30000, TimeUnit.MILLISECONDS)
+ .readTimeout(45000, TimeUnit.MILLISECONDS);
+ } else {
+ builder.connectTimeout(5000, TimeUnit.MILLISECONDS)
+ .readTimeout(25000, TimeUnit.MILLISECONDS);
+ }
+
+ // If a pinned cert is available, use it!
+ // NOTE: this fails gracefully back to "no pinning" if no cert is available.
+ if (url != null && TlsHelper.getPinnedSslSocketFactory(url) != null) {
+ builder.sslSocketFactory(TlsHelper.getPinnedSslSocketFactory(url));
+ }
+
+ return builder.build();
+ }
+
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpKeybaseClient.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpKeybaseClient.java
index d2c90cf..8d3eb69 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpKeybaseClient.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpKeybaseClient.java
@@ -17,55 +17,42 @@
package org.sufficientlysecure.keychain.util;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.OkUrlFactory;
+
import com.textuality.keybase.lib.KeybaseUrlConnectionClient;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+
import org.sufficientlysecure.keychain.Constants;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
-import java.net.URLConnection;
-import java.util.concurrent.TimeUnit;
/**
* Wrapper for Keybase Lib
*/
public class OkHttpKeybaseClient implements KeybaseUrlConnectionClient {
- private OkUrlFactory generateUrlFactory() {
- OkHttpClient client = new OkHttpClient();
- return new OkUrlFactory(client);
- }
-
@Override
- public URLConnection openConnection(URL url, Proxy proxy, boolean isKeybase) throws IOException {
- OkUrlFactory factory = generateUrlFactory();
- if (proxy != null) {
- factory.client().setProxy(proxy);
- factory.client().setConnectTimeout(30000, TimeUnit.MILLISECONDS);
- factory.client().setReadTimeout(40000, TimeUnit.MILLISECONDS);
- } else {
- factory.client().setConnectTimeout(5000, TimeUnit.MILLISECONDS);
- factory.client().setReadTimeout(25000, TimeUnit.MILLISECONDS);
- }
-
- factory.client().setFollowSslRedirects(false);
-
- // forced the usage of api.keybase.io pinned certificate
- if (isKeybase) {
- try {
- if (!TlsHelper.usePinnedCertificateIfAvailable(factory.client(), url)) {
- throw new IOException("no pinned certificate found for URL!");
- }
- } catch (TlsHelper.TlsHelperException e) {
- Log.e(Constants.TAG, "TlsHelper failed", e);
- throw new IOException("TlsHelper failed");
+ public Response getUrlResponse(URL url, Proxy proxy, boolean isKeybase) throws IOException {
+ OkHttpClient client = null;
+
+ try {
+ if (proxy != null) {
+ client = OkHttpClientFactory.getClientPinnedIfAvailable(url, proxy);
+ } else {
+ client = OkHttpClientFactory.getSimpleClient();
}
+ } catch (TlsHelper.TlsHelperException e) {
+ Log.e(Constants.TAG, "TlsHelper failed", e);
+ throw new IOException("TlsHelper failed");
}
- return factory.open(url);
+ Request request = new Request.Builder()
+ .url(url).build();
+ okhttp3.Response okResponse = client.newCall(request).execute();
+ return new Response(okResponse.body().byteStream(), okResponse.code(), okResponse.message(), okResponse.headers().toMultimap());
}
@Override
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
index b3d679a..5f53845 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java
@@ -19,20 +19,9 @@
package org.sufficientlysecure.keychain.util;
-import java.io.Serializable;
-import java.net.Proxy;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Set;
-import java.util.Vector;
-
+import android.accounts.Account;
import android.annotation.SuppressLint;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Parcel;
@@ -42,9 +31,23 @@ import android.support.annotation.NonNull;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Constants.Pref;
+import org.sufficientlysecure.keychain.KeychainApplication;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService;
+import java.io.Serializable;
+import java.net.Proxy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Vector;
+
/**
* Singleton Implementation of a Preference Helper
*/
@@ -76,9 +79,8 @@ public class Preferences {
/**
* Makes android's preference framework write to our file instead of default.
- * This allows us to use the "persistent" attribute to simplify code, which automatically
+ * This allows us to use the xml "persistent" attribute to simplify code, which automatically
* writes and reads preference values.
- * @param manager
*/
public static void setPreferenceManagerFileAndMode(PreferenceManager manager) {
manager.setSharedPreferencesName(PREF_FILE_NAME);
@@ -302,6 +304,23 @@ public class Preferences {
}
+ /**
+ * @return true if a periodic sync exists and is set to run automatically, false otherwise
+ */
+ public static boolean getKeyserverSyncEnabled(Context context) {
+ Account account = KeychainApplication.createAccountIfNecessary(context);
+
+ if (account == null) {
+ // if the account could not be created for some reason, we can't have a sync
+ return false;
+ }
+
+ String authority = Constants.PROVIDER_AUTHORITY;
+
+ return ContentResolver.getSyncAutomatically(account, authority) &&
+ !ContentResolver.getPeriodicSyncs(account, authority).isEmpty();
+ }
+
public CacheTTLPrefs getPassphraseCacheTtl() {
Set<String> pref = mSharedPreferences.getStringSet(Constants.Pref.PASSPHRASE_CACHE_TTLS, null);
if (pref == null) {
@@ -424,6 +443,12 @@ public class Preferences {
};
}
+ // sync preferences
+
+ public boolean getWifiOnlySync() {
+ return mSharedPreferences.getBoolean(Pref.ENABLE_WIFI_SYNC_ONLY, true);
+ }
+
// experimental prefs
public boolean getExperimentalEnableWordConfirm() {
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java
index 1492abd..fe62eff 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java
@@ -19,8 +19,6 @@ package org.sufficientlysecure.keychain.util;
import android.content.res.AssetManager;
-import com.squareup.okhttp.OkHttpClient;
-
import org.sufficientlysecure.keychain.Constants;
import java.io.ByteArrayInputStream;
@@ -39,16 +37,11 @@ import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
public class TlsHelper {
- public static class TlsHelperException extends Exception {
- public TlsHelperException(Exception e) {
- super(e);
- }
- }
-
private static Map<String, byte[]> sPinnedCertificates = new HashMap<>();
/**
@@ -80,30 +73,29 @@ public class TlsHelper {
* @throws TlsHelperException
* @throws IOException
*/
- public static boolean usePinnedCertificateIfAvailable(OkHttpClient client, URL url) throws TlsHelperException, IOException {
+ public static SSLSocketFactory getPinnedSslSocketFactory(URL url) throws TlsHelperException, IOException {
if (url.getProtocol().equals("https")) {
// use certificate PIN from assets if we have one
for (String host : sPinnedCertificates.keySet()) {
if (url.getHost().endsWith(host)) {
- pinCertificate(sPinnedCertificates.get(host), client);
- return true;
+ return pinCertificate(sPinnedCertificates.get(host));
}
}
}
- return false;
+ return null;
}
/**
- * Modifies the client to accept only requests with a given certificate. Applies to all URLs requested by the
- * client.
- * Therefore a client that is pinned this way should be used to only make requests to URLs with passed certificate.
+ * Modifies the builder to accept only requests with a given certificate.
+ * Applies to all URLs requested by the builder.
+ * Therefore a builder that is pinned this way should be used to only make requests
+ * to URLs with passed certificate.
*
* @param certificate certificate to pin
- * @param client OkHttpClient to enforce pinning on
* @throws TlsHelperException
* @throws IOException
*/
- private static void pinCertificate(byte[] certificate, OkHttpClient client)
+ private static SSLSocketFactory pinCertificate(byte[] certificate)
throws TlsHelperException, IOException {
// We don't use OkHttp's CertificatePinner since it can not be used to pin self-signed
// certificate if such certificate is not accepted by TrustManager.
@@ -130,10 +122,16 @@ public class TlsHelper {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
- client.setSslSocketFactory(context.getSocketFactory());
+ return context.getSocketFactory();
} catch (CertificateException | KeyStoreException | KeyManagementException | NoSuchAlgorithmException e) {
throw new TlsHelperException(e);
}
}
+ public static class TlsHelperException extends Exception {
+ public TlsHelperException(Exception e) {
+ super(e);
+ }
+ }
+
}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/UsbConnectionDispatcher.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/UsbConnectionDispatcher.java
new file mode 100644
index 0000000..7a8e65a
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/UsbConnectionDispatcher.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.ui.UsbEventReceiverActivity;
+
+public class UsbConnectionDispatcher {
+ private Activity mActivity;
+
+ private OnDiscoveredUsbDeviceListener mListener;
+ private UsbManager mUsbManager;
+
+ /**
+ * Receives broadcast when a supported USB device get permission.
+ */
+ private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ switch (action) {
+ case UsbEventReceiverActivity.ACTION_USB_PERMISSION: {
+ UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+ boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,
+ false);
+ if (permission) {
+ Log.d(Constants.TAG, "Got permission for " + usbDevice.getDeviceName());
+ mListener.usbDeviceDiscovered(usbDevice);
+ }
+ break;
+ }
+ }
+ }
+ };
+
+ public UsbConnectionDispatcher(final Activity activity, final OnDiscoveredUsbDeviceListener listener) {
+ this.mActivity = activity;
+ this.mListener = listener;
+ this.mUsbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE);
+ }
+
+ public void onStart() {
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(UsbEventReceiverActivity.ACTION_USB_PERMISSION);
+
+ mActivity.registerReceiver(mUsbReceiver, intentFilter);
+ }
+
+ public void onStop() {
+ mActivity.unregisterReceiver(mUsbReceiver);
+ }
+
+ /**
+ * Rescans devices and triggers {@link OnDiscoveredUsbDeviceListener}
+ */
+ public void rescanDevices() {
+ // Note: we don't check devices VID/PID because
+ // we check for permission instead.
+ // We should have permission only for matching devices
+ for (UsbDevice device : mUsbManager.getDeviceList().values()) {
+ if (mUsbManager.hasPermission(device)) {
+ if (mListener != null) {
+ mListener.usbDeviceDiscovered(device);
+ }
+ break;
+ }
+ }
+ }
+
+ public interface OnDiscoveredUsbDeviceListener {
+ void usbDeviceDiscovered(UsbDevice usbDevice);
+ }
+}
diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_change_grey_24dp.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_change_grey_24dp.png
new file mode 100644
index 0000000..f625ba4
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-hdpi/ic_change_grey_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_change_grey_24dp.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_change_grey_24dp.png
new file mode 100644
index 0000000..6cf9d04
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-mdpi/ic_change_grey_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_change_grey_24dp.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_change_grey_24dp.png
new file mode 100644
index 0000000..1c30b6f
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-xhdpi/ic_change_grey_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_change_grey_24dp.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_change_grey_24dp.png
new file mode 100644
index 0000000..672a9c9
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_change_grey_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_change_grey_24dp.png b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_change_grey_24dp.png
new file mode 100644
index 0000000..5be1010
--- /dev/null
+++ b/OpenKeychain/src/main/res/drawable-xxxhdpi/ic_change_grey_24dp.png
Binary files differ
diff --git a/OpenKeychain/src/main/res/layout-land/backup_code_fragment.xml b/OpenKeychain/src/main/res/layout-land/backup_code_fragment.xml
deleted file mode 100644
index 0a4f97d..0000000
--- a/OpenKeychain/src/main/res/layout-land/backup_code_fragment.xml
+++ /dev/null
@@ -1,199 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:custom="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:paddingTop="20dp">
-
- <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
- android:id="@+id/title_animator"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:inAnimation="@anim/fade_in"
- android:outAnimation="@anim/fade_out"
- custom:initialView="0">
-
- <TextView
- style="?android:textAppearanceMedium"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:gravity="center_horizontal"
- android:padding="10dp"
- android:text="@string/backup_code_explanation" />
-
- <TextView
- style="?android:textAppearanceMedium"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:gravity="center_horizontal"
- android:padding="10dp"
- android:text="@string/backup_code_enter" />
-
- <TextView
- style="?android:textAppearanceMedium"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:gravity="center_horizontal"
- android:padding="10dp"
- android:text="@string/backup_code_ok" />
-
- </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
-
- <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
- android:id="@+id/code_animator"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_marginBottom="15dp"
- android:layout_marginTop="15dp"
- android:inAnimation="@anim/fade_in"
- android:outAnimation="@anim/fade_out"
- custom:initialView="1">
-
- <TextView
- android:id="@+id/backup_code_display"
- style="@android:style/Widget.EditText"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_marginLeft="16dp"
- android:layout_marginRight="16dp"
- android:clickable="false"
- android:focusable="false"
- android:singleLine="true"
- android:textSize="18dp"
- android:textStyle="bold"
- android:typeface="monospace"
- tools:ignore="SpUsage"
- tools:text="AAAA-AAAA-AAAA-AAAA-AAAA-AAAA" />
-
- <com.github.pinball83.maskededittext.MaskedEditText
- android:id="@+id/backup_code_input"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_marginLeft="16dp"
- android:layout_marginRight="16dp"
- android:textSize="18dp"
- android:textStyle="bold"
- android:typeface="monospace"
- app:deleteChar="\u00a0"
- app:mask="****-****-****-****-****-****"
- app:maskIconColor="@color/colorPrimary"
- app:notMaskedSymbol="*"
- app:replacementChar="\u00a0"
- tools:ignore="SpUsage" />
-
- </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
-
- <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
- android:id="@+id/status_animator"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:inAnimation="@anim/fade_in_delayed"
- android:outAnimation="@anim/fade_out"
- custom:initialView="2">
-
- <Button
- android:id="@+id/button_backup_input"
- style="?android:buttonBarButtonStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_margin="10dp"
- android:drawableLeft="@drawable/ic_mode_edit_grey_24dp"
- android:drawablePadding="8dp"
- android:padding="12dp"
- android:text="@string/btn_code_wrotedown" />
-
- <Space
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:orientation="horizontal">
-
- <TextView
- style="?android:textAppearanceMedium"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:text="@string/backup_code_wrong" />
-
- <Button
- android:id="@+id/button_backup_back"
- style="?android:buttonBarButtonStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_margin="10dp"
- android:drawableLeft="@drawable/ic_repeat_grey_24dp"
- android:drawablePadding="8dp"
- android:padding="12dp"
- android:text="@string/btn_backup_back" />
-
- </LinearLayout>
-
- <LinearLayout
- style="?android:buttonBarStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:orientation="vertical">
-
- <LinearLayout
- style="?android:buttonBarStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal">
-
- <Button
- android:id="@+id/button_backup_share"
- style="?android:buttonBarButtonStyle"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_margin="10dp"
- android:layout_weight="1"
- android:drawableLeft="@drawable/ic_share_grey_24dp"
- android:drawablePadding="8dp"
- android:padding="12dp"
- android:text="@string/btn_backup_share" />
-
- <Button
- android:id="@+id/button_backup_save"
- style="?android:buttonBarButtonStyle"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_margin="10dp"
- android:layout_weight="1"
- android:drawableLeft="@drawable/ic_save_grey_24dp"
- android:drawablePadding="8dp"
- android:padding="12dp"
- android:text="@string/btn_backup_save" />
-
-
- </LinearLayout>
-
- <Button
- android:id="@+id/button_faq"
- style="?android:buttonBarButtonStyle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:text="@string/how_to_import" />
- </LinearLayout>
-
- </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
-
-</LinearLayout>
diff --git a/OpenKeychain/src/main/res/layout-w400dp/backup_code_fragment.xml b/OpenKeychain/src/main/res/layout-w400dp/backup_code_fragment.xml
new file mode 100644
index 0000000..407fa2d
--- /dev/null
+++ b/OpenKeychain/src/main/res/layout-w400dp/backup_code_fragment.xml
@@ -0,0 +1,459 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingTop="20dp">
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:id="@+id/title_animator"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ custom:initialView="0">
+
+ <TextView
+ style="?android:textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:padding="10dp"
+ android:text="@string/backup_code_explanation" />
+
+ <TextView
+ style="?android:textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:padding="10dp"
+ android:text="@string/backup_code_enter" />
+
+ <TextView
+ style="?android:textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal"
+ android:padding="10dp"
+ android:text="@string/backup_code_ok" />
+
+ </org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
+
+ <org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
+ android:id="@+id/code_animator"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="15dp"
+ android:layout_marginTop="15dp"
+ android:inAnimation="@anim/fade_in"
+ android:outAnimation="@anim/fade_out"
+ custom:initialView="1">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal">
+
+ <TextView
+ android:id="@+id/backup_code_display_1"
+ style="@android:style/Widget.EditText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:clickable="false"
+ android:focusable="false"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="SpUsage"
+ tools:text="ABCD" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:text="-"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:id="@+id/backup_code_display_2"
+ style="@android:style/Widget.EditText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:clickable="false"
+ android:focusable="false"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="SpUsage"
+ tools:text="EFGH" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:text="-"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:id="@+id/backup_code_display_3"
+ style="@android:style/Widget.EditText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:clickable="false"
+ android:focusable="false"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="SpUsage"
+ tools:text="IJKL" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:text="-"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:id="@+id/backup_code_display_4"
+ style="@android:style/Widget.EditText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:clickable="false"
+ android:focusable="false"
+ android:singleLine="true"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="SpUsage"
+ tools:text="MNOP" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:text="-"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:id="@+id/backup_code_display_5"
+ style="@android:style/Widget.EditText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:clickable="false"
+ android:focusable="false"
+ android:singleLine="true"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="SpUsage"
+ tools:text="QRST" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:text="-"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:id="@+id/backup_code_display_6"
+ style="@android:style/Widget.EditText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:clickable="false"
+ android:focusable="false"
+ android:singleLine="true"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="SpUsage"
+ tools:text="UVWX" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal">
+
+ <!--
+ The most reliable way to correctly size these I found was to put a transparent hint on them.
+ Theoretically, this should be what the android:ems attribute is for - didn't work for me.
+ -->
+ <EditText
+ android:id="@+id/backup_code_1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:hint="ABCD"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:maxLength="4"
+ android:singleLine="true"
+ android:textColorHint="@android:color/transparent"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:text="-"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <EditText
+ android:id="@+id/backup_code_2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:hint="ABCD"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:maxLength="4"
+ android:singleLine="true"
+ android:textColorHint="@android:color/transparent"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:text="-"
+ android:textSize="18dp"
+ android:textStyle="bold"
+ android:typeface="monospace"
+ tools:ignore="HardcodedText,SpUsage" />
+
+ <EditText
+ android:id="@+id/backup_code_3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:hint="ABCD"
+ android:inputType="textNoSuggestions|textCapCharacters"
+ android:maxLength="4"
+ android:singleLine="true"
+ android:textColorHint="@android:color/transparent"
+ android:textSize="18dp"<