Merge branch 'master' into backup-api

Conflicts:
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
	extern/openpgp-api-lib
This commit is contained in:
Dominik Schürmann
2016-05-07 12:01:16 +03:00
272 changed files with 12293 additions and 4368 deletions
+6 -15
View File
@@ -1,37 +1,28 @@
#Android specific # Android specific
bin
gen
obj
libs/armeabi
lint.xml lint.xml
local.properties local.properties
release.properties release.properties
ant.properties
*.class *.class
*.apk *.apk
#Gradle # Gradle
.gradle .gradle
build 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 target
pom.xml.* pom.xml.*
#Eclipse # Eclipse
.project .project
.classpath .classpath
.settings .settings
.metadata .metadata
#IntelliJ IDEA # IntelliJ IDEA
.idea .idea
*.iml *.iml
#OS Specific # OS Specific
[Tt]humbs.db [Tt]humbs.db
.DS_Store .DS_Store
+2
View File
@@ -14,6 +14,8 @@ addons:
hostname: short-hostname hostname: short-hostname
android: android:
components: components:
- tools
- build-tools-23.0.2
- build-tools-23.0.1 - build-tools-23.0.1
- android-23 - android-23
- android-22 - android-22
+1 -1
View File
@@ -1,6 +1,6 @@
[main] [main]
host = https://www.transifex.com 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] [open-keychain.strings]
file_filter = OpenKeychain/src/main/res/values-<lang>/strings.xml file_filter = OpenKeychain/src/main/res/values-<lang>/strings.xml
+98 -99
View File
@@ -8,11 +8,53 @@ dependencies {
// NOTE: libraries are pinned to a specific build, see below // NOTE: libraries are pinned to a specific build, see below
// from local Android SDK // from local Android SDK
compile 'com.android.support:support-v4:23.2.0' compile 'com.android.support:support-v4:23.3.0'
compile 'com.android.support:appcompat-v7:23.2.0' compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support:design:23.2.0' compile 'com.android.support:design:23.3.0'
compile 'com.android.support:recyclerview-v7:23.2.0' compile 'com.android.support:recyclerview-v7:23.3.0'
compile 'com.android.support:cardview-v7:23.2.0' compile 'com.android.support:cardview-v7:23.3.0'
compile 'com.android.support:support-annotations:23.3.0'
// JCenter etc.
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.7@aar'
compile 'se.emilsjolander:stickylistheaders:2.7.0'
compile 'org.sufficientlysecure:html-textview:1.5'
compile 'org.sufficientlysecure:donations:2.4'
compile 'com.nispok:snackbar:2.11.0'
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: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(':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(':KeybaseLib')
compile project(':safeslinger-exchange')
// Unit tests in the local JVM with Robolectric // Unit tests in the local JVM with Robolectric
// https://developer.android.com/training/testing/unit-testing/local-unit-tests.html // https://developer.android.com/training/testing/unit-testing/local-unit-tests.html
@@ -25,111 +67,68 @@ dependencies {
testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'org.mockito:mockito-core:1.10.19'
// UI testing with Espresso // UI testing with Espresso
androidTestCompile 'com.android.support.test:runner:0.4.1' // Force usage of support libs in the test app, since they are internally used by the runner module.
androidTestCompile 'com.android.support.test:rules:0.4.1' // https://github.com/googlesamples/android-testing/blob/master/ui/espresso/BasicSample/app/build.gradle#L28
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' androidTestCompile 'com.android.support:support-annotations:23.3.0'
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1' androidTestCompile 'com.android.support:appcompat-v7:23.3.0'
androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.2.1') { 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: 'appcompat'
exclude group: 'com.android.support', module: 'support-v4' exclude group: 'com.android.support', module: 'support-v4'
exclude module: 'recyclerview-v7' 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'
}
// JCenter etc.
compile 'com.eftimoff:android-patternview:1.0.5@aar'
compile 'com.journeyapps:zxing-android-embedded:3.1.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 'se.emilsjolander:stickylistheaders:2.7.0'
compile 'org.sufficientlysecure:html-textview:1.3'
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 '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'
// 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(':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')
} }
// Output of ./gradlew -q calculateChecksums // Output of ./gradlew -q calculateChecksums
// Comment out the libs referenced as git submodules! // Comment out the libs referenced as git submodules!
dependencyVerification { dependencyVerification {
verify = [ verify = [
'com.android.support:support-v4:992666398b80724a2f95ea3d7bf7c3d94fb810dcba7ae1aa6e38c0d6120d2603', 'com.android.support:support-v4:1e8b7cc1cb3d6f6a2fd913791a6313df6bbaa470be450384474e906bc234bd49',
'com.android.support:appcompat-v7:14ab04eb2e3f302a082b79c308f6283d21909d1feb831a4e117cdacdad70adb7', 'com.android.support:appcompat-v7:dce81c41f76d83fa315617f4bc8ef2f84c5aa54b686f37b559422b939f622490',
'com.android.support:design:e7f2a383ba675cf65d834f27079630a1e373f2f3a08330bec6a7c798ef50b3b2', 'com.android.support:design:1023c9d1ca3ecae3e0c54b7678cd275032fe5720bbb4f58e3c8b2f2a595d0051',
'com.android.support:recyclerview-v7:81aad9ff4104a20d8d5cda49dbbe0f2925e7dcd607a689374db843c6f2eca20a', 'com.android.support:recyclerview-v7:32b98ca177d9352b19a92af80ff9a3c3589b50232b6722d2bce5514abe90be51',
'com.android.support:cardview-v7:b2c2c070a78fbf7683ab4d84b23f9eecf2af3848f126775d048ae62238b55aed', 'com.android.support:cardview-v7:2f592da4dd1db85dab99653474dd139135aa3ca7596dfdbcfef714e0339b603a',
'com.eftimoff:android-patternview:594dde382fb9a445ef0c92d614f6f127727ce699f124de8167929e10f298bf8b', 'com.android.support:support-annotations:e9e076f3ea4fb144387c6054a6f69a2f6150ad4b1907897aaf55d6e8f4b8b91e',
'com.journeyapps:zxing-android-embedded:90840a4457e68962fdfb74f691c6a736be7596291001045241901f1f0e6db2ac', 'com.journeyapps:zxing-android-embedded:afe4cd51d95ba0fd3a4bfe08c5a160bd32602aa174d511600ac824b6de4c79f1',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259', 'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'com.jpardogo.materialtabstrip:library:24d19232b319f8c73e25793432357919a7ed972186f57a3b2c9093ea74ad8311', 'com.jpardogo.materialtabstrip:library:24d19232b319f8c73e25793432357919a7ed972186f57a3b2c9093ea74ad8311',
'com.getbase:floatingactionbutton:3edefa511aac4d90794c7b0496aca59cff2eee1e32679247b4f85acbeee05240', 'com.getbase:floatingactionbutton:3edefa511aac4d90794c7b0496aca59cff2eee1e32679247b4f85acbeee05240',
'org.commonjava.googlecode.markdown4j:markdown4j:e952e825d29e1317d96f79f346bfb6786c7c5eef50bd26e54a80823704b62e13', 'org.commonjava.googlecode.markdown4j:markdown4j:e952e825d29e1317d96f79f346bfb6786c7c5eef50bd26e54a80823704b62e13',
'org.ocpsoft.prettytime:prettytime:ef7098d973ae78b57d1a22dc37d3b8a771bf030301300e24055d676b6cdc5e75', 'org.ocpsoft.prettytime:prettytime:ef7098d973ae78b57d1a22dc37d3b8a771bf030301300e24055d676b6cdc5e75',
'com.splitwise:tokenautocomplete:2fc238424130b42155b5f2e39799a90bbbd13b148850afbe534ab08bb913c7f7', 'com.splitwise:tokenautocomplete:f56239588390f103b270b7c12361d99b06313a5a0410dc7f66e241ac4baf9baa',
'com.github.pinball83:masked-edittext:b1913d86482c7066ebb7831696773ac131865dc441cf8a3fc41d3b7d5691724e',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb', 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
'org.sufficientlysecure:html-textview:b3799eea5f440b32bdec18360e4ce66f6f77814c8df9d382718506a7da0de961',
'org.sufficientlysecure:donations:96f8197bab26dfe41900d824f10f8f1914519cd62eedb77bdac5b223eccdf0a6', 'org.sufficientlysecure:donations:96f8197bab26dfe41900d824f10f8f1914519cd62eedb77bdac5b223eccdf0a6',
'org.sufficientlysecure:html-textview:39048e35894e582adada388e6c00631803283f8defed8e07ad58a5f284f272ee',
'com.squareup.okhttp:okhttp:8df336e3e93b22ba8c05da5d94caf968950db845869c7ca16ed682e065135aa8',
'com.nispok:snackbar:46b5eb9d630d329e13c2ce00ee9fb115ffb66c23c72cff32ee97eedd76824c6f', 'com.nispok:snackbar:46b5eb9d630d329e13c2ce00ee9fb115ffb66c23c72cff32ee97eedd76824c6f',
'com.squareup.okhttp3:okhttp:a41cdb7b024c56436a21e38f00b4d12e3b7e01451ffe6c4f545acba805bba03b',
'com.squareup.okhttp3:okhttp-urlconnection:7d6598a6665c166e2d4b78956a96056b9be7de192b3c923ccf4695d08e580833',
'org.apache.james:apache-mime4j-core:4d7434c68f94b81a253c12f28e6bbb4d6239c361d6086a46e22e594bb43ac660', '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', 'org.apache.james:apache-mime4j-dom:7e6b06ee164a1c21b7e477249ea0b74a18fddce44764e5764085f58dd8c34633',
'com.mikepenz:materialdrawer:4e2644f454cc2ce48b956536d3339957c3f592adb2e0b6dad72d477da29f7677', 'org.thoughtcrime.ssl.pinning:AndroidPinning:afa1d74e699257fa75cb109ff29bac50726ef269c6e306bdeffe8223cee06ef4',
'com.cocosw:bottomsheet:4af6112a7f4cad4e2b70e5fdf1edc39f51275523a0f53011a012837dc103e597', 'com.cocosw:bottomsheet:4af6112a7f4cad4e2b70e5fdf1edc39f51275523a0f53011a012837dc103e597',
'com.mikepenz:iconics-core:d2495547db9d881168b1b502b1934f6a000ed5086c6c6a7114f3bbcbbb7ec306', 'com.mikepenz:materialdrawer:4169462fdde042e2bb53a7c2b4e2334d569d16b2020781ee05741b50e1a2967d',
'com.mikepenz:materialize:2457dbe0b874a422c0a21bc6716cf5af1d5a8d39387857ff7c20855ab5543bf8', 'com.mikepenz:fastadapter:1bfc00216d71dfdfe0d8e7a9d92bb97bfaa1794543930e34b1f79d5d7adbddf6',
'com.mikepenz:fontawesome-typeface:69cb09934a83bac607e78a29459868d537f766224b4a65a042d1f84c98c7b05d', 'com.mikepenz:materialize:575195b2fa5b2414fb14a59470ee21d8a8cd8355b651e0cf52e477e3ff1cd96c',
'com.mikepenz:google-material-typeface:48b2712de87d542e9b050846e9f602238a367f38e2d5e8ea4557c5b12adfcbec', '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.fidesmo:nordpol-android:56f43fe2b1676817bcb4085926de14a08282ef6729c855c198d81aec62b20d65',
'com.mikepenz:community-material-typeface:990acfcfb892a733d36748fe29176bd61dd5ab34bc8ca1c591200e639d955b99', // 'OpenKeychain:openpgp-api-lib:e4f456b77f80886eb2094bd643c8c8c9cf0eb3e8ef919706d7e92da6fc3c5517',
// 'OpenKeychain.extern.bouncycastle:core:b22dfb37e09fb520683dd0ba089351787560a75b59b60822143f633ec984cab5', // 'OpenKeychain:openkeychain-api-lib:cddb2953fc3ec2876923f01acbe91e136eaad1c327450a4fab21274b0edf73d9',
// 'OpenKeychain.extern.openpgp-api-lib:openpgp-api:fbd9a53022747bdc3c5b0926c6f9cbcbee19e766dd96f090a4310dc65026b393', // 'OpenKeychain.extern.bouncycastle:core:d4574e14e78d78b8b0b30b37089f82901a8fb46679915e034fe5d15cc5431c6a',
// 'OpenKeychain.extern.openkeychain-api-lib:openkeychain-intents:9263330c00497b7bb70502160f50c8396228129376f48f4f5656d28360a2edac', // 'OpenKeychain.extern.bouncycastle:pg:064250718891c89d3bfa1e9bdef68bbf1cd2adb83532707754390b42643f2156',
// 'OpenKeychain.extern.bouncycastle:prov:2d93a52e1b519995b18c0a92a1e59a2773d67d9b466a9cce6af5202a66502577', // 'OpenKeychain.extern.bouncycastle:prov:6933802a03ffd53c084415550db834a59540327434a061af39442e00002611f8',
// 'OpenKeychain.extern.bouncycastle:pg:1397025acf36be36d329c0345b136af776be82fe5d6dad70cc06db09d2f02201', // 'OpenKeychain.extern:minidns:357901cf1b93f74f9729f6e3a1dfa5f148d627bf23423c8597ab82be84b3dc9d',
// 'OpenKeychain.extern.safeslinger-exchange:safeslinger-exchange:989fcc0eba663489a41aa166f6bb39f21271d980faddea5f06ab75339e792d10', // 'OpenKeychain:KeybaseLib:cbba456bf50083cde99f9c460fcbd812dbcd7d9e1a5c1df0ee9e080a85802aa8',
'com.android.support:support-annotations:7f21659b084da073b77b6f7fe7ab250c4f23346238d4efdbbbb937e017ae4693', // 'OpenKeychain:safeslinger-exchange:422efcad6868e32ce1446d18f08d728d266a30725fdc09a71f1b78c133bfc051',
// 'OpenKeychain.extern:minidns:109d5851ab351d7628ed62a0ed96b40598952424e56657c17debbeb4a704f0ce', 'com.android.support:support-vector-drawable:a4feae56880e385e147dfed3f73593ce61164ec49bb401f34de9a77a17c68a44',
// 'OpenKeychain.extern.KeybaseLib:Lib:c5b1567ff781c311240e83f865c4ba76ae435eb00994529b8364371abf0d76de', 'com.android.support:animated-vector-drawable:ddf1f9dc18d38b21b01c21842d4bdf57613ea86398ede8f914f29b5770f4c32b',
'com.android.support:animated-vector-drawable:4d8366192dedc8ca9e6ff62e9be9ba6145166554d61befc9df312e8328836f55',
'com.android.support:support-vector-drawable:0f43fc47b0d2797c4e1851aba61ab87a4fd33323348c2bd022821aaac052a266',
'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266', 'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266',
'com.fidesmo:nordpol-core:3de58e850a00bba5b4d3a604d1399bcd89f695ea191ec0b03a57222e18062d15', 'com.fidesmo:nordpol-core:3de58e850a00bba5b4d3a604d1399bcd89f695ea191ec0b03a57222e18062d15',
] ]
@@ -141,14 +140,12 @@ android {
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion buildToolsVersion rootProject.ext.buildToolsVersion
// TODO: remove org.apache dependencies in LinkedTokenResource etc.
useLibrary 'org.apache.http.legacy'
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 23 targetSdkVersion 23
versionCode 38200 versionCode 39600
versionName "3.8.2" versionName "3.9.6 beta"
applicationId "org.sufficientlysecure.keychain" applicationId "org.sufficientlysecure.keychain"
// the androidjunitrunner is broken regarding coverage, see here: // the androidjunitrunner is broken regarding coverage, see here:
// https://code.google.com/p/android/issues/detail?id=170607 // https://code.google.com/p/android/issues/detail?id=170607
@@ -190,6 +187,12 @@ android {
} }
debug { debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// Enable code coverage (Jacoco)
testCoverageEnabled true
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
// Reference them in the java files with e.g. BuildConfig.ACCOUNT_TYPE. // Reference them in the java files with e.g. BuildConfig.ACCOUNT_TYPE.
@@ -203,9 +206,6 @@ android {
// Github API // Github API
buildConfigField "String", "GITHUB_CLIENT_ID", "\"c942cd81844d94e7e41b\"" buildConfigField "String", "GITHUB_CLIENT_ID", "\"c942cd81844d94e7e41b\""
buildConfigField "String", "GITHUB_CLIENT_SECRET", "\"f1dd17e70a0614abbd9310b00a310e23c6c8edff\"" buildConfigField "String", "GITHUB_CLIENT_SECRET", "\"f1dd17e70a0614abbd9310b00a310e23c6c8edff\""
// Enable code coverage (Jacoco)
testCoverageEnabled true
} }
} }
@@ -267,10 +267,11 @@ android {
dexOptions { dexOptions {
incremental = true incremental = true
// Disable preDexing, causes com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000) on some systems preDexLibraries = true
preDexLibraries = false // dexInProcess requires much RAM, which is not available on all dev systems
dexInProcess = false
jumboMode = true jumboMode = true
javaMaxHeapSize "2g" javaMaxHeapSize "4g"
} }
packagingOptions { packagingOptions {
@@ -284,8 +285,6 @@ android {
} }
} }
// apply plugin: 'spoon'
task jacocoTestReport(type:JacocoReport, dependsOn: "testFdroidDebugUnitTest") { task jacocoTestReport(type:JacocoReport, dependsOn: "testFdroidDebugUnitTest") {
group = "Reporting" group = "Reporting"
description = "Generate Jacoco coverage reports" description = "Generate Jacoco coverage reports"
+17 -24
View File
@@ -1,28 +1,21 @@
# Add project specific ProGuard rules here. # Documentation for ProGuard:
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html
# http://proguard.sourceforge.net/
# Add any project specific keep options here: #-dontshrink # shrinking enabled, see below
#-dontobfuscate # obfuscation enabled for one class (see below)
# 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
-dontoptimize -dontoptimize
-dontshrink -dontpreverify
-keepattributes **
-dontwarn ** -dontwarn **
-dontnote ** -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.**,** {*;}
+64 -21
View File
@@ -5,11 +5,6 @@
android:installLocation="auto"> 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 Association of file types to Keychain
===================================== =====================================
General remarks about file ending conventions: General remarks about file ending conventions:
@@ -48,6 +43,11 @@
android:name="android.hardware.screen.portrait" android:name="android.hardware.screen.portrait"
android:required="false" /> 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 --> <!-- TemporaryStorageProvider should be writable by OpenKeychain only, thus signature-level permission -->
<permission <permission
android:name="${applicationId}.WRITE_TEMPORARY_STORAGE" android:name="${applicationId}.WRITE_TEMPORARY_STORAGE"
@@ -81,6 +81,7 @@
<!-- other group (for free) --> <!-- other group (for free) -->
<uses-permission android:name="android.permission.INTERNET" /> <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.NFC" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
@@ -94,6 +95,15 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Keychain.Light"> 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 --> <!-- singleTop for NFC dispatch, see SecurityTokenOperationActivity -->
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
@@ -503,21 +513,6 @@
android:theme="@style/Theme.Keychain.Transparent" android:theme="@style/Theme.Keychain.Transparent"
android:windowSoftInputMode="stateHidden"> 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 --> <!-- IMPORT_KEY without mimeType to allow import with extras Bundle -->
<intent-filter android:label="@string/intent_import_key"> <intent-filter android:label="@string/intent_import_key">
<action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_QR_CODE" /> <action android:name="org.sufficientlysecure.keychain.action.IMPORT_KEY_FROM_QR_CODE" />
@@ -730,10 +725,35 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.MainActivity" /> android:value=".ui.MainActivity" />
</activity> </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 <activity
android:name=".ui.LogDisplayActivity" android:name=".ui.LogDisplayActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
@@ -806,7 +826,12 @@
android:exported="false" android:exported="false"
android:label="@string/keyserver_sync_settings_title" /> 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 <activity
android:name=".remote.ui.RemoteCreateAccountActivity" android:name=".remote.ui.RemoteCreateAccountActivity"
android:exported="false" android:exported="false"
@@ -864,6 +889,24 @@
android:configChanges="keyboardHidden|keyboard" android:configChanges="keyboardHidden|keyboard"
android:label="@string/title_backup" /> 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, <!-- DEPRECATED service,
using this service may lead to truncated data being returned to the caller --> using this service may lead to truncated data being returned to the caller -->
<service <service
@@ -6,15 +6,16 @@
package org.bouncycastle.openpgp.operator.jcajce; package org.bouncycastle.openpgp.operator.jcajce;
import java.nio.ByteBuffer;
import java.util.Map;
import org.bouncycastle.jcajce.util.NamedJcaJceHelper; import org.bouncycastle.jcajce.util.NamedJcaJceHelper;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.operator.PGPDataDecryptor; import org.bouncycastle.openpgp.operator.PGPDataDecryptor;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import java.nio.ByteBuffer;
import java.util.Map;
public class CachingDataDecryptorFactory implements PublicKeyDataDecryptorFactory public class CachingDataDecryptorFactory implements PublicKeyDataDecryptorFactory
{ {
private final PublicKeyDataDecryptorFactory mWrappedDecryptor; private final PublicKeyDataDecryptorFactory mWrappedDecryptor;
@@ -59,6 +60,10 @@ public class CachingDataDecryptorFactory implements PublicKeyDataDecryptorFactor
return mSessionKeyCache.get(bi); 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); byte[] sessionData = mWrappedDecryptor.recoverSessionData(keyAlgorithm, secKeyData);
mSessionKeyCache.put(bi, sessionData); mSessionKeyCache.put(bi, sessionData);
return sessionData; return sessionData;
@@ -118,6 +118,7 @@ public final class Constants {
// keyserver sync settings // keyserver sync settings
public static final String SYNC_CONTACTS = "syncContacts"; public static final String SYNC_CONTACTS = "syncContacts";
public static final String SYNC_KEYSERVER = "syncKeyserver"; public static final String SYNC_KEYSERVER = "syncKeyserver";
public static final String ENABLE_WIFI_SYNC_ONLY = "enableWifiSyncOnly";
// other settings // other settings
public static final String EXPERIMENTAL_ENABLE_WORD_CONFIRM = "experimentalEnableWordConfirm"; public static final String EXPERIMENTAL_ENABLE_WORD_CONFIRM = "experimentalEnableWordConfirm";
public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities"; public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities";
@@ -100,6 +100,11 @@ public class KeychainApplication extends Application {
// Add OpenKeychain account to Android to link contacts with keys and keyserver sync // Add OpenKeychain account to Android to link contacts with keys and keyserver sync
createAccountIfNecessary(this); 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 first time, enable keyserver and contact sync
if (Preferences.getPreferences(this).isFirstTime()) { if (Preferences.getPreferences(this).isFirstTime()) {
KeyserverSyncAdapterService.enableKeyserverSync(this); KeyserverSyncAdapterService.enableKeyserverSync(this);
@@ -21,11 +21,12 @@ package org.sufficientlysecure.keychain.keyimport;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull; 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.Constants;
import org.sufficientlysecure.keychain.pgp.PgpHelper; import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; 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.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
import org.sufficientlysecure.keychain.util.TlsHelper;
import java.io.IOException; import java.io.IOException;
import java.net.Proxy; import java.net.Proxy;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -104,11 +106,10 @@ public class FacebookKeyserver extends Keyserver {
String request = String.format(FB_KEY_URL_FORMAT, fbUsername); String request = String.format(FB_KEY_URL_FORMAT, fbUsername);
Log.d(Constants.TAG, "fetching from Facebook with: " + request + " proxy: " + mProxy); Log.d(Constants.TAG, "fetching from Facebook with: " + request + " proxy: " + mProxy);
OkHttpClient client = new OkHttpClient();
client.setProxy(mProxy);
URL url = new URL(request); URL url = new URL(request);
OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(url, mProxy);
Response response = client.newCall(new Request.Builder().url(url).build()).execute(); Response response = client.newCall(new Request.Builder().url(url).build()).execute();
// contains body both in case of success or failure // 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. " throw new QueryFailedException("Cannot connect to Facebook. "
+ "Check your Internet connection!" + "Check your Internet connection!"
+ (mProxy == Proxy.NO_PROXY ? "" : " Using proxy " + mProxy)); + (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); 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(); String host = uri.getHost();
return host.equalsIgnoreCase(FB_HOST) || host.equalsIgnoreCase(FB_HOST_WWW); return FB_HOST.equalsIgnoreCase(host) || FB_HOST_WWW.equalsIgnoreCase(host);
} }
} }
@@ -18,16 +18,18 @@
package org.sufficientlysecure.keychain.keyimport; 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.Constants;
import org.sufficientlysecure.keychain.pgp.PgpHelper; import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
import org.sufficientlysecure.keychain.util.TlsHelper; import org.sufficientlysecure.keychain.util.TlsHelper;
import java.io.IOException; import java.io.IOException;
@@ -42,7 +44,6 @@ import java.util.Comparator;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -199,43 +200,12 @@ public class HkpKeyserver extends Keyserver {
return mSecure ? "https://" : "http://"; 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 { private String query(String request, @NonNull Proxy proxy) throws QueryFailedException, HttpError {
try { try {
URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + request); URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + request);
Log.d(Constants.TAG, "hkp keyserver query: " + url + " Proxy: " + proxy); 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(); 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 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); Log.e(Constants.TAG, "IOException at HkpKeyserver", e);
throw new QueryFailedException("Keyserver '" + mHost + "' is unavailable. Check your Internet connection!" + throw new QueryFailedException("Keyserver '" + mHost + "' is unavailable. Check your Internet connection!" +
(proxy == Proxy.NO_PROXY ? "" : " Using proxy " + proxy)); (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, "hkp keyserver add: " + url);
Log.d(Constants.TAG, "params: " + params); Log.d(Constants.TAG, "params: " + params);
RequestBody body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), params); RequestBody body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), params);
Request request = new Request.Builder() Request request = new Request.Builder()
@@ -422,7 +396,7 @@ public class HkpKeyserver extends Keyserver {
.post(body) .post(body)
.build(); .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, "response code: " + response.code());
Log.d(Constants.TAG, "answer: " + response.body().string()); Log.d(Constants.TAG, "answer: " + response.body().string());
@@ -434,6 +408,9 @@ public class HkpKeyserver extends Keyserver {
} catch (IOException e) { } catch (IOException e) {
Log.e(Constants.TAG, "IOException", e); Log.e(Constants.TAG, "IOException", e);
throw new AddKeyException(); throw new AddKeyException();
} catch (TlsHelper.TlsHelperException e) {
Log.e(Constants.TAG, "Exception in pinning certs", e);
throw new AddKeyException();
} }
} }
@@ -2,12 +2,10 @@ package org.sufficientlysecure.keychain.linked;
import android.content.Context; import android.content.Context;
import org.apache.http.HttpEntity; import okhttp3.CertificatePinner;
import org.apache.http.HttpResponse; import okhttp3.OkHttpClient;
import org.apache.http.client.HttpClient; import okhttp3.Request;
import org.apache.http.client.methods.HttpRequestBase; import okhttp3.Response;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.json.JSONException; import org.json.JSONException;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource; 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.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log; 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.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.util.HashMap; 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) private static CertificatePinner getCertificatePinner(String hostname, String[] pins){
throws IOException, HttpStatusException { CertificatePinner.Builder builder = new CertificatePinner.Builder();
return getResponseBody(context, request, null); for(String pin : pins){
builder.add(hostname,pin);
}
return builder.build();
} }
@SuppressWarnings("deprecation") // HttpRequestBase is deprecated
public static String getResponseBody(Context context, HttpRequestBase request, String[] pins)
throws IOException, HttpStatusException {
StringBuilder sb = new StringBuilder();
request.setHeader("User-Agent", "Open Keychain"); public static String getResponseBody(Request request, String... pins)
throws IOException, HttpStatusException {
Log.d("Connection to: " + request.url().url().getHost(), "");
HttpClient httpClient; OkHttpClient client;
if (pins == null) { if (pins != null) {
httpClient = new DefaultHttpClient(new BasicHttpParams()); client = OkHttpClientFactory.getSimpleClientPinned(getCertificatePinner(request.url().url().getHost(), pins));
} else { } else {
httpClient = PinningHelper.getPinnedHttpClient(context, pins); client = OkHttpClientFactory.getSimpleClient();
} }
HttpResponse response = httpClient.execute(request); Response response = client.newCall(request).execute();
int statusCode = response.getStatusLine().getStatusCode();
String reason = response.getStatusLine().getReasonPhrase();
int statusCode = response.code();
String reason = response.message();
if (statusCode != 200) { if (statusCode != 200) {
throw new HttpStatusException(statusCode, reason); throw new HttpStatusException(statusCode, reason);
} }
HttpEntity entity = response.getEntity(); return response.body().string();
InputStream inputStream = entity.getContent();
BufferedReader bReader = new BufferedReader(
new InputStreamReader(inputStream, "UTF-8"), 8);
String line;
while ((line = bReader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} }
public static class HttpStatusException extends Throwable { public static class HttpStatusException extends Throwable {
@@ -6,7 +6,7 @@ import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import org.apache.http.client.methods.HttpGet; import okhttp3.Request;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
@@ -32,14 +32,16 @@ public class GenericHttpsResource extends LinkedTokenResource {
token, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24)); token, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24));
} }
@SuppressWarnings("deprecation") // HttpGet is deprecated
@Override @Override
protected String fetchResource (Context context, OperationLog log, int indent) protected String fetchResource (Context context, OperationLog log, int indent)
throws HttpStatusException, IOException { throws HttpStatusException, IOException {
log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString()); log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
HttpGet httpGet = new HttpGet(mSubUri); Request request = new Request.Builder()
return getResponseBody(context, httpGet); .url(mSubUri.toURL())
.addHeader("User-Agent", "OpenKeychain")
.build();
return getResponseBody(request);
} }
@@ -6,7 +6,7 @@ import android.net.Uri;
import android.support.annotation.DrawableRes; import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import org.apache.http.client.methods.HttpGet; import okhttp3.Request;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; 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); return String.format(context.getResources().getString(R.string.linked_id_github_text), token);
} }
@SuppressWarnings("deprecation") // HttpGet is deprecated
@Override @Override
protected String fetchResource (Context context, OperationLog log, int indent) protected String fetchResource (Context context, OperationLog log, int indent)
throws HttpStatusException, IOException, JSONException { throws HttpStatusException, IOException, JSONException {
@@ -55,8 +55,11 @@ public class GithubResource extends LinkedTokenResource {
log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString()); log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
indent += 1; indent += 1;
HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + mGistId); Request request = new Request.Builder()
String response = getResponseBody(context, httpGet); .url("https://api.github.com/gists/" + mGistId)
.addHeader("User-Agent", "OpenKeychain")
.build();
String response = getResponseBody(request);
JSONObject obj = new JSONObject(response); 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" }) @SuppressWarnings({ "deprecation", "unused" })
public static GithubResource searchInGithubStream( public static GithubResource searchInGithubStream(
Context context, String screenName, String needle, OperationLog log) { Context context, String screenName, String needle, OperationLog log) {
@@ -94,12 +97,12 @@ public class GithubResource extends LinkedTokenResource {
try { try {
JSONArray array; { JSONArray array; {
HttpGet httpGet = Request request = new Request.Builder()
new HttpGet("https://api.github.com/users/" + screenName + "/gists"); .url("https://api.github.com/users/" + screenName + "/gists")
httpGet.setHeader("Content-Type", "application/json"); .addHeader("Content-Type", "application/json")
httpGet.setHeader("User-Agent", "OpenKeychain"); .addHeader("User-Agent", "OpenKeychain")
.build();
String response = getResponseBody(context, httpGet); String response = getResponseBody(request);
array = new JSONArray(response); array = new JSONArray(response);
} }
@@ -116,10 +119,13 @@ public class GithubResource extends LinkedTokenResource {
continue; continue;
} }
String id = obj.getString("id"); 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"); JSONObject gistFiles = gistObj.getJSONObject("files");
Iterator<String> gistIt = gistFiles.keys(); Iterator<String> gistIt = gistFiles.keys();
if (!gistIt.hasNext()) { if (!gistIt.hasNext()) {
@@ -9,9 +9,9 @@ import android.util.Log;
import com.textuality.keybase.lib.JWalk; import com.textuality.keybase.lib.JWalk;
import org.apache.http.client.methods.HttpGet; import okhttp3.MediaType;
import org.apache.http.client.methods.HttpPost; import okhttp3.Request;
import org.apache.http.entity.StringEntity; import okhttp3.RequestBody;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@@ -84,18 +84,19 @@ public class TwitterResource extends LinkedTokenResource {
return null; 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 // construct a normal HTTPS request and include an Authorization
// header with the value of Bearer <> // header with the value of Bearer <>
httpGet.setHeader("Authorization", "Bearer " + authToken); Request request = new Request.Builder()
httpGet.setHeader("Content-Type", "application/json"); .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 { try {
String response = getResponseBody(context, httpGet, CERT_PINS); String response = getResponseBody(request, CERT_PINS);
JSONObject obj = new JSONObject(response); JSONObject obj = new JSONObject(response);
JSONObject user = obj.getJSONObject("user"); JSONObject user = obj.getJSONObject("user");
if (!mHandle.equalsIgnoreCase(user.getString("screen_name"))) { if (!mHandle.equalsIgnoreCase(user.getString("screen_name"))) {
@@ -157,21 +158,20 @@ public class TwitterResource extends LinkedTokenResource {
return null; return null;
} }
HttpGet httpGet = Request request = new Request.Builder()
new HttpGet("https://api.twitter.com/1.1/statuses/user_timeline.json" .url("https://api.twitter.com/1.1/statuses/user_timeline.json"
+ "?screen_name=" + screenName + "?screen_name=" + screenName
+ "&count=15" + "&count=15"
+ "&include_rts=false" + "&include_rts=false"
+ "&trim_user=true" + "&trim_user=true"
+ "&exclude_replies=true"); + "&exclude_replies=true")
.addHeader("Authorization", "Bearer " + authToken)
// construct a normal HTTPS request and include an Authorization .addHeader("Content-Type", "application/json")
// header with the value of Bearer <> .addHeader("User-Agent", "OpenKeychain")
httpGet.setHeader("Authorization", "Bearer " + authToken); .build();
httpGet.setHeader("Content-Type", "application/json");
try { try {
String response = getResponseBody(context, httpGet, CERT_PINS); String response = getResponseBody(request, CERT_PINS);
JSONArray array = new JSONArray(response); JSONArray array = new JSONArray(response);
for (int i = 0; i < array.length(); i++) { for (int i = 0; i < array.length(); i++) {
@@ -216,12 +216,20 @@ public class TwitterResource extends LinkedTokenResource {
String base64Encoded = rot13("D293FQqanH0jH29KIaWJER5DomqSGRE2Ewc1LJACn3cbD1c" String base64Encoded = rot13("D293FQqanH0jH29KIaWJER5DomqSGRE2Ewc1LJACn3cbD1c"
+ "Fq1bmqSAQAz5MI2cIHKOuo3cPoRAQI1OyqmIVFJS6LHMXq2g6MRLkIj") + "=="; + "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 // Step 2: Obtain a bearer token
HttpPost httpPost = new HttpPost("https://api.twitter.com/oauth2/token"); Request request = new Request.Builder()
httpPost.setHeader("Authorization", "Basic " + base64Encoded); .url("https://api.twitter.com/oauth2/token")
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); .addHeader("Authorization", "Basic " + base64Encoded)
httpPost.setEntity(new StringEntity("grant_type=client_credentials")); .addHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
JSONObject rawAuthorization = new JSONObject(getResponseBody(context, httpPost, CERT_PINS)); .addHeader("User-Agent", "OpenKeychain")
.post(requestBody)
.build();
JSONObject rawAuthorization = new JSONObject(getResponseBody(request, CERT_PINS));
// Applications should verify that the value associated with the // Applications should verify that the value associated with the
// token_type key of the returned object is bearer // token_type key of the returned object is bearer
@@ -47,7 +47,7 @@ import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; 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.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Passphrase; 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; int certifyOk = 0, certifyError = 0, uploadOk = 0, uploadError = 0;
NfcSignOperationsBuilder allRequiredInput = new NfcSignOperationsBuilder( SecurityTokenSignOperationsBuilder allRequiredInput = new SecurityTokenSignOperationsBuilder(
cryptoInput.getSignatureTime(), masterKeyId, masterKeyId); cryptoInput.getSignatureTime(), masterKeyId, masterKeyId);
// Work through all requested certifications // Work through all requested certifications
@@ -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());
}
}
@@ -119,8 +119,9 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
// inform the storage provider about the mime type for this uri // inform the storage provider about the mime type for this uri
if (decryptResult.getDecryptionMetadata() != null) { if (decryptResult.getDecryptionMetadata() != null) {
TemporaryFileProvider.setMimeType(mContext, currentInputUri, OpenPgpMetadata meta = decryptResult.getDecryptionMetadata();
decryptResult.getDecryptionMetadata().getMimeType()); TemporaryFileProvider.setName(mContext, currentInputUri, meta.getFilename());
TemporaryFileProvider.setMimeType(mContext, currentInputUri, meta.getMimeType());
} }
} else { } else {
@@ -43,7 +43,7 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; 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.service.input.RequiredInputParcel.RequiredInputType;
import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.FileHelper;
import org.sufficientlysecure.keychain.util.InputData; 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; int total = inputBytes != null ? 1 : inputUris.size(), count = 0;
ArrayList<PgpSignEncryptResult> results = new ArrayList<>(); 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 signing subkey has not explicitly been set, get first usable subkey capable of signing
if (input.getSignatureMasterKeyId() != Constants.key.none if (input.getSignatureMasterKeyId() != Constants.key.none
@@ -161,7 +161,7 @@ public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> {
return new SignEncryptResult(log, requiredInput, results, cryptoInput); return new SignEncryptResult(log, requiredInput, results, cryptoInput);
} }
if (pendingInputBuilder == null) { if (pendingInputBuilder == null) {
pendingInputBuilder = new NfcSignOperationsBuilder(requiredInput.mSignatureTime, pendingInputBuilder = new SecurityTokenSignOperationsBuilder(requiredInput.mSignatureTime,
input.getSignatureMasterKeyId(), input.getSignatureSubKeyId()); input.getSignatureMasterKeyId(), input.getSignatureSubKeyId());
} }
pendingInputBuilder.addAll(requiredInput); pendingInputBuilder.addAll(requiredInput);
@@ -131,7 +131,8 @@ public class DeleteResult extends InputPendingResult {
else if (mFail == 0) { else if (mFail == 0) {
str = activity.getString(R.string.delete_nothing); str = activity.getString(R.string.delete_nothing);
} else { } else {
str = activity.getResources().getQuantityString(R.plurals.delete_fail, mFail); str = activity.getResources().getQuantityString(
R.plurals.delete_fail, mFail, mFail);
} }
} }
@@ -539,6 +539,7 @@ public abstract class OperationResult implements Parcelable {
// secret key modify // secret key modify
MSG_MF (LogLevel.START, R.string.msg_mr), MSG_MF (LogLevel.START, R.string.msg_mr),
MSG_MF_DIVERT (LogLevel.DEBUG, R.string.msg_mf_divert), 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_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_DIVERT_SERIAL (LogLevel.ERROR, R.string.msg_mf_error_divert_serial),
MSG_MF_ERROR_ENCODE (LogLevel.ERROR, R.string.msg_mf_error_encode), MSG_MF_ERROR_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_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_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_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_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_PGP (LogLevel.ERROR, R.string.msg_mf_error_pgp),
MSG_MF_ERROR_RESTRICTED(LogLevel.ERROR, R.string.msg_mf_error_restricted), 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_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_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_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_PASSPHRASE (LogLevel.INFO, R.string.msg_mf_passphrase),
MSG_MF_PIN (LogLevel.INFO, R.string.msg_mf_pin), MSG_MF_PIN (LogLevel.INFO, R.string.msg_mf_pin),
MSG_MF_ADMIN_PIN (LogLevel.INFO, R.string.msg_mf_admin_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_PGP (LogLevel.ERROR, R.string.msg_pse_error_pgp),
MSG_PSE_ERROR_SIG (LogLevel.ERROR, R.string.msg_pse_error_sig), 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_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_OK (LogLevel.OK, R.string.msg_pse_key_ok),
MSG_PSE_KEY_UNKNOWN (LogLevel.DEBUG, R.string.msg_pse_key_unknown), MSG_PSE_KEY_UNKNOWN (LogLevel.DEBUG, R.string.msg_pse_key_unknown),
MSG_PSE_KEY_WARN (LogLevel.WARN, R.string.msg_pse_key_warn), MSG_PSE_KEY_WARN (LogLevel.WARN, R.string.msg_pse_key_warn),
@@ -61,6 +61,8 @@ public abstract class KeyRing {
private static final Pattern USER_ID_PATTERN = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$"); 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 * Splits userId string into naming part, email part, and comment part
* <p/> * <p/>
@@ -71,25 +73,38 @@ public abstract class KeyRing {
if (!TextUtils.isEmpty(userId)) { if (!TextUtils.isEmpty(userId)) {
final Matcher matcher = USER_ID_PATTERN.matcher(userId); final Matcher matcher = USER_ID_PATTERN.matcher(userId);
if (matcher.matches()) { 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); 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) { public static String createUserId(UserId userId) {
String userIdString = userId.name; // consider name a required value StringBuilder userIdBuilder = new StringBuilder();
if (userIdString != null && !TextUtils.isEmpty(userId.comment)) { if (!TextUtils.isEmpty(userId.name)) {
userIdString += " (" + userId.comment + ")"; userIdBuilder.append(userId.name);
} }
if (userIdString != null && !TextUtils.isEmpty(userId.email)) { if (!TextUtils.isEmpty(userId.comment)) {
userIdString += " <" + userId.email + ">"; userIdBuilder.append(" (");
userIdBuilder.append(userId.comment);
userIdBuilder.append(")");
} }
if (!TextUtils.isEmpty(userId.email)) {
return userIdString; userIdBuilder.append(" <");
userIdBuilder.append(userId.email);
userIdBuilder.append(">");
}
return userIdBuilder.length() == 0 ? null : userIdBuilder.toString();
} }
public static class UserId implements Serializable { public static class UserId implements Serializable {
@@ -26,6 +26,8 @@ public class OpenPgpDecryptionResultBuilder {
// builder // builder
private boolean mInsecure = false; private boolean mInsecure = false;
private boolean mEncrypted = false; private boolean mEncrypted = false;
private byte[] sessionKey;
private byte[] decryptedSessionKey;
public void setInsecure(boolean insecure) { public void setInsecure(boolean insecure) {
this.mInsecure = insecure; this.mInsecure = insecure;
@@ -36,24 +38,26 @@ public class OpenPgpDecryptionResultBuilder {
} }
public OpenPgpDecryptionResult build() { public OpenPgpDecryptionResult build() {
OpenPgpDecryptionResult result = new OpenPgpDecryptionResult();
if (mInsecure) { if (mInsecure) {
Log.d(Constants.TAG, "RESULT_INSECURE"); Log.d(Constants.TAG, "RESULT_INSECURE");
result.setResult(OpenPgpDecryptionResult.RESULT_INSECURE); return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_INSECURE, sessionKey, decryptedSessionKey);
return result;
} }
if (mEncrypted) { if (mEncrypted) {
Log.d(Constants.TAG, "RESULT_ENCRYPTED"); Log.d(Constants.TAG, "RESULT_ENCRYPTED");
result.setResult(OpenPgpDecryptionResult.RESULT_ENCRYPTED); return new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_ENCRYPTED, sessionKey, decryptedSessionKey);
} else {
Log.d(Constants.TAG, "RESULT_NOT_ENCRYPTED");
result.setResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED);
} }
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;
}
} }
@@ -37,7 +37,7 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; 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.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
@@ -76,7 +76,7 @@ public class PgpCertifyOperation {
// get the master subkey (which we certify for) // get the master subkey (which we certify for)
PGPPublicKey publicKey = publicRing.getPublicKey().getPublicKey(); PGPPublicKey publicKey = publicRing.getPublicKey().getPublicKey();
NfcSignOperationsBuilder requiredInput = new NfcSignOperationsBuilder(creationTimestamp, SecurityTokenSignOperationsBuilder requiredInput = new SecurityTokenSignOperationsBuilder(creationTimestamp,
publicKey.getKeyID(), publicKey.getKeyID()); publicKey.getKeyID(), publicKey.getKeyID());
try { try {
@@ -26,9 +26,12 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.SignatureException; import java.security.SignatureException;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@@ -60,7 +63,6 @@ import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.Constants.key; import org.sufficientlysecure.keychain.Constants.key;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.BaseOperation; 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.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; 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.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.CharsetVerifier;
import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.FileHelper;
import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
@@ -197,6 +200,10 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
PGPEncryptedData encryptedData; PGPEncryptedData encryptedData;
InputStream cleartextStream; InputStream cleartextStream;
// the cached session key
byte[] sessionKey;
byte[] decryptedSessionKey;
int symmetricEncryptionAlgo = 0; int symmetricEncryptionAlgo = 0;
boolean skippedDisallowedKey = false; boolean skippedDisallowedKey = false;
@@ -304,6 +311,9 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
// if this worked out so far, the data is encrypted // if this worked out so far, the data is encrypted
decryptionResultBuilder.setEncrypted(true); decryptionResultBuilder.setEncrypted(true);
if (esResult.sessionKey != null && esResult.decryptedSessionKey != null) {
decryptionResultBuilder.setSessionKey(esResult.sessionKey, esResult.decryptedSessionKey);
}
if (esResult.insecureEncryptionKey) { if (esResult.insecureEncryptionKey) {
log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1); 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 asymmetricPacketFound = false;
boolean symmetricPacketFound = false; boolean symmetricPacketFound = false;
boolean anyPacketFound = false; boolean anyPacketFound = false;
boolean decryptedSessionKeyAvailable = false;
PGPPublicKeyEncryptedData encryptedDataAsymmetric = null; PGPPublicKeyEncryptedData encryptedDataAsymmetric = null;
PGPPBEEncryptedData encryptedDataSymmetric = null; PGPPBEEncryptedData encryptedDataSymmetric = null;
CanonicalizedSecretKey decryptionKey = null; CanonicalizedSecretKey decryptionKey = null;
CachingDataDecryptorFactory cachedKeyDecryptorFactory = new CachingDataDecryptorFactory(
Constants.BOUNCY_CASTLE_PROVIDER_NAME, cryptoInput.getCryptoData());
;
Passphrase passphrase = null; Passphrase passphrase = null;
@@ -569,6 +583,13 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
log.add(LogType.MSG_DC_ASYM, indent, log.add(LogType.MSG_DC_ASYM, indent,
KeyFormattingUtils.convertKeyIdToHex(subKeyId)); KeyFormattingUtils.convertKeyIdToHex(subKeyId));
decryptedSessionKeyAvailable = cachedKeyDecryptorFactory.hasCachedSessionData(encData);
if (decryptedSessionKeyAvailable) {
asymmetricPacketFound = true;
encryptedDataAsymmetric = encData;
break;
}
CachedPublicKeyRing cachedPublicKeyRing; CachedPublicKeyRing cachedPublicKeyRing;
try { try {
// get actual keyring object based on master key id // get actual keyring object based on master key id
@@ -746,34 +767,38 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
currentProgress += 2; currentProgress += 2;
updateProgress(R.string.progress_extracting_key, currentProgress, 100); updateProgress(R.string.progress_extracting_key, currentProgress, 100);
try { CachingDataDecryptorFactory decryptorFactory;
log.add(LogType.MSG_DC_UNLOCKING, indent + 1); if (decryptedSessionKeyAvailable) {
if (!decryptionKey.unlock(passphrase)) { decryptorFactory = cachedKeyDecryptorFactory;
log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1); } 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)); 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; currentProgress += 2;
updateProgress(R.string.progress_preparing_streams, currentProgress, 100); updateProgress(R.string.progress_preparing_streams, currentProgress, 100);
CachingDataDecryptorFactory decryptorFactory decryptorFactory = decryptionKey.getCachingDecryptorFactory(cryptoInput);
= decryptionKey.getCachingDecryptorFactory(cryptoInput);
// special case: if the decryptor does not have a session key cached for this encrypted // 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 // data, and can't actually decrypt on its own, return a pending intent
if (!decryptorFactory.canDecrypt() if (!decryptorFactory.canDecrypt()
&& !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) { && !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) {
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));
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 { try {
@@ -786,8 +811,13 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
result.symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory); result.symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory);
result.encryptedData = encryptedDataAsymmetric; 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 { } else {
// there wasn't even any useful data // there wasn't even any useful data
if (!anyPacketFound) { if (!anyPacketFound) {
@@ -18,6 +18,23 @@
package org.sufficientlysecure.keychain.pgp; 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.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.S2K;
import org.bouncycastle.bcpg.sig.Features; 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.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; 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;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; 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.Curve;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenKeyToCardOperationsBuilder;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcKeyToCardOperationsBuilder; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.SecurityTokenSignOperationsBuilder;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log; 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.Primes;
import org.sufficientlysecure.keychain.util.ProgressScaler; 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 * This class is the single place where ALL operations that actually modify a PGP public or secret
* key take place. * key take place.
@@ -496,10 +497,10 @@ public class PgpKeyOperation {
OperationLog log, OperationLog log,
int indent) { int indent) {
NfcSignOperationsBuilder nfcSignOps = new NfcSignOperationsBuilder( SecurityTokenSignOperationsBuilder nfcSignOps = new SecurityTokenSignOperationsBuilder(
cryptoInput.getSignatureTime(), masterSecretKey.getKeyID(), cryptoInput.getSignatureTime(), masterSecretKey.getKeyID(),
masterSecretKey.getKeyID()); masterSecretKey.getKeyID());
NfcKeyToCardOperationsBuilder nfcKeyToCardOps = new NfcKeyToCardOperationsBuilder( SecurityTokenKeyToCardOperationsBuilder nfcKeyToCardOps = new SecurityTokenKeyToCardOperationsBuilder(
masterSecretKey.getKeyID()); masterSecretKey.getKeyID());
progress(R.string.progress_modify, 0); progress(R.string.progress_modify, 0);
@@ -1053,13 +1054,13 @@ public class PgpKeyOperation {
} }
// 6. If requested, change passphrase // 6. If requested, change passphrase
if (saveParcel.mNewUnlock != null) { if (saveParcel.getChangeUnlockParcel() != null) {
progress(R.string.progress_modify_passphrase, 90); progress(R.string.progress_modify_passphrase, 90);
log.add(LogType.MSG_MF_PASSPHRASE, indent); log.add(LogType.MSG_MF_PASSPHRASE, indent);
indent += 1; indent += 1;
sKR = applyNewUnlock(sKR, masterPublicKey, masterPrivateKey, sKR = applyNewPassphrase(sKR, masterPublicKey, cryptoInput.getPassphrase(),
cryptoInput.getPassphrase(), saveParcel.mNewUnlock, log, indent); saveParcel.getChangeUnlockParcel().mNewPassphrase, log, indent);
if (sKR == null) { if (sKR == null) {
// The error has been logged above, just return a bad state // The error has been logged above, just return a bad state
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null); return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
@@ -1192,73 +1193,80 @@ public class PgpKeyOperation {
} }
private static PGPSecretKeyRing applyNewUnlock( public PgpEditKeyResult modifyKeyRingPassphrase(CanonicalizedSecretKeyRing wsKR,
PGPSecretKeyRing sKR, CryptoInputParcel cryptoInput,
PGPPublicKey masterPublicKey, ChangeUnlockParcel changeUnlockParcel) {
PGPPrivateKey masterPrivateKey,
Passphrase passphrase,
ChangeUnlockParcel newUnlock,
OperationLog log, int indent) throws PGPException {
if (newUnlock.mNewPassphrase != null) { OperationLog log = new OperationLog();
sKR = applyNewPassphrase(sKR, masterPublicKey, passphrase, newUnlock.mNewPassphrase, log, indent); int indent = 0;
// if there is any old packet with notation data if (changeUnlockParcel.mMasterKeyId == null || changeUnlockParcel.mMasterKeyId != wsKR.getMasterKeyId()) {
if (hasNotationData(sKR)) { log.add(LogType.MSG_MF_ERROR_KEYID, indent);
return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
}
log.add(LogType.MSG_MF_NOTATION_EMPTY, indent); log.add(LogType.MSG_MF, indent,
KeyFormattingUtils.convertKeyIdToHex(wsKR.getMasterKeyId()));
indent += 1;
progress(R.string.progress_building_key, 0);
// add packet with EMPTY notation data (updates old one, but will be stripped later) // We work on bouncycastle object level here
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( PGPSecretKeyRing sKR = wsKR.getRing();
masterPrivateKey.getPublicKeyPacket().getAlgorithm(), PGPSecretKey masterSecretKey = sKR.getSecretKey();
PgpSecurityConstants.SECRET_KEY_BINDING_SIGNATURE_HASH_ALGO) PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); // Make sure the fingerprint matches
PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder); if (changeUnlockParcel.mFingerprint == null || !Arrays.equals(changeUnlockParcel.mFingerprint,
{ // set subpackets masterSecretKey.getPublicKey().getFingerprint())) {
PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator(); log.add(LogType.MSG_MF_ERROR_FINGERPRINT, indent);
hashedPacketsGen.setExportable(false, false); return new PgpEditKeyResult(PgpEditKeyResult.RESULT_ERROR, log, null);
sGen.setHashedSubpackets(hashedPacketsGen.generate()); }
// 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 (!cryptoInput.hasPassphrase()) {
log.add(LogType.MSG_MF_REQUIRE_PASSPHRASE, 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;
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);
} }
sGen.init(PGPSignature.DIRECT_KEY, masterPrivateKey); } catch (PGPException e) {
PGPSignature emptySig = sGen.generateCertification(masterPublicKey); throw new UnsupportedOperationException("Failed to build encryptor/decryptor!");
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));
} }
}
if (newUnlock.mNewPin != null) { private static PGPSecretKey firstNonDummySecretKeyID(PGPSecretKeyRing secRing) {
sKR = applyNewPassphrase(sKR, masterPublicKey, passphrase, newUnlock.mNewPin, log, indent); Iterator<PGPSecretKey> secretKeyIterator = secRing.getSecretKeys();
log.add(LogType.MSG_MF_NOTATION_PIN, indent); while(secretKeyIterator.hasNext()) {
PGPSecretKey secretKey = secretKeyIterator.next();
// add packet with "pin" notation data if(!isDummy(secretKey)){
PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( return secretKey;
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());
} }
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;
} }
return null;
throw new UnsupportedOperationException("PIN passphrases not yet implemented!");
} }
/** This method returns true iff the provided keyring has a local direct key signature /** 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_SYMMETRIC_ALGO, encryptorHashCalc,
PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT) PgpSecurityConstants.SECRET_KEY_ENCRYPTOR_S2K_COUNT)
.setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(newPassphrase.getCharArray()); .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(newPassphrase.getCharArray());
boolean keysModified = false;
// noinspection unchecked for (PGPSecretKey sKey : new IterableIterator<>(sKR.getSecretKeys())) {
for (PGPSecretKey sKey : new IterableIterator<PGPSecretKey>(sKR.getSecretKeys())) {
log.add(LogType.MSG_MF_PASSPHRASE_KEY, indent, log.add(LogType.MSG_MF_PASSPHRASE_KEY, indent,
KeyFormattingUtils.convertKeyIdToHex(sKey.getKeyID())); KeyFormattingUtils.convertKeyIdToHex(sKey.getKeyID()));
@@ -1307,8 +1315,8 @@ public class PgpKeyOperation {
ok = true; ok = true;
} catch (PGPException e) { } catch (PGPException e) {
// if this is the master key, error! // if the master key failed && it's not stripped, error!
if (sKey.getKeyID() == masterPublicKey.getKeyID()) { if (sKey.getKeyID() == masterPublicKey.getKeyID() && !isDummy(sKey)) {
log.add(LogType.MSG_MF_ERROR_PASSPHRASE_MASTER, indent+1); log.add(LogType.MSG_MF_ERROR_PASSPHRASE_MASTER, indent+1);
return null; return null;
} }
@@ -1335,7 +1343,13 @@ public class PgpKeyOperation {
} }
sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey); 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; return sKR;
@@ -1348,7 +1362,7 @@ public class PgpKeyOperation {
PGPPublicKey masterPublicKey, PGPPublicKey masterPublicKey,
int flags, long expiry, int flags, long expiry,
CryptoInputParcel cryptoInput, CryptoInputParcel cryptoInput,
NfcSignOperationsBuilder nfcSignOps, SecurityTokenSignOperationsBuilder nfcSignOps,
int indent, OperationLog log) int indent, OperationLog log)
throws PGPException, IOException, SignatureException { throws PGPException, IOException, SignatureException {
@@ -38,7 +38,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
protected Long mSignatureSubKeyId = null; protected Long mSignatureSubKeyId = null;
protected int mSignatureHashAlgorithm = PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT; protected int mSignatureHashAlgorithm = PgpSecurityConstants.OpenKeychainHashAlgorithmTags.USE_DEFAULT;
protected long mAdditionalEncryptId = Constants.key.none; protected long mAdditionalEncryptId = Constants.key.none;
protected boolean mFailOnMissingEncryptionKeyIds = false;
protected String mCharset; protected String mCharset;
protected boolean mCleartextSignature; protected boolean mCleartextSignature;
protected boolean mDetachedSignature = false; protected boolean mDetachedSignature = false;
@@ -65,7 +64,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
mSignatureSubKeyId = source.readInt() == 1 ? source.readLong() : null; mSignatureSubKeyId = source.readInt() == 1 ? source.readLong() : null;
mSignatureHashAlgorithm = source.readInt(); mSignatureHashAlgorithm = source.readInt();
mAdditionalEncryptId = source.readLong(); mAdditionalEncryptId = source.readLong();
mFailOnMissingEncryptionKeyIds = source.readInt() == 1;
mCharset = source.readString(); mCharset = source.readString();
mCleartextSignature = source.readInt() == 1; mCleartextSignature = source.readInt() == 1;
mDetachedSignature = source.readInt() == 1; mDetachedSignature = source.readInt() == 1;
@@ -96,7 +94,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
} }
dest.writeInt(mSignatureHashAlgorithm); dest.writeInt(mSignatureHashAlgorithm);
dest.writeLong(mAdditionalEncryptId); dest.writeLong(mAdditionalEncryptId);
dest.writeInt(mFailOnMissingEncryptionKeyIds ? 1 : 0);
dest.writeString(mCharset); dest.writeString(mCharset);
dest.writeInt(mCleartextSignature ? 1 : 0); dest.writeInt(mCleartextSignature ? 1 : 0);
dest.writeInt(mDetachedSignature ? 1 : 0); dest.writeInt(mDetachedSignature ? 1 : 0);
@@ -113,10 +110,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
this.mCharset = mCharset; this.mCharset = mCharset;
} }
public boolean isFailOnMissingEncryptionKeyIds() {
return mFailOnMissingEncryptionKeyIds;
}
public long getAdditionalEncryptId() { public long getAdditionalEncryptId() {
return mAdditionalEncryptId; return mAdditionalEncryptId;
} }
@@ -207,11 +200,6 @@ public class PgpSignEncryptInputParcel implements Parcelable {
return this; return this;
} }
public PgpSignEncryptInputParcel setFailOnMissingEncryptionKeyIds(boolean failOnMissingEncryptionKeyIds) {
mFailOnMissingEncryptionKeyIds = failOnMissingEncryptionKeyIds;
return this;
}
public PgpSignEncryptInputParcel setCleartextSignature(boolean cleartextSignature) { public PgpSignEncryptInputParcel setCleartextSignature(boolean cleartextSignature) {
this.mCleartextSignature = cleartextSignature; this.mCleartextSignature = cleartextSignature;
return this; return this;
@@ -168,10 +168,17 @@ public class PgpSignEncryptOperation extends BaseOperation {
try { try {
long signingMasterKeyId = input.getSignatureMasterKeyId(); long signingMasterKeyId = input.getSignatureMasterKeyId();
long signingSubKeyId = input.getSignatureSubKeyId(); long signingSubKeyId = input.getSignatureSubKeyId();
{
CanonicalizedSecretKeyRing signingKeyRing = CanonicalizedSecretKeyRing signingKeyRing =
mProviderHelper.getCanonicalizedSecretKeyRing(signingMasterKeyId); mProviderHelper.getCanonicalizedSecretKeyRing(signingMasterKeyId);
signingKey = signingKeyRing.getSecretKey(input.getSignatureSubKeyId()); 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! // Make sure we are allowed to sign here!
@@ -281,16 +288,17 @@ public class PgpSignEncryptOperation extends BaseOperation {
if (encryptSubKeyIds.isEmpty()) { if (encryptSubKeyIds.isEmpty()) {
log.add(LogType.MSG_PSE_KEY_WARN, indent + 1, log.add(LogType.MSG_PSE_KEY_WARN, indent + 1,
KeyFormattingUtils.convertKeyIdToHex(id)); 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) { } catch (ProviderHelper.NotFoundException e) {
log.add(LogType.MSG_PSE_KEY_UNKNOWN, indent + 1, log.add(LogType.MSG_PSE_KEY_UNKNOWN, indent + 1,
KeyFormattingUtils.convertKeyIdToHex(id)); 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()); InputStream in = new BufferedInputStream(inputData.getInputStream());
if (enableCompression) { 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)); bcpgOut = new BCPGOutputStream(compressGen.open(out));
} else { } else {
bcpgOut = new BCPGOutputStream(out); bcpgOut = new BCPGOutputStream(out);
@@ -514,7 +528,7 @@ public class PgpSignEncryptOperation extends BaseOperation {
} catch (NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded e) { } catch (NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded e) {
// this secret key diverts to a OpenPGP card, throw exception with hash that will be signed // this secret key diverts to a OpenPGP card, throw exception with hash that will be signed
log.add(LogType.MSG_PSE_PENDING_NFC, indent); 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(), signingKey.getRing().getMasterKeyId(), signingKey.getKeyId(),
e.hashToSign, e.hashAlgo, cryptoInput.getSignatureTime()), cryptoInput); e.hashToSign, e.hashAlgo, cryptoInput.getSignatureTime()), cryptoInput);
} }
@@ -35,6 +35,8 @@ import java.util.Set;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.TreeSet; import java.util.TreeSet;
import android.support.annotation.VisibleForTesting;
import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.SignatureSubpacketTags; import org.bouncycastle.bcpg.SignatureSubpacketTags;
@@ -42,15 +44,22 @@ import org.bouncycastle.bcpg.UserAttributeSubpacketTags;
import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.bcpg.sig.KeyFlags;
import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPKeyRing;
import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector;
import org.bouncycastle.openpgp.PGPUtil; 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.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
@@ -1310,4 +1319,37 @@ public class UncachedKeyRing {
|| algorithm == PGPPublicKey.ECDH; || 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);
}
} }
@@ -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();
}
}
}
}
@@ -60,6 +60,9 @@ public class KeychainContract {
String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID
String TYPE = "type"; // not a database id String TYPE = "type"; // not a database id
String USER_ID = "user_id"; // 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 ATTRIBUTE_DATA = "attribute_data"; // not a database id
String RANK = "rank"; // ONLY used for sorting! no key, no nothing! String RANK = "rank"; // ONLY used for sorting! no key, no nothing!
String IS_PRIMARY = "is_primary"; String IS_PRIMARY = "is_primary";
@@ -359,6 +362,9 @@ public class KeychainContract {
public static class Certs implements CertsColumns, BaseColumns { public static class Certs implements CertsColumns, BaseColumns {
public static final String USER_ID = UserPacketsColumns.USER_ID; 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 String SIGNER_UID = "signer_user_id";
public static final int UNVERIFIED = 0; public static final int UNVERIFIED = 0;
@@ -54,7 +54,7 @@ import java.io.IOException;
*/ */
public class KeychainDatabase extends SQLiteOpenHelper { public class KeychainDatabase extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "openkeychain.db"; 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; static Boolean apgHack = false;
private Context mContext; private Context mContext;
@@ -115,6 +115,9 @@ public class KeychainDatabase extends SQLiteOpenHelper {
+ UserPacketsColumns.MASTER_KEY_ID + " INTEGER, " + UserPacketsColumns.MASTER_KEY_ID + " INTEGER, "
+ UserPacketsColumns.TYPE + " INT, " + UserPacketsColumns.TYPE + " INT, "
+ UserPacketsColumns.USER_ID + " TEXT, " + UserPacketsColumns.USER_ID + " TEXT, "
+ UserPacketsColumns.NAME + " TEXT, "
+ UserPacketsColumns.EMAIL + " TEXT, "
+ UserPacketsColumns.COMMENT + " TEXT, "
+ UserPacketsColumns.ATTRIBUTE_DATA + " BLOB, " + UserPacketsColumns.ATTRIBUTE_DATA + " BLOB, "
+ UserPacketsColumns.IS_PRIMARY + " INTEGER, " + 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 type INTEGER");
db.execSQL("ALTER TABLE user_ids ADD COLUMN attribute_data BLOB"); db.execSQL("ALTER TABLE user_ids ADD COLUMN attribute_data BLOB");
case 7: case 7:
// consolidate
case 8:
// new table for allowed key ids in API // new table for allowed key ids in API
try { try {
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
} catch (Exception e) { } catch (Exception e) {
// never mind, the column probably already existed // never mind, the column probably already existed
} }
case 9: case 8:
// tbale name for user_ids changed to user_packets // tbale name for user_ids changed to user_packets
db.execSQL("DROP TABLE IF EXISTS certs"); db.execSQL("DROP TABLE IF EXISTS certs");
db.execSQL("DROP TABLE IF EXISTS user_ids"); db.execSQL("DROP TABLE IF EXISTS user_ids");
db.execSQL(CREATE_USER_PACKETS); db.execSQL(CREATE_USER_PACKETS);
db.execSQL(CREATE_CERTS); db.execSQL(CREATE_CERTS);
case 10: case 9:
// do nothing here, just consolidate // do nothing here, just consolidate
case 11: case 10:
// fix problems in database, see #1402 for details // fix problems in database, see #1402 for details
// https://github.com/open-keychain/open-keychain/issues/1402 // https://github.com/open-keychain/open-keychain/issues/1402
db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3"); db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3");
case 12: case 11:
db.execSQL(CREATE_UPDATE_KEYS); db.execSQL(CREATE_UPDATE_KEYS);
case 13: case 12:
// do nothing here, just consolidate // do nothing here, just consolidate
case 14: case 13:
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");"); db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");");
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", " db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
+ UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");"); + UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");");
db.execSQL("CREATE INDEX verified_certs ON certs (" db.execSQL("CREATE INDEX verified_certs ON certs ("
+ CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");"); + 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 // always do consolidate after upgrade
@@ -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() {
}
}
@@ -1,7 +1,7 @@
/* /*
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> * 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 * 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 * 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.ALGORITHM, Tables.KEYS + "." + Keys.ALGORITHM);
projectionMap.put(KeyRings.FINGERPRINT, Tables.KEYS + "." + Keys.FINGERPRINT); projectionMap.put(KeyRings.FINGERPRINT, Tables.KEYS + "." + Keys.FINGERPRINT);
projectionMap.put(KeyRings.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID); 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, 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 + " WHERE dups." + UserPackets.MASTER_KEY_ID
+ " != " + Tables.KEYS + "." + Keys.MASTER_KEY_ID + " != " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
+ " AND dups." + UserPackets.RANK + " = 0" + " AND dups." + UserPackets.RANK + " = 0"
+ " AND dups." + UserPackets.USER_ID + " AND dups." + UserPackets.NAME
+ " = "+ Tables.USER_PACKETS + "." + UserPackets.USER_ID + " = " + Tables.USER_PACKETS + "." + UserPackets.NAME + " COLLATE NOCASE"
+ " AND dups." + UserPackets.EMAIL
+ " = " + Tables.USER_PACKETS + "." + UserPackets.EMAIL + " COLLATE NOCASE"
+ ")) AS " + KeyRings.HAS_DUPLICATE_USER_ID); + ")) AS " + KeyRings.HAS_DUPLICATE_USER_ID);
projectionMap.put(KeyRings.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED); projectionMap.put(KeyRings.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED);
projectionMap.put(KeyRings.PUBKEY_DATA, projectionMap.put(KeyRings.PUBKEY_DATA,
@@ -451,12 +456,12 @@ public class KeychainProvider extends ContentProvider {
if (i != 0) { if (i != 0) {
emailWhere += " OR "; 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) { if (match == KEY_RINGS_FIND_BY_EMAIL) {
emailWhere += DatabaseUtils.sqlEscapeString("%<" + chunks[i] + ">"); emailWhere += "tmp." + UserPackets.EMAIL + " LIKE "
+ DatabaseUtils.sqlEscapeString(chunks[i]);
} else { } else {
emailWhere += DatabaseUtils.sqlEscapeString("%" + chunks[i] + "%"); emailWhere += "tmp." + UserPackets.USER_ID + " LIKE "
+ DatabaseUtils.sqlEscapeString("%" + chunks[i] + "%");
} }
gotCondition = true; gotCondition = true;
} }
@@ -18,6 +18,7 @@
package org.sufficientlysecure.keychain.provider; package org.sufficientlysecure.keychain.provider;
import android.content.ContentProviderOperation; import android.content.ContentProviderOperation;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentValues; import android.content.ContentValues;
@@ -29,19 +30,12 @@ import android.os.RemoteException;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.util.LongSparseArray; import android.support.v4.util.LongSparseArray;
import org.bouncycastle.bcpg.CompressionAlgorithmTags;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; 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.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.ImportOperation;
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; 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.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; 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.CanonicalizedSecretKey.SecretKeyType;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.KeyRing;
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
import org.sufficientlysecure.keychain.pgp.WrappedSignature; import org.sufficientlysecure.keychain.pgp.WrappedSignature;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps;
import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys;
import org.sufficientlysecure.keychain.remote.AccountSettings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.remote.AppSettings; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.IterableIterator;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ParcelableFileCache; 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.ProgressFixedScaler;
import org.sufficientlysecure.keychain.util.ProgressScaler; import org.sufficientlysecure.keychain.util.ProgressScaler;
import org.sufficientlysecure.keychain.util.Utf8Util; import org.sufficientlysecure.keychain.util.Utf8Util;
@@ -80,10 +75,8 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@@ -459,11 +452,13 @@ public class ProviderHelper {
mIndent += 1; mIndent += 1;
for (byte[] rawUserId : masterKey.getUnorderedRawUserIds()) { for (byte[] rawUserId : masterKey.getUnorderedRawUserIds()) {
String userId = Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(rawUserId); String userId = Utf8Util.fromUTF8ByteArrayReplaceBadEncoding(rawUserId);
UserPacketItem item = new UserPacketItem(); UserPacketItem item = new UserPacketItem();
uids.add(item); uids.add(item);
KeyRing.UserId splitUserId = KeyRing.splitUserId(userId);
item.userId = userId; item.userId = userId;
item.name = splitUserId.name;
item.email = splitUserId.email;
item.comment = splitUserId.comment;
int unknownCerts = 0; int unknownCerts = 0;
log(LogType.MSG_IP_UID_PROCESSING, userId); log(LogType.MSG_IP_UID_PROCESSING, userId);
@@ -753,6 +748,9 @@ public class ProviderHelper {
private static class UserPacketItem implements Comparable<UserPacketItem> { private static class UserPacketItem implements Comparable<UserPacketItem> {
Integer type; Integer type;
String userId; String userId;
String name;
String email;
String comment;
byte[] attributeData; byte[] attributeData;
boolean isPrimary = false; boolean isPrimary = false;
WrappedSignature selfCert; WrappedSignature selfCert;
@@ -1444,6 +1442,9 @@ public class ProviderHelper {
values.put(UserPackets.MASTER_KEY_ID, masterKeyId); values.put(UserPackets.MASTER_KEY_ID, masterKeyId);
values.put(UserPackets.TYPE, item.type); values.put(UserPackets.TYPE, item.type);
values.put(UserPackets.USER_ID, item.userId); 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.ATTRIBUTE_DATA, item.attributeData);
values.put(UserPackets.IS_PRIMARY, item.isPrimary); values.put(UserPackets.IS_PRIMARY, item.isPrimary);
values.put(UserPackets.IS_REVOKED, item.selfRevocation != null); values.put(UserPackets.IS_REVOKED, item.selfRevocation != null);
@@ -1481,191 +1482,6 @@ public class ProviderHelper {
return mContentResolver.insert(UpdatedKeys.CONTENT_URI, values); 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() { public ContentResolver getContentResolver() {
return mContentResolver; return mContentResolver;
} }
@@ -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);
}
@@ -99,6 +99,12 @@ public class TemporaryFileProvider extends ContentProvider {
return context.getContentResolver().insert(CONTENT_URI, contentValues); 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) { public static int setMimeType(Context context, Uri uri, String mimetype) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(TemporaryFileColumns.COLUMN_TYPE, mimetype); values.put(TemporaryFileColumns.COLUMN_TYPE, mimetype);
@@ -283,8 +289,11 @@ public class TemporaryFileProvider extends ContentProvider {
@Override @Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
if (values.size() != 1 || !values.containsKey(TemporaryFileColumns.COLUMN_TYPE)) { if (values.size() != 1) {
throw new UnsupportedOperationException("Update supported only for type field!"); 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) { if (selection != null || selectionArgs != null) {
throw new UnsupportedOperationException("Update supported only for plain uri!"); throw new UnsupportedOperationException("Update supported only for plain uri!");
@@ -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!");
}
}
}
@@ -51,10 +51,10 @@ public class ApiPendingIntentFactory {
CryptoInputParcel cryptoInput) { CryptoInputParcel cryptoInput) {
switch (requiredInput.mType) { switch (requiredInput.mType) {
case NFC_MOVE_KEY_TO_CARD: case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
case NFC_DECRYPT: case SECURITY_TOKEN_DECRYPT:
case NFC_SIGN: { case SECURITY_TOKEN_SIGN: {
return createNfcOperationPendingIntent(data, requiredInput, cryptoInput); return createSecurityTokenOperationPendingIntent(data, requiredInput, cryptoInput);
} }
case PASSPHRASE: { 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); Intent intent = new Intent(mContext, RemoteSecurityTokenOperationActivity.class);
// pass params through to activity that it can be returned again later to repeat pgp operation // pass params through to activity that it can be returned again later to repeat pgp operation
intent.putExtra(RemoteSecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput); intent.putExtra(RemoteSecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput);
@@ -18,6 +18,10 @@
package org.sufficientlysecure.keychain.remote; package org.sufficientlysecure.keychain.remote;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
@@ -33,15 +37,10 @@ import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpApi;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.util.Log; 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. * Abstract service class for remote APIs that handle app registration and user input.
@@ -49,13 +48,13 @@ import java.util.Arrays;
public class ApiPermissionHelper { public class ApiPermissionHelper {
private final Context mContext; private final Context mContext;
private final ProviderHelper mProviderHelper; private final ApiDataAccessObject mApiDao;
private PackageManager mPackageManager; private PackageManager mPackageManager;
public ApiPermissionHelper(Context context) { public ApiPermissionHelper(Context context, ApiDataAccessObject apiDao) {
mContext = context; mContext = context;
mPackageManager = context.getPackageManager(); mPackageManager = context.getPackageManager();
mProviderHelper = new ProviderHelper(context); mApiDao = apiDao;
} }
public static class WrongPackageCertificateException extends Exception { 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 * Checks if caller is allowed to access the API
* *
* @return null if caller is allowed, or a Bundle with a PendingIntent * @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); ApiPendingIntentFactory piFactory = new ApiPendingIntentFactory(mContext);
try { try {
if (isCallerAllowed()) { if (isCallerAllowed()) {
return null; return null;
@@ -168,7 +177,7 @@ public class ApiPermissionHelper {
Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(currentPkg, accountName); Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(currentPkg, accountName);
return mProviderHelper.getApiAccountSettings(uri); // can be null! return mApiDao.getApiAccountSettings(uri); // can be null!
} }
@Deprecated @Deprecated
@@ -224,35 +233,29 @@ public class ApiPermissionHelper {
private boolean isPackageAllowed(String packageName) throws WrongPackageCertificateException { private boolean isPackageAllowed(String packageName) throws WrongPackageCertificateException {
Log.d(Constants.TAG, "isPackageAllowed packageName: " + packageName); Log.d(Constants.TAG, "isPackageAllowed packageName: " + packageName);
ArrayList<String> allowedPkgs = mProviderHelper.getRegisteredApiApps(); byte[] storedPackageCert = mApiDao.getApiAppCertificate(packageName);
Log.d(Constants.TAG, "allowed: " + allowedPkgs);
// check if package is allowed to use our service boolean isKnownPackage = storedPackageCert != null;
if (allowedPkgs.contains(packageName)) { if (!isKnownPackage) {
Log.d(Constants.TAG, "Package is allowed! packageName: " + packageName); 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[] currentPackageCert;
byte[] currentCert; try {
try { currentPackageCert = getPackageCertificate(packageName);
currentCert = getPackageCertificate(packageName); } catch (NameNotFoundException e) {
} catch (NameNotFoundException e) { throw new WrongPackageCertificateException(e.getMessage());
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)");
}
} }
Log.d(Constants.TAG, "Package is NOT allowed! packageName: " + packageName); boolean packageCertMatchesStored = Arrays.equals(currentPackageCert, storedPackageCert);
return false; if (packageCertMatchesStored) {
Log.d(Constants.TAG,"Package certificate matches expected.");
return true;
}
throw new WrongPackageCertificateException("PACKAGE NOT ALLOWED DUE TO CERTIFICATE MISMATCH!");
} }
} }
@@ -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();
}
}
@@ -1,5 +1,6 @@
/* /*
* Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de> * 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 * 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 * it under the terms of the GNU General Public License as published by
@@ -17,11 +18,24 @@
package org.sufficientlysecure.keychain.remote; 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.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.os.Parcelable; 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.OperationResult.LogEntryParcel;
import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; 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.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants;
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel;
import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; 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;
import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; 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.Log;
import org.sufficientlysecure.keychain.util.Passphrase; 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 { 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._ID,
KeyRings.MASTER_KEY_ID, KeyRings.MASTER_KEY_ID,
KeyRings.IS_EXPIRED, KeyRings.IS_EXPIRED,
@@ -80,35 +91,50 @@ public class OpenPgpService extends Service {
}; };
// do not pre-select revoked or expired keys // 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"; + " = 0 AND " + KeychainContract.KeyRings.IS_EXPIRED + " = 0";
private ApiPermissionHelper mApiPermissionHelper; private ApiPermissionHelper mApiPermissionHelper;
private ProviderHelper mProviderHelper; private ProviderHelper mProviderHelper;
private ApiDataAccessObject mApiDao;
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
mApiPermissionHelper = new ApiPermissionHelper(this); mApiPermissionHelper = new ApiPermissionHelper(this, new ApiDataAccessObject(this));
mProviderHelper = new ProviderHelper(this); mProviderHelper = new ProviderHelper(this);
mApiDao = new ApiDataAccessObject(this);
} }
/** private static class KeyIdResult {
* Search database for key ids based on emails. final Intent mResultIntent;
*/ final HashSet<Long> mKeyIds;
private Intent returnKeyIdsFromEmails(Intent data, String[] encryptionUserIds) {
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 noUserIdsCheck = (encryptionUserIds == null || encryptionUserIds.length == 0);
boolean missingUserIdsCheck = false; boolean missingUserIdsCheck = false;
boolean duplicateUserIdsCheck = false; boolean duplicateUserIdsCheck = false;
ArrayList<Long> keyIds = new ArrayList<>(); HashSet<Long> keyIds = new HashSet<>();
ArrayList<String> missingEmails = new ArrayList<>(); ArrayList<String> missingEmails = new ArrayList<>();
ArrayList<String> duplicateEmails = new ArrayList<>(); ArrayList<String> duplicateEmails = new ArrayList<>();
if (!noUserIdsCheck) { 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 // try to find the key for this specific email
Uri uri = KeyRings.buildUnifiedKeyRingsFindByEmailUri(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 { try {
// result should be one entry containing the key id // result should be one entry containing the key id
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
@@ -137,15 +163,17 @@ public class OpenPgpService extends Service {
} }
} }
// convert ArrayList<Long> to long[] if (isOpportunistic && (noUserIdsCheck || missingUserIdsCheck)) {
long[] keyIdsArray = new long[keyIds.size()]; Intent result = new Intent();
for (int i = 0; i < keyIdsArray.length; i++) { result.putExtra(OpenPgpApi.RESULT_ERROR,
keyIdsArray[i] = keyIds.get(i); 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) { 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()); ApiPendingIntentFactory piFactory = new ApiPendingIntentFactory(getBaseContext());
PendingIntent pi = piFactory.createSelectPublicKeyPendingIntent(data, keyIdsArray, PendingIntent pi = piFactory.createSelectPublicKeyPendingIntent(data, keyIdsArray,
missingEmails, duplicateEmails, noUserIdsCheck); missingEmails, duplicateEmails, noUserIdsCheck);
@@ -154,19 +182,15 @@ public class OpenPgpService extends Service {
Intent result = new Intent(); Intent result = new Intent();
result.putExtra(OpenPgpApi.RESULT_INTENT, pi); result.putExtra(OpenPgpApi.RESULT_INTENT, pi);
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED);
return result; return new KeyIdResult(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!");
}
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, private Intent signImpl(Intent data, InputStream inputStream,
@@ -280,20 +304,31 @@ public class OpenPgpService extends Service {
compressionId = PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.UNCOMPRESSED; compressionId = PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags.UNCOMPRESSED;
} }
// first try to get key ids from non-ambiguous key id extra long[] keyIds;
long[] keyIds = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS); {
if (keyIds == null) { 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 (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0) == OpenPgpApi.RESULT_CODE_SUCCESS) { // get key ids based on given user ids
keyIds = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS); if (data.hasExtra(OpenPgpApi.EXTRA_USER_IDS)) {
} else { String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_USER_IDS);
// if not success -> result contains a PendingIntent for user interaction boolean isOpportunistic = data.getBooleanExtra(OpenPgpApi.EXTRA_OPPORTUNISTIC_ENCRYPTION, false);
return result; // give params through to activity...
KeyIdResult result = returnKeyIdsFromEmails(data, userIds, isOpportunistic);
if (result.mResultIntent != null) {
return result.mResultIntent;
}
encryptKeyIds.addAll(result.mKeyIds);
} }
// 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! // TODO this is not correct!
@@ -305,8 +340,7 @@ public class OpenPgpService extends Service {
.setVersionHeader(null) .setVersionHeader(null)
.setCompressionAlgorithm(compressionId) .setCompressionAlgorithm(compressionId)
.setSymmetricEncryptionAlgorithm(PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT) .setSymmetricEncryptionAlgorithm(PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT)
.setEncryptionMasterKeyIds(keyIds) .setEncryptionMasterKeyIds(keyIds);
.setFailOnMissingEncryptionKeyIds(true);
if (sign) { if (sign) {
@@ -405,11 +439,11 @@ public class OpenPgpService extends Service {
} }
String currentPkg = mApiPermissionHelper.getCurrentCallingPackage(); String currentPkg = mApiPermissionHelper.getCurrentCallingPackage();
HashSet<Long> allowedKeyIds = mProviderHelper.getAllowedKeyIdsForApp( HashSet<Long> allowedKeyIds = mApiDao.getAllowedKeyIdsForApp(
KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg)); KeychainContract.ApiAllowedKeys.buildBaseUri(currentPkg));
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 7) { if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 7) {
allowedKeyIds.addAll(mProviderHelper.getAllKeyIdsForApp( allowedKeyIds.addAll(mApiDao.getAllKeyIdsForApp(
ApiAccounts.buildBaseUri(currentPkg))); ApiAccounts.buildBaseUri(currentPkg)));
} }
@@ -422,6 +456,15 @@ public class OpenPgpService extends Service {
cryptoInput.mPassphrase = cryptoInput.mPassphrase =
new Passphrase(data.getCharArrayExtra(OpenPgpApi.EXTRA_PASSPHRASE)); 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); byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE);
@@ -582,7 +625,8 @@ public class OpenPgpService extends Service {
try { try {
// try to find key, throws NotFoundException if not in db! // try to find key, throws NotFoundException if not in db!
CanonicalizedPublicKeyRing keyRing = CanonicalizedPublicKeyRing keyRing =
mProviderHelper.getCanonicalizedPublicKeyRing(masterKeyId); mProviderHelper.getCanonicalizedPublicKeyRing(
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(masterKeyId));
Intent result = new Intent(); Intent result = new Intent();
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
@@ -669,7 +713,21 @@ public class OpenPgpService extends Service {
} else { } else {
// get key ids based on given user ids // get key ids based on given user ids
String[] userIds = data.getStringArrayExtra(OpenPgpApi.EXTRA_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) { private Intent getSignKeyMasterId(Intent data) {
// NOTE: Accounts are deprecated on API version >= 7 // NOTE: Accounts are deprecated on API version >= 7
if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) < 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! // version code is required and needs to correspond to version code of service!
// History of versions in openpgp-api's CHANGELOG.md // History of versions in openpgp-api's CHANGELOG.md
List<Integer> supportedVersions = Arrays.asList(3, 4, 5, 6, 7, 8, 9, 10); if (!SUPPORTED_VERSIONS.contains(data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1))) {
if (!supportedVersions.contains(data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1))) {
Intent result = new Intent(); Intent result = new Intent();
OpenPgpError error = new OpenPgpError OpenPgpError error = new OpenPgpError
(OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n" (OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n"
+ "used API version: " + data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) + "\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_ERROR, error);
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
return result; return result;
} }
// check if caller is allowed to access OpenKeychain // check if caller is allowed to access OpenKeychain
Intent result = mApiPermissionHelper.isAllowed(data); Intent result = mApiPermissionHelper.isAllowedOrReturnIntent(data);
if (result != null) { if (result != null) {
return result; return result;
} }
@@ -845,6 +922,9 @@ public class OpenPgpService extends Service {
String action = data.getAction(); String action = data.getAction();
switch (action) { switch (action) {
case OpenPgpApi.ACTION_CHECK_PERMISSION: {
return checkPermissionImpl(data);
}
case OpenPgpApi.ACTION_CLEARTEXT_SIGN: { case OpenPgpApi.ACTION_CLEARTEXT_SIGN: {
return signImpl(data, inputStream, outputStream, true); return signImpl(data, inputStream, outputStream, true);
} }
@@ -29,7 +29,7 @@ import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.SingletonResult; 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.remote.AccountSettings;
import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
@@ -98,7 +98,7 @@ public class AccountSettingsActivity extends BaseActivity {
} }
private void loadData(Uri accountUri) { private void loadData(Uri accountUri) {
AccountSettings settings = new ProviderHelper(this).getApiAccountSettings(accountUri); AccountSettings settings = new ApiDataAccessObject(this).getApiAccountSettings(accountUri);
mAccountSettingsFragment.setAccSettings(settings); mAccountSettingsFragment.setAccSettings(settings);
} }
@@ -110,7 +110,7 @@ public class AccountSettingsActivity extends BaseActivity {
} }
private void save() { private void save() {
new ProviderHelper(this).updateApiAccount(mAccountUri, mAccountSettingsFragment.getAccSettings()); new ApiDataAccessObject(this).updateApiAccount(mAccountUri, mAccountSettingsFragment.getAccSettings());
SingletonResult result = new SingletonResult( SingletonResult result = new SingletonResult(
SingletonResult.RESULT_OK, LogType.MSG_ACC_SAVED); SingletonResult.RESULT_OK, LogType.MSG_ACC_SAVED);
Intent intent = new Intent(); Intent intent = new Intent();
@@ -37,8 +37,8 @@ import org.bouncycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.remote.AppSettings; import org.sufficientlysecure.keychain.remote.AppSettings;
import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.dialog.AdvancedAppSettingsDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.AdvancedAppSettingsDialogFragment;
@@ -182,7 +182,7 @@ public class AppSettingsActivity extends BaseActivity {
} }
private void loadData(Bundle savedInstanceState, Uri appUri) { 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 // get application name and icon from package manager
String appName; String appName;
@@ -36,8 +36,8 @@ import android.widget.ListView;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround; import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract; 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.KeyAdapter;
import org.sufficientlysecure.keychain.ui.adapter.KeySelectableAdapter; import org.sufficientlysecure.keychain.ui.adapter.KeySelectableAdapter;
import org.sufficientlysecure.keychain.ui.widget.FixedListView; 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 static final String ARG_DATA_URI = "uri";
private KeySelectableAdapter mAdapter; private KeySelectableAdapter mAdapter;
private ProviderHelper mProviderHelper; private ApiDataAccessObject mApiDao;
private Uri mDataUri; private Uri mDataUri;
@@ -69,7 +69,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mProviderHelper = new ProviderHelper(getActivity()); mApiDao = new ApiDataAccessObject(getActivity());
} }
@Override @Override
@@ -107,7 +107,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
// application this would come from a resource. // application this would come from a resource.
setEmptyText(getString(R.string.list_empty)); 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); mAdapter = new KeySelectableAdapter(getActivity(), null, 0, checked);
setListAdapter(mAdapter); setListAdapter(mAdapter);
getListView().setOnItemClickListener(mAdapter); getListView().setOnItemClickListener(mAdapter);
@@ -141,7 +141,7 @@ public class AppSettingsAllowedKeysListFragment extends ListFragmentWorkaround i
public void saveAllowedKeys() { public void saveAllowedKeys() {
try { try {
mProviderHelper.saveAllowedKeyIdsForApp(mDataUri, getSelectedMasterKeyIds()); mApiDao.saveAllowedKeyIdsForApp(mDataUri, getSelectedMasterKeyIds());
} catch (RemoteException | OperationApplicationException e) { } catch (RemoteException | OperationApplicationException e) {
Log.e(Constants.TAG, "Problem saving allowed key ids!", e); Log.e(Constants.TAG, "Problem saving allowed key ids!", e);
} }
@@ -17,6 +17,7 @@
package org.sufficientlysecure.keychain.remote.ui; package org.sufficientlysecure.keychain.remote.ui;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
@@ -25,8 +26,8 @@ import android.widget.TextView;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.remote.AccountSettings; import org.sufficientlysecure.keychain.remote.AccountSettings;
import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.util.Notify; 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 packageName = extras.getString(EXTRA_PACKAGE_NAME);
final String accName = extras.getString(EXTRA_ACC_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( mAccSettingsFragment = (AccountSettingsFragment) getSupportFragmentManager().findFragmentById(
R.id.api_account_settings_fragment); R.id.api_account_settings_fragment);
@@ -65,7 +66,7 @@ public class RemoteCreateAccountActivity extends BaseActivity {
// update existing? // update existing?
Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(packageName, accName); Uri uri = KeychainContract.ApiAccounts.buildByPackageAndAccountUri(packageName, accName);
AccountSettings settings = providerHelper.getApiAccountSettings(uri); AccountSettings settings = apiDao.getApiAccountSettings(uri);
if (settings == null) { if (settings == null) {
// create new account // create new account
settings = new AccountSettings(accName); settings = new AccountSettings(accName);
@@ -94,11 +95,11 @@ public class RemoteCreateAccountActivity extends BaseActivity {
if (mUpdateExistingAccount) { if (mUpdateExistingAccount) {
Uri baseUri = KeychainContract.ApiAccounts.buildBaseUri(packageName); Uri baseUri = KeychainContract.ApiAccounts.buildBaseUri(packageName);
Uri accountUri = baseUri.buildUpon().appendEncodedPath(accName).build(); Uri accountUri = baseUri.buildUpon().appendEncodedPath(accName).build();
providerHelper.updateApiAccount( apiDao.updateApiAccount(
accountUri, accountUri,
mAccSettingsFragment.getAccSettings()); mAccSettingsFragment.getAccSettings());
} else { } else {
providerHelper.insertApiAccount( apiDao.insertApiAccount(
KeychainContract.ApiAccounts.buildBaseUri(packageName), KeychainContract.ApiAccounts.buildBaseUri(packageName),
mAccSettingsFragment.getAccSettings()); mAccSettingsFragment.getAccSettings());
} }
@@ -17,13 +17,14 @@
package org.sufficientlysecure.keychain.remote.ui; package org.sufficientlysecure.keychain.remote.ui;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; 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.remote.AppSettings;
import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
@@ -52,7 +53,7 @@ public class RemoteRegisterActivity extends BaseActivity {
final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE); final byte[] packageSignature = extras.getByteArray(EXTRA_PACKAGE_SIGNATURE);
Log.d(Constants.TAG, "ACTION_REGISTER packageName: " + packageName); Log.d(Constants.TAG, "ACTION_REGISTER packageName: " + packageName);
final ProviderHelper providerHelper = new ProviderHelper(this); final ApiDataAccessObject apiDao = new ApiDataAccessObject(this);
mAppSettingsHeaderFragment = (AppSettingsHeaderFragment) getSupportFragmentManager().findFragmentById( mAppSettingsHeaderFragment = (AppSettingsHeaderFragment) getSupportFragmentManager().findFragmentById(
R.id.api_app_settings_fragment); R.id.api_app_settings_fragment);
@@ -67,8 +68,7 @@ public class RemoteRegisterActivity extends BaseActivity {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
// Allow // Allow
apiDao.insertApiApp(mAppSettingsHeaderFragment.getAppSettings());
providerHelper.insertApiApp(mAppSettingsHeaderFragment.getAppSettings());
// give data through for new service call // give data through for new service call
Intent resultData = extras.getParcelable(EXTRA_DATA); Intent resultData = extras.getParcelable(EXTRA_DATA);
@@ -17,6 +17,7 @@
package org.sufficientlysecure.keychain.remote.ui; package org.sufficientlysecure.keychain.remote.ui;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -36,9 +37,9 @@ import org.openintents.openpgp.util.OpenPgpApi;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround; import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
import org.sufficientlysecure.keychain.provider.ApiDataAccessObject;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; 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.adapter.SelectKeyCursorAdapter;
import org.sufficientlysecure.keychain.ui.widget.FixedListView; import org.sufficientlysecure.keychain.ui.widget.FixedListView;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
@@ -49,7 +50,7 @@ public class SelectSignKeyIdListFragment extends ListFragmentWorkaround implemen
public static final String ARG_DATA = "data"; public static final String ARG_DATA = "data";
private SelectKeyCursorAdapter mAdapter; private SelectKeyCursorAdapter mAdapter;
private ProviderHelper mProviderHelper; private ApiDataAccessObject mApiDao;
private Uri mDataUri; private Uri mDataUri;
@@ -72,7 +73,7 @@ public class SelectSignKeyIdListFragment extends ListFragmentWorkaround implemen
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mProviderHelper = new ProviderHelper(getActivity()); mApiDao = new ApiDataAccessObject(getActivity());
} }
@Override @Override
@@ -116,7 +117,7 @@ public class SelectSignKeyIdListFragment extends ListFragmentWorkaround implemen
Uri allowedKeysUri = mDataUri.buildUpon().appendPath(KeychainContract.PATH_ALLOWED_KEYS).build(); Uri allowedKeysUri = mDataUri.buildUpon().appendPath(KeychainContract.PATH_ALLOWED_KEYS).build();
Log.d(Constants.TAG, "allowedKeysUri: " + allowedKeysUri); Log.d(Constants.TAG, "allowedKeysUri: " + allowedKeysUri);
mProviderHelper.addAllowedKeyIdForApp(allowedKeysUri, masterKeyId); mApiDao.addAllowedKeyIdForApp(allowedKeysUri, masterKeyId);
resultData.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, masterKeyId); resultData.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, masterKeyId);
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -143,6 +143,14 @@ public class ContactSyncAdapterService extends Service {
} }
public static void requestContactsSync() { 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(); Bundle extras = new Bundle();
// no need to wait, do it immediately // no need to wait, do it immediately
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
@@ -38,6 +38,7 @@ import org.sufficientlysecure.keychain.operations.BackupOperation;
import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.ImportOperation;
import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation; import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation;
import org.sufficientlysecure.keychain.operations.InputDataOperation; import org.sufficientlysecure.keychain.operations.InputDataOperation;
import org.sufficientlysecure.keychain.operations.ChangeUnlockOperation;
import org.sufficientlysecure.keychain.operations.PromoteKeyOperation; import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
import org.sufficientlysecure.keychain.operations.RevokeOperation; import org.sufficientlysecure.keychain.operations.RevokeOperation;
import org.sufficientlysecure.keychain.operations.SignEncryptOperation; 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); op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof SaveKeyringParcel) { } else if (inputParcel instanceof SaveKeyringParcel) {
op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); 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) { } else if (inputParcel instanceof RevokeKeyringParcel) {
op = new RevokeOperation(outerThis, new ProviderHelper(outerThis), outerThis); op = new RevokeOperation(outerThis, new ProviderHelper(outerThis), outerThis);
} else if (inputParcel instanceof CertifyActionsParcel) { } else if (inputParcel instanceof CertifyActionsParcel) {
@@ -11,11 +11,14 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.PeriodicSync;
import android.content.SyncResult; import android.content.SyncResult;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; 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.operations.results.OperationResult;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.receiver.NetworkReceiver;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity; import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; 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_IGNORE_TOR = "ignore_tor";
private static final String ACTION_UPDATE_ALL = "update_all"; 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_DISMISS_NOTIFICATION = "cancel_sync";
private static final String ACTION_START_ORBOT = "start_orbot"; private static final String ACTION_START_ORBOT = "start_orbot";
private static final String ACTION_CANCEL = "cancel"; private static final String ACTION_CANCEL = "cancel";
@@ -176,8 +180,25 @@ public class KeyserverSyncAdapterService extends Service {
@Override @Override
public void onPerformSync(Account account, Bundle extras, String authority, public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) { 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 PowerManager pm = (PowerManager) KeyserverSyncAdapterService.this
.getSystemService(Context.POWER_SERVICE); .getSystemService(Context.POWER_SERVICE);
@SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20 @SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20
@@ -509,6 +530,10 @@ public class KeyserverSyncAdapterService extends Service {
return builder.build(); 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) { public static void enableKeyserverSync(Context context) {
Account account = KeychainApplication.createAccountIfNecessary(context); Account account = KeychainApplication.createAccountIfNecessary(context);
@@ -519,12 +544,26 @@ public class KeyserverSyncAdapterService extends Service {
ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1); ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY, true); ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY, true);
ContentResolver.addPeriodicSync(
account, boolean intervalChanged = false;
Constants.PROVIDER_AUTHORITY, boolean syncExists = Preferences.getKeyserverSyncEnabled(context);
new Bundle(),
SYNC_INTERVAL 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() { private boolean isSyncEnabled() {
@@ -49,8 +49,6 @@ public class SaveKeyringParcel implements Parcelable {
// the key fingerprint, for safety. MUST be null for a new key. // the key fingerprint, for safety. MUST be null for a new key.
public byte[] mFingerprint; public byte[] mFingerprint;
public ChangeUnlockParcel mNewUnlock;
public ArrayList<String> mAddUserIds; public ArrayList<String> mAddUserIds;
public ArrayList<WrappedUserAttribute> mAddUserAttribute; public ArrayList<WrappedUserAttribute> mAddUserAttribute;
public ArrayList<SubkeyAdd> mAddSubKeys; public ArrayList<SubkeyAdd> mAddSubKeys;
@@ -70,6 +68,9 @@ public class SaveKeyringParcel implements Parcelable {
private boolean mUploadAtomic; private boolean mUploadAtomic;
private String mKeyserver; private String mKeyserver;
// private because we have to set other details like key id
private ChangeUnlockParcel mNewUnlock;
public SaveKeyringParcel() { public SaveKeyringParcel() {
reset(); reset();
} }
@@ -102,6 +103,18 @@ public class SaveKeyringParcel implements Parcelable {
mKeyserver = keysever; 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() { public boolean isUpload() {
return mUpload; return mUpload;
} }
@@ -344,64 +357,6 @@ public class SaveKeyringParcel implements Parcelable {
// BRAINPOOL_P256, BRAINPOOL_P384, BRAINPOOL_P512 // 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 + ")");
}
}
} }
@@ -17,18 +17,18 @@
package org.sufficientlysecure.keychain.service.input; package org.sufficientlysecure.keychain.service.input;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.ParcelableProxy;
import org.sufficientlysecure.keychain.util.Passphrase; 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;
/** /**
* This is a base class for the input of crypto operations. * This is a base class for the input of crypto operations.
*/ */
@@ -14,8 +14,8 @@ import java.util.Date;
public class RequiredInputParcel implements Parcelable { public class RequiredInputParcel implements Parcelable {
public enum RequiredInputType { public enum RequiredInputType {
PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, NFC_SIGN, NFC_DECRYPT, PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, SECURITY_TOKEN_SIGN, SECURITY_TOKEN_DECRYPT,
NFC_MOVE_KEY_TO_CARD, NFC_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY, SECURITY_TOKEN_MOVE_KEY_TO_CARD, SECURITY_TOKEN_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY,
} }
public Date mSignatureTime; public Date mSignatureTime;
@@ -89,22 +89,22 @@ public class RequiredInputParcel implements Parcelable {
return new RequiredInputParcel(RequiredInputType.ENABLE_ORBOT, null, null, null, 0L, 0L); return new RequiredInputParcel(RequiredInputType.ENABLE_ORBOT, null, null, null, 0L, 0L);
} }
public static RequiredInputParcel createNfcSignOperation( public static RequiredInputParcel createSecurityTokenSignOperation(
long masterKeyId, long subKeyId, long masterKeyId, long subKeyId,
byte[] inputHash, int signAlgo, Date signatureTime) { 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 }, new byte[][] { inputHash }, new int[] { signAlgo },
signatureTime, masterKeyId, subKeyId); signatureTime, masterKeyId, subKeyId);
} }
public static RequiredInputParcel createNfcDecryptOperation( public static RequiredInputParcel createSecurityTokenDecryptOperation(
long masterKeyId, long subKeyId, byte[] encryptedSessionKey) { 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); new byte[][] { encryptedSessionKey }, null, null, masterKeyId, subKeyId);
} }
public static RequiredInputParcel createNfcReset() { public static RequiredInputParcel createSecurityTokenReset() {
return new RequiredInputParcel(RequiredInputType.NFC_RESET_CARD, return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_RESET_CARD,
null, null, null, null, null); null, null, null, null, null);
} }
@@ -188,14 +188,14 @@ public class RequiredInputParcel implements Parcelable {
} }
}; };
public static class NfcSignOperationsBuilder { public static class SecurityTokenSignOperationsBuilder {
Date mSignatureTime; Date mSignatureTime;
ArrayList<Integer> mSignAlgos = new ArrayList<>(); ArrayList<Integer> mSignAlgos = new ArrayList<>();
ArrayList<byte[]> mInputHashes = new ArrayList<>(); ArrayList<byte[]> mInputHashes = new ArrayList<>();
long mMasterKeyId; long mMasterKeyId;
long mSubKeyId; long mSubKeyId;
public NfcSignOperationsBuilder(Date signatureTime, long masterKeyId, long subKeyId) { public SecurityTokenSignOperationsBuilder(Date signatureTime, long masterKeyId, long subKeyId) {
mSignatureTime = signatureTime; mSignatureTime = signatureTime;
mMasterKeyId = masterKeyId; mMasterKeyId = masterKeyId;
mSubKeyId = subKeyId; mSubKeyId = subKeyId;
@@ -209,7 +209,7 @@ public class RequiredInputParcel implements Parcelable {
signAlgos[i] = mSignAlgos.get(i); signAlgos[i] = mSignAlgos.get(i);
} }
return new RequiredInputParcel(RequiredInputType.NFC_SIGN, return new RequiredInputParcel(RequiredInputType.SECURITY_TOKEN_SIGN,
inputHashes, signAlgos, mSignatureTime, mMasterKeyId, mSubKeyId); inputHashes, signAlgos, mSignatureTime, mMasterKeyId, mSubKeyId);
} }
@@ -222,7 +222,7 @@ public class RequiredInputParcel implements Parcelable {
if (!mSignatureTime.equals(input.mSignatureTime)) { if (!mSignatureTime.equals(input.mSignatureTime)) {
throw new AssertionError("input times must match, this is a programming error!"); 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!"); 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<>(); ArrayList<byte[]> mSubkeysToExport = new ArrayList<>();
Long mMasterKeyId; Long mMasterKeyId;
byte[] mPin; byte[] mPin;
byte[] mAdminPin; byte[] mAdminPin;
public NfcKeyToCardOperationsBuilder(Long masterKeyId) { public SecurityTokenKeyToCardOperationsBuilder(Long masterKeyId) {
mMasterKeyId = masterKeyId; mMasterKeyId = masterKeyId;
} }
@@ -264,7 +264,7 @@ public class RequiredInputParcel implements Parcelable {
ByteBuffer buf = ByteBuffer.wrap(mSubkeysToExport.get(0)); ByteBuffer buf = ByteBuffer.wrap(mSubkeysToExport.get(0));
// We need to pass in a subkey here... // 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()); inputData, null, null, mMasterKeyId, buf.getLong());
} }
@@ -287,7 +287,7 @@ public class RequiredInputParcel implements Parcelable {
if (!mMasterKeyId.equals(input.mMasterKeyId)) { if (!mMasterKeyId.equals(input.mMasterKeyId)) {
throw new AssertionError("Master keys must match, this is a programming error!"); 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!"); throw new AssertionError("Operation types must match, this is a programming error!");
} }
@@ -18,14 +18,6 @@
package org.sufficientlysecure.keychain.ui; 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.ArgbEvaluator;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener; 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;
import android.support.v4.app.FragmentManager.OnBackStackChangedListener; import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
import android.text.Editable; import android.text.Editable;
import android.text.InputType; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@@ -53,12 +45,9 @@ import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator; import android.view.animation.AccelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import com.github.pinball83.maskededittext.MaskedEditText;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.ExportResult; 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.FileHelper;
import org.sufficientlysecure.keychain.util.Passphrase; 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> public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringParcel, ExportResult>
implements OnBackStackChangedListener { implements OnBackStackChangedListener {
@@ -100,7 +97,7 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
String mBackupCode; String mBackupCode;
private boolean mExecuteBackupOperation; private boolean mExecuteBackupOperation;
private MaskedEditText mCodeEditText; private EditText[] mCodeEditText;
private ToolableViewAnimator mStatusAnimator, mTitleAnimator, mCodeFieldsAnimator; private ToolableViewAnimator mStatusAnimator, mTitleAnimator, mCodeFieldsAnimator;
private Integer mBackStackLevel; private Integer mBackStackLevel;
@@ -152,8 +149,13 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
boolean newCheckedState = !item.isChecked(); boolean newCheckedState = !item.isChecked();
item.setChecked(newCheckedState); item.setChecked(newCheckedState);
mDebugModeAcceptAnyCode = newCheckedState; mDebugModeAcceptAnyCode = newCheckedState;
if (newCheckedState) { if (newCheckedState && TextUtils.isEmpty(mCodeEditText[0].getText())) {
mCodeEditText.setText("ABCD-EFGH-IJKL-MNOP-QRST-UVWX"); 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(); Notify.create(getActivity(), "Actual backup code is all 'A's", Style.WARN).show();
} }
return true; return true;
@@ -177,11 +179,9 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
mTitleAnimator.setDisplayedChild(1, animate); mTitleAnimator.setDisplayedChild(1, animate);
mStatusAnimator.setDisplayedChild(1, animate); mStatusAnimator.setDisplayedChild(1, animate);
mCodeFieldsAnimator.setDisplayedChild(1, animate); mCodeFieldsAnimator.setDisplayedChild(1, animate);
// use non-breaking spaces to enlarge the empty EditText appropriately for (EditText editText : mCodeEditText) {
String empty = "\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0" + editText.setText("");
"-\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0" + }
"-\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0";
mCodeEditText.setText(empty);
pushBackStackEntry(); pushBackStackEntry();
@@ -195,7 +195,7 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
hideKeyboard(); hideKeyboard();
if (animate) { if (animate) {
@ColorInt int black = mCodeEditText.getCurrentTextColor(); @ColorInt int black = mCodeEditText[0].getCurrentTextColor();
@ColorInt int red = getResources().getColor(R.color.android_red_dark); @ColorInt int red = getResources().getColor(R.color.android_red_dark);
animateFlashText(mCodeEditText, black, red, false); animateFlashText(mCodeEditText, black, red, false);
} }
@@ -214,14 +214,18 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
hideKeyboard(); hideKeyboard();
mCodeEditText.setEnabled(false); for (EditText editText : mCodeEditText) {
editText.setEnabled(false);
}
@ColorInt int green = getResources().getColor(R.color.android_green_dark); @ColorInt int green = getResources().getColor(R.color.android_green_dark);
if (animate) { if (animate) {
@ColorInt int black = mCodeEditText.getCurrentTextColor(); @ColorInt int black = mCodeEditText[0].getCurrentTextColor();
animateFlashText(mCodeEditText, black, green, true); animateFlashText(mCodeEditText, black, green, true);
} else { } else {
mCodeEditText.setTextColor(green); for (TextView textView : mCodeEditText) {
textView.setTextColor(green);
}
} }
popBackStackNoAction(); popBackStackNoAction();
@@ -257,23 +261,39 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
mExportSecret = args.getBoolean(ARG_EXPORT_SECRET); mExportSecret = args.getBoolean(ARG_EXPORT_SECRET);
mExecuteBackupOperation = args.getBoolean(ARG_EXECUTE_BACKUP_OPERATION, true); mExecuteBackupOperation = args.getBoolean(ARG_EXECUTE_BACKUP_OPERATION, true);
// NOTE: order of these method calls matter, see setupAutomaticLinebreak() mCodeEditText = new EditText[6];
mCodeEditText = (MaskedEditText) view.findViewById(R.id.backup_code_input); mCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1);
mCodeEditText.setInputType( mCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2);
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); mCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3);
setupAutomaticLinebreak(mCodeEditText); mCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4);
mCodeEditText.setImeOptions(EditorInfo.IME_ACTION_DONE); 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!
for (TextView textView : codeDisplayText) {
// noinspection deprecation, setBackground(Drawable) is API level >=16
textView.setBackgroundDrawable(null);
}
}
setupEditTextFocusNext(mCodeEditText);
setupEditTextSuccessListener(mCodeEditText); setupEditTextSuccessListener(mCodeEditText);
TextView codeDisplayText = (TextView) view.findViewById(R.id.backup_code_display);
setupAutomaticLinebreak(codeDisplayText);
// set background to null in TextViews - this will retain padding from EditText style!
// noinspection deprecation, setBackground(Drawable) is API level >=16
codeDisplayText.setBackgroundDrawable(null);
codeDisplayText.setText(mBackupCode);
mStatusAnimator = (ToolableViewAnimator) view.findViewById(R.id.status_animator); mStatusAnimator = (ToolableViewAnimator) view.findViewById(R.id.status_animator);
mTitleAnimator = (ToolableViewAnimator) view.findViewById(R.id.title_animator); mTitleAnimator = (ToolableViewAnimator) view.findViewById(R.id.title_animator);
mCodeFieldsAnimator = (ToolableViewAnimator) view.findViewById(R.id.code_animator); mCodeFieldsAnimator = (ToolableViewAnimator) view.findViewById(R.id.code_animator);
@@ -350,67 +370,76 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
outState.putInt(ARG_BACK_STACK, mBackStackLevel == null ? -1 : mBackStackLevel); outState.putInt(ARG_BACK_STACK, mBackStackLevel == null ? -1 : mBackStackLevel);
} }
/** private void setupEditTextSuccessListener(final EditText[] backupCodes) {
* Automatic line break with max 6 lines for smaller displays for (EditText backupCode : backupCodes) {
* <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 MaskedEditText backupCode) { backupCode.addTextChangedListener(new TextWatcher() {
backupCode.addTextChangedListener(new TextWatcher() { @Override
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
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;
} }
checkIfCodeIsCorrect(backupCode); @Override
} public void onTextChanged(CharSequence s, int start, int before, int count) {
}); }
@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();
}
});
}
} }
private void checkIfCodeIsCorrect(EditText backupCode) { private void checkIfCodeIsCorrect() {
if (Constants.DEBUG && mDebugModeAcceptAnyCode) { if (Constants.DEBUG && mDebugModeAcceptAnyCode) {
switchState(BackupCodeState.STATE_OK, true); switchState(BackupCodeState.STATE_OK, true);
return; 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); switchState(BackupCodeState.STATE_OK, true);
return; return;
} }
switchState(BackupCodeState.STATE_INPUT_ERROR, true); switchState(BackupCodeState.STATE_INPUT_ERROR, true);
} }
private static void animateFlashText( 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); ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), color1, color2);
anim.addUpdateListener(new AnimatorUpdateListener() { anim.addUpdateListener(new AnimatorUpdateListener() {
@Override @Override
public void onAnimationUpdate(ValueAnimator animator) { public void onAnimationUpdate(ValueAnimator animator) {
textView.setTextColor((Integer) animator.getAnimatedValue()); for (TextView textView : textViews) {
textView.setTextColor((Integer) animator.getAnimatedValue());
}
} }
}); });
anim.setRepeatMode(ValueAnimator.REVERSE); 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() { private void pushBackStackEntry() {
if (mBackStackLevel != null) { if (mBackStackLevel != null) {
return; return;
@@ -28,7 +28,8 @@ import org.sufficientlysecure.keychain.ui.base.BaseActivity;
public class CertifyKeyActivity extends BaseActivity { public class CertifyKeyActivity extends BaseActivity {
public static final String EXTRA_RESULT = "operation_result"; 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"; public static final String EXTRA_CERTIFY_KEY_ID = "certify_key_id";
@Override @Override
@@ -62,58 +62,26 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
public class CertifyKeyFragment public class CertifyKeyFragment
extends CachingCryptoOperationFragment<CertifyActionsParcel, CertifyResult> extends CachingCryptoOperationFragment<CertifyActionsParcel, CertifyResult> {
implements LoaderManager.LoaderCallbacks<Cursor> {
public static final String ARG_CHECK_STATES = "check_states";
private CheckBox mUploadKeyCheckbox; private CheckBox mUploadKeyCheckbox;
ListView mUserIds;
private CertifyKeySpinner mCertifyKeySpinner; private CertifyKeySpinner mCertifyKeySpinner;
private long[] mPubMasterKeyIds; private MultiUserIdsFragment mMultiUserIdsFragment;
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;
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
mPubMasterKeyIds = getActivity().getIntent().getLongArrayExtra(CertifyKeyActivity.EXTRA_KEY_IDS); if (savedInstanceState == null) {
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;
// preselect certify key id if given // preselect certify key id if given
long certifyKeyId = getActivity().getIntent() long certifyKeyId = getActivity().getIntent()
.getLongExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, Constants.key.none); .getLongExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, Constants.key.none);
if (certifyKeyId != Constants.key.none) { if (certifyKeyId != Constants.key.none) {
try { try {
CachedPublicKeyRing key = (new ProviderHelper(getActivity())).getCachedPublicKeyRing(certifyKeyId); CachedPublicKeyRing key = (new ProviderHelper(getActivity()))
.getCachedPublicKeyRing(certifyKeyId);
if (key.canCertify()) { if (key.canCertify()) {
mCertifyKeySpinner.setPreSelectedKeyId(certifyKeyId); mCertifyKeySpinner.setPreSelectedKeyId(certifyKeyId);
} }
@@ -121,15 +89,8 @@ public class CertifyKeyFragment
Log.e(Constants.TAG, "certify certify check failed", e); 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); OperationResult result = getActivity().getIntent().getParcelableExtra(CertifyKeyActivity.EXTRA_RESULT);
if (result != null) { if (result != null) {
// display result from import // display result from import
@@ -137,22 +98,14 @@ 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 @Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.certify_key_fragment, null); View view = inflater.inflate(R.layout.certify_key_fragment, null);
mCertifyKeySpinner = (CertifyKeySpinner) view.findViewById(R.id.certify_key_spinner); mCertifyKeySpinner = (CertifyKeySpinner) view.findViewById(R.id.certify_key_spinner);
mUploadKeyCheckbox = (CheckBox) view.findViewById(R.id.sign_key_upload_checkbox); 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 // make certify image gray, like action icons
ImageView vActionCertifyImage = ImageView vActionCertifyImage =
@@ -183,128 +136,11 @@ public class CertifyKeyFragment
return view; return view;
} }
@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 @Override
public CertifyActionsParcel createOperationInput() { public CertifyActionsParcel createOperationInput() {
// Bail out if there is not at least one user id selected // 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()) { if (certifyActions.isEmpty()) {
Notify.create(getActivity(), "No identities selected!", Notify.create(getActivity(), "No identities selected!",
Notify.Style.ERROR).show(); Notify.Style.ERROR).show();
@@ -21,6 +21,7 @@ import android.content.Intent;
import android.nfc.NfcAdapter; import android.nfc.NfcAdapter;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction; import android.support.v4.app.FragmentTransaction;
import org.sufficientlysecure.keychain.R; 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.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper; 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.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.Preferences;
@@ -36,7 +37,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; 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_NAME = "name";
public static final String EXTRA_EMAIL = "email"; 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_PIN = "yubi_key_pin";
public static final String EXTRA_SECURITY_TOKEN_ADMIN_PIN = "yubi_key_admin_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_SECURITY_TOKEN_USER_ID = "nfc_user_id";
public static final String EXTRA_NFC_AID = "nfc_aid"; public static final String EXTRA_SECURITY_TOKEN_AID = "nfc_aid";
public static final String EXTRA_NFC_FINGERPRINTS = "nfc_fingerprints"; public static final String EXTRA_SECURITY_FINGERPRINTS = "nfc_fingerprints";
public static final String FRAGMENT_TAG = "currentFragment"; public static final String FRAGMENT_TAG = "currentFragment";
@@ -66,8 +67,8 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
byte[] mScannedFingerprints; byte[] mScannedFingerprints;
byte[] mNfcAid; byte[] mSecurityTokenAid;
String mNfcUserId; String mSecurityTokenUserId;
@Override @Override
public void onCreate(Bundle savedInstanceState) { 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 // NOTE: ACTION_NDEF_DISCOVERED and not ACTION_TAG_DISCOVERED like in BaseNfcActivity
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) { if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
mTagDispatcher.interceptIntent(getIntent()); mNfcTagDispatcher.interceptIntent(getIntent());
setTitle(R.string.title_manage_my_keys); setTitle(R.string.title_manage_my_keys);
@@ -107,10 +108,10 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
mFirstTime = intent.getBooleanExtra(EXTRA_FIRST_TIME, false); mFirstTime = intent.getBooleanExtra(EXTRA_FIRST_TIME, false);
mCreateSecurityToken = intent.getBooleanExtra(EXTRA_CREATE_SECURITY_TOKEN, false); mCreateSecurityToken = intent.getBooleanExtra(EXTRA_CREATE_SECURITY_TOKEN, false);
if (intent.hasExtra(EXTRA_NFC_FINGERPRINTS)) { if (intent.hasExtra(EXTRA_SECURITY_FINGERPRINTS)) {
byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_NFC_FINGERPRINTS); byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_SECURITY_FINGERPRINTS);
String nfcUserId = intent.getStringExtra(EXTRA_NFC_USER_ID); String nfcUserId = intent.getStringExtra(EXTRA_SECURITY_TOKEN_USER_ID);
byte[] nfcAid = intent.getByteArrayExtra(EXTRA_NFC_AID); byte[] nfcAid = intent.getByteArrayExtra(EXTRA_SECURITY_TOKEN_AID);
if (containsKeys(nfcFingerprints)) { if (containsKeys(nfcFingerprints)) {
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance( Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
@@ -143,24 +144,32 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
} }
@Override @Override
protected void doNfcInBackground() throws IOException { protected void doSecurityTokenInBackground() throws IOException {
if (mCurrentFragment instanceof NfcListenerFragment) { if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
((NfcListenerFragment) mCurrentFragment).doNfcInBackground(); ((SecurityTokenListenerFragment) mCurrentFragment).doSecurityTokenInBackground();
return; return;
} }
mScannedFingerprints = nfcGetFingerprints(); mScannedFingerprints = mSecurityTokenHelper.getFingerprints();
mNfcAid = nfcGetAid(); mSecurityTokenAid = mSecurityTokenHelper.getAid();
mNfcUserId = nfcGetUserId(); mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
} }
@Override @Override
protected void onNfcPostExecute() { protected void onSecurityTokenPostExecute() {
if (mCurrentFragment instanceof NfcListenerFragment) { if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
((NfcListenerFragment) mCurrentFragment).onNfcPostExecute(); ((SecurityTokenListenerFragment) mCurrentFragment).onSecurityTokenPostExecute();
return; 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)) { if (containsKeys(mScannedFingerprints)) {
try { try {
long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mScannedFingerprints); long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mScannedFingerprints);
@@ -169,15 +178,15 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
Intent intent = new Intent(this, ViewKeyActivity.class); Intent intent = new Intent(this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid); intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId); intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mScannedFingerprints); intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mScannedFingerprints);
startActivity(intent); startActivity(intent);
finish(); finish();
} catch (PgpKeyNotFoundException e) { } catch (PgpKeyNotFoundException e) {
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance( Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
mScannedFingerprints, mNfcAid, mNfcUserId); mScannedFingerprints, mSecurityTokenAid, mSecurityTokenUserId);
loadFragment(frag, FragAction.TO_RIGHT); loadFragment(frag, FragAction.TO_RIGHT);
} }
} else { } else {
@@ -252,12 +261,11 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
// do it immediately! // do it immediately!
getSupportFragmentManager().executePendingTransactions(); getSupportFragmentManager().executePendingTransactions();
} }
interface NfcListenerFragment { interface SecurityTokenListenerFragment {
void doNfcInBackground() throws IOException; void doSecurityTokenInBackground() throws IOException;
void onNfcPostExecute(); void onSecurityTokenPostExecute();
} }
@Override @Override
@@ -44,7 +44,6 @@ import org.sufficientlysecure.keychain.ui.widget.EmailEditText;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
public class CreateKeyEmailFragment extends Fragment { public class CreateKeyEmailFragment extends Fragment {
private CreateKeyActivity mCreateKeyActivity; private CreateKeyActivity mCreateKeyActivity;
@@ -52,10 +51,6 @@ public class CreateKeyEmailFragment extends Fragment {
private ArrayList<EmailAdapter.ViewModel> mAdditionalEmailModels = new ArrayList<>(); private ArrayList<EmailAdapter.ViewModel> mAdditionalEmailModels = new ArrayList<>();
private EmailAdapter mEmailAdapter; 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 * Creates new instance of this fragment
*/ */
@@ -76,16 +71,15 @@ public class CreateKeyEmailFragment extends Fragment {
* @return true if EditText is not empty * @return true if EditText is not empty
*/ */
private boolean isMainEmailValid(EditText editText) { private boolean isMainEmailValid(EditText editText) {
boolean output = true; if (editText.getText().length() == 0) {
if (!checkEmail(editText.getText().toString(), false)) {
editText.setError(getString(R.string.create_key_empty)); editText.setError(getString(R.string.create_key_empty));
editText.requestFocus(); editText.requestFocus();
output = false; return false;
} else { } else if (!checkEmail(editText.getText().toString(), false)){
editText.setError(null); return false;
} }
editText.setError(null);
return output; return true;
} }
@Override @Override
@@ -146,10 +140,9 @@ public class CreateKeyEmailFragment extends Fragment {
* @return * @return
*/ */
private boolean checkEmail(String email, boolean additionalEmail) { private boolean checkEmail(String email, boolean additionalEmail) {
// check for email format or if the user did any input if (email.isEmpty()) {
if (!isEmailFormatValid(email)) {
Notify.create(getActivity(), 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); Notify.LENGTH_LONG, Notify.Style.ERROR).show(CreateKeyEmailFragment.this);
return false; return false;
} }
@@ -166,18 +159,6 @@ public class CreateKeyEmailFragment extends Fragment {
return true; return true;
} }
/**
* 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. * Checks for duplicated emails inside the additional email adapter.
* *
@@ -44,9 +44,9 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; 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.UploadKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
@@ -57,6 +57,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.regex.Pattern;
public class CreateKeyFinalFragment extends Fragment { public class CreateKeyFinalFragment extends Fragment {
@@ -81,6 +82,10 @@ public class CreateKeyFinalFragment extends Fragment {
private OperationResult mQueuedFinishResult; private OperationResult mQueuedFinishResult;
private EditKeyResult mQueuedDisplayResult; 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() { public static CreateKeyFinalFragment newInstance() {
CreateKeyFinalFragment frag = new CreateKeyFinalFragment(); CreateKeyFinalFragment frag = new CreateKeyFinalFragment();
frag.setRetainInstance(true); frag.setRetainInstance(true);
@@ -106,7 +111,11 @@ public class CreateKeyFinalFragment extends Fragment {
CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity(); CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity();
// set values // 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) { if (createKeyActivity.mAdditionalEmails != null && createKeyActivity.mAdditionalEmails.size() > 0) {
String emailText = createKeyActivity.mEmail + ", "; String emailText = createKeyActivity.mEmail + ", ";
Iterator<?> it = createKeyActivity.mAdditionalEmails.iterator(); Iterator<?> it = createKeyActivity.mAdditionalEmails.iterator();
@@ -122,6 +131,8 @@ public class CreateKeyFinalFragment extends Fragment {
mEmailEdit.setText(createKeyActivity.mEmail); mEmailEdit.setText(createKeyActivity.mEmail);
} }
checkEmailValidity();
mCreateButton.setOnClickListener(new View.OnClickListener() { mCreateButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@@ -278,18 +289,20 @@ public class CreateKeyFinalFragment extends Fragment {
2048, null, KeyFlags.AUTHENTICATION, 0L)); 2048, null, KeyFlags.AUTHENTICATION, 0L));
// use empty passphrase // use empty passphrase
saveKeyringParcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase(), null); saveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(new Passphrase()));
} else { } else {
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA, 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, 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, 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 if (createKeyActivity.mPassphrase != null) {
? new ChangeUnlockParcel(createKeyActivity.mPassphrase, null) saveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(createKeyActivity.mPassphrase));
: null; } else {
saveKeyringParcel.setNewUnlock(null);
}
} }
String userId = KeyRing.createUserId( String userId = KeyRing.createUserId(
new KeyRing.UserId(createKeyActivity.mName, createKeyActivity.mEmail, null) new KeyRing.UserId(createKeyActivity.mName, createKeyActivity.mEmail, null)
@@ -309,6 +322,31 @@ public class CreateKeyFinalFragment extends Fragment {
return saveKeyringParcel; 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() { private void createKey() {
CreateKeyActivity activity = (CreateKeyActivity) getActivity(); CreateKeyActivity activity = (CreateKeyActivity) getActivity();
if (activity == null) { if (activity == null) {
@@ -18,13 +18,11 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
@@ -50,27 +48,6 @@ public class CreateKeyNameFragment extends Fragment {
return frag; 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 @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.create_key_name_fragment, container, false); View view = inflater.inflate(R.layout.create_key_name_fragment, container, false);
@@ -109,13 +86,11 @@ public class CreateKeyNameFragment extends Fragment {
} }
private void nextClicked() { private void nextClicked() {
if (isEditTextNotEmpty(getActivity(), mNameEdit)) { // save state
// save state mCreateKeyActivity.mName = mNameEdit.getText().length() == 0 ? null : mNameEdit.getText().toString();
mCreateKeyActivity.mName = mNameEdit.getText().toString();
CreateKeyEmailFragment frag = CreateKeyEmailFragment.newInstance(); CreateKeyEmailFragment frag = CreateKeyEmailFragment.newInstance();
mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
}
} }
} }
@@ -25,7 +25,6 @@ import java.util.ArrayList;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; 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.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; 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.base.QueueingCryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.Preferences;
@@ -51,7 +50,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
public class CreateSecurityTokenImportResetFragment public class CreateSecurityTokenImportResetFragment
extends QueueingCryptoOperationFragment<ImportKeyringParcel, ImportKeyResult> extends QueueingCryptoOperationFragment<ImportKeyringParcel, ImportKeyResult>
implements NfcListenerFragment { implements SecurityTokenListenerFragment {
private static final int REQUEST_CODE_RESET = 0x00005001; private static final int REQUEST_CODE_RESET = 0x00005001;
@@ -231,7 +230,7 @@ public class CreateSecurityTokenImportResetFragment
public void resetCard() { public void resetCard() {
Intent intent = new Intent(getActivity(), SecurityTokenOperationActivity.class); 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_REQUIRED_INPUT, resetP);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, new CryptoInputParcel()); intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, new CryptoInputParcel());
startActivityForResult(intent, REQUEST_CODE_RESET); startActivityForResult(intent, REQUEST_CODE_RESET);
@@ -248,11 +247,11 @@ public class CreateSecurityTokenImportResetFragment
} }
@Override @Override
public void doNfcInBackground() throws IOException { public void doSecurityTokenInBackground() throws IOException {
mTokenFingerprints = mCreateKeyActivity.nfcGetFingerprints(); mTokenFingerprints = mCreateKeyActivity.getSecurityTokenHelper().getFingerprints();
mTokenAid = mCreateKeyActivity.nfcGetAid(); mTokenAid = mCreateKeyActivity.getSecurityTokenHelper().getAid();
mTokenUserId = mCreateKeyActivity.nfcGetUserId(); mTokenUserId = mCreateKeyActivity.getSecurityTokenHelper().getUserId();
byte[] fp = new byte[20]; byte[] fp = new byte[20];
ByteBuffer.wrap(fp).put(mTokenFingerprints, 0, 20); ByteBuffer.wrap(fp).put(mTokenFingerprints, 0, 20);
@@ -260,7 +259,7 @@ public class CreateSecurityTokenImportResetFragment
} }
@Override @Override
public void onNfcPostExecute() { public void onSecurityTokenPostExecute() {
setData(); setData();
@@ -17,22 +17,35 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import android.app.Activity; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.Animation;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
public class CreateSecurityTokenWaitFragment extends Fragment { public class CreateSecurityTokenWaitFragment extends Fragment {
public static boolean sDisableFragmentAnimations = false;
CreateKeyActivity mCreateKeyActivity; CreateKeyActivity mCreateKeyActivity;
View mBackButton; View mBackButton;
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (this.getActivity() instanceof BaseSecurityTokenActivity) {
((BaseSecurityTokenActivity) this.getActivity()).checkDeviceConnection();
}
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.create_security_token_wait_fragment, container, false); View view = inflater.inflate(R.layout.create_security_token_wait_fragment, container, false);
@@ -50,9 +63,22 @@ public class CreateSecurityTokenWaitFragment extends Fragment {
} }
@Override @Override
public void onAttach(Activity activity) { public void onAttach(Context context) {
super.onAttach(activity); super.onAttach(context);
mCreateKeyActivity = (CreateKeyActivity) getActivity(); 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);
}
} }
@@ -35,6 +35,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ListView; import android.widget.ListView;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; 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.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; 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;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.adapter.SubkeysAdapter; import org.sufficientlysecure.keychain.ui.adapter.SubkeysAdapter;
@@ -128,7 +129,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { 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); mUserIdsList = (ListView) view.findViewById(R.id.edit_key_user_ids);
mSubkeysList = (ListView) view.findViewById(R.id.edit_key_keys); mSubkeysList = (ListView) view.findViewById(R.id.edit_key_keys);
@@ -338,10 +339,8 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
Bundle data = message.getData(); Bundle data = message.getData();
// cache new returned passphrase! // cache new returned passphrase!
mSaveKeyringParcel.mNewUnlock = new ChangeUnlockParcel( mSaveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE), (Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE)));
null
);
} }
} }
}; };
@@ -441,50 +440,45 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
} }
break; break;
} }
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: { case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN: {
// TODO: enable later when Admin PIN handling is resolved SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
Notify.create(getActivity(), if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
"This feature will be available in an upcoming OpenKeychain version.", secretKeyType == SecretKeyType.GNU_DUMMY) {
Notify.Style.WARN).show(); Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_stripped, Notify.Style.ERROR)
break; .show();
break;
}
// Activity activity = EditKeyFragment.this.getActivity(); int algorithm = mSubkeysAdapter.getAlgorithm(position);
// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position); if (algorithm != PublicKeyAlgorithmTags.RSA_GENERAL
// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD || && algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT
// secretKeyType == SecretKeyType.GNU_DUMMY) { && algorithm != PublicKeyAlgorithmTags.RSA_SIGN) {
// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR) Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_algo, Notify.Style.ERROR)
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); .show();
// break; break;
// } }
// int algorithm = mSubkeysAdapter.getAlgorithm(position);
// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN if (mSubkeysAdapter.getKeySize(position) != 2048) {
// if (algorithm != 1 && algorithm != 2 && algorithm != 3) { Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_size, Notify.Style.ERROR)
// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR) .show();
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); break;
// break; }
// }
// if (mSubkeysAdapter.getKeySize(position) != 2048) { SubkeyChange change;
// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR) change = mSaveKeyringParcel.getSubkeyChange(keyId);
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); if (change == null) {
// break; mSaveKeyringParcel.mChangeSubKeys.add(
// } new SubkeyChange(keyId, false, true)
// );
// break;
// SubkeyChange change; }
// change = mSaveKeyringParcel.getSubkeyChange(keyId); // toggle
// if (change == null) { change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
// mSaveKeyringParcel.mChangeSubKeys.add( if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
// new SubkeyChange(keyId, false, true) // User had chosen to strip key, but now wants to divert it.
// ); change.mDummyStrip = false;
// break; }
// } 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(); getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad();
@@ -562,15 +556,9 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
} }
private void addSubkey() { private void addSubkey() {
boolean willBeMasterKey; // new subkey will never be a masterkey, as masterkey cannot be removed
if (mSubkeysAdapter != null) {
willBeMasterKey = mSubkeysAdapter.getCount() == 0 && mSubkeysAddedAdapter.getCount() == 0;
} else {
willBeMasterKey = mSubkeysAddedAdapter.getCount() == 0;
}
AddSubkeyDialogFragment addSubkeyDialogFragment = AddSubkeyDialogFragment addSubkeyDialogFragment =
AddSubkeyDialogFragment.newInstance(willBeMasterKey); AddSubkeyDialogFragment.newInstance(false);
addSubkeyDialogFragment addSubkeyDialogFragment
.setOnAlgorithmSelectedListener( .setOnAlgorithmSelectedListener(
new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() { new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() {
@@ -247,10 +247,10 @@ public class EncryptFilesFragment
try { try {
mFilesAdapter.add(inputUri); mFilesAdapter.add(inputUri);
} catch (IOException e) { } catch (IOException e) {
String fileName = FileHelper.getFilename(getActivity(), inputUri);
Notify.create(getActivity(), 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); Notify.Style.ERROR).show(this);
return;
} }
// remove from pending input uris // remove from pending input uris
@@ -729,6 +729,8 @@ public class EncryptFilesFragment
// make sure this is correct at this point // make sure this is correct at this point
mAfterEncryptAction = AfterEncryptAction.SAVE; mAfterEncryptAction = AfterEncryptAction.SAVE;
cryptoOperation(new CryptoInputParcel(new Date())); cryptoOperation(new CryptoInputParcel(new Date()));
} else if (resultCode == Activity.RESULT_CANCELED) {
onCryptoOperationCancelled();
} }
return; return;
} }
@@ -21,6 +21,7 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ViewAnimator; import android.widget.ViewAnimator;
import com.tokenautocomplete.TokenCompleteTextView; import com.tokenautocomplete.TokenCompleteTextView;
@@ -79,9 +80,6 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment {
mSignKeySpinner = (KeySpinner) view.findViewById(R.id.sign); mSignKeySpinner = (KeySpinner) view.findViewById(R.id.sign);
mEncryptKeyView = (EncryptKeyCompletionView) view.findViewById(R.id.recipient_list); mEncryptKeyView = (EncryptKeyCompletionView) view.findViewById(R.id.recipient_list);
mEncryptKeyView.setThreshold(1); // Start working from first character 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); final ViewAnimator vSignatureIcon = (ViewAnimator) view.findViewById(R.id.result_signature_icon);
mSignKeySpinner.setOnKeyChangedListener(new OnKeyChangedListener() { 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; return view;
} }
@@ -131,8 +131,11 @@ public class ImportKeysActivity extends BaseActivity
if (Intent.ACTION_VIEW.equals(action)) { if (Intent.ACTION_VIEW.equals(action)) {
if (FacebookKeyserver.isFacebookHost(dataUri)) { if (FacebookKeyserver.isFacebookHost(dataUri)) {
action = ACTION_IMPORT_KEY_FROM_FACEBOOK; 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; action = ACTION_SEARCH_KEYSERVER_FROM_URL;
} else if ("openpgp4fpr".equalsIgnoreCase(scheme)) {
action = ACTION_IMPORT_KEY_FROM_KEYSERVER;
extras.putString(EXTRA_FINGERPRINT, dataUri.getSchemeSpecificPart());
} else { } else {
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml) // Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
// delegate action to ACTION_IMPORT_KEY // delegate action to ACTION_IMPORT_KEY
@@ -413,11 +416,18 @@ public class ImportKeysActivity extends BaseActivity
intent.putExtra(ImportKeyResult.EXTRA_RESULT, result); intent.putExtra(ImportKeyResult.EXTRA_RESULT, result);
setResult(RESULT_OK, intent); setResult(RESULT_OK, intent);
finish(); finish();
return; } else if (result.isOkNew() || result.isOkUpdated()) {
} // User has successfully imported a key, hide first time dialog
Preferences.getPreferences(this).setFirstTime(false);
result.createNotify(ImportKeysActivity.this) // Close activities opened for importing keys and go to the list of keys
.show((ViewGroup) findViewById(R.id.import_snackbar)); 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));
}
} }
// methods from CryptoOperationHelper.Callback // methods from CryptoOperationHelper.Callback
@@ -17,29 +17,46 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
import org.sufficientlysecure.keychain.pgp.PgpHelper; 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;
import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.FileHelper; 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 { public class ImportKeysFileFragment extends Fragment {
private ImportKeysActivity mImportActivity; private ImportKeysActivity mImportActivity;
private View mBrowse; private View mBrowse;
private View mClipboardButton; 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 * Creates new instance of this fragment
@@ -83,10 +100,10 @@ public class ImportKeysFileFragment extends Fragment {
sendText = clipboardText.toString(); sendText = clipboardText.toString();
sendText = PgpHelper.getPgpKeyContent(sendText); sendText = PgpHelper.getPgpKeyContent(sendText);
if (sendText == null) { if (sendText == null) {
Notify.create(mImportActivity, "Bad data!", Style.ERROR).show(); Notify.create(mImportActivity, R.string.error_bad_data, Style.ERROR).show();
return; 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) { switch (requestCode) {
case REQUEST_CODE_FILE: { case REQUEST_CODE_FILE: {
if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) { if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
mCurrentUri = data.getData();
// load data if (checkAndRequestReadPermission(mCurrentUri)) {
mImportActivity.loadCallback(new ImportKeysListFragment.BytesLoaderState(null, data.getData())); startImportingKeys();
}
} }
break; 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();
}
}
} }
@@ -330,9 +330,16 @@ public class ImportKeysListFragment extends ListFragment implements
} }
public void loadNew(LoaderState loaderState) { public void loadNew(LoaderState loaderState) {
mLoaderState = loaderState; mLoaderState = loaderState;
if (mLoaderState instanceof BytesLoaderState) {
BytesLoaderState ls = (BytesLoaderState) mLoaderState;
if ( ls.mDataUri != null && ! checkAndRequestReadPermission(ls.mDataUri)) {
return;
}
}
restartLoaders(); restartLoaders();
} }
@@ -40,11 +40,11 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment; 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.FabContainer;
import org.sufficientlysecure.keychain.util.Preferences; 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_KEYS = 1;
static final int ID_ENCRYPT_DECRYPT = 2; static final int ID_ENCRYPT_DECRYPT = 2;
@@ -90,8 +90,9 @@ public class MainActivity extends BaseSecurityTokenNfcActivity implements FabCon
@Override @Override
public boolean onItemClick(View view, int position, IDrawerItem drawerItem) { public boolean onItemClick(View view, int position, IDrawerItem drawerItem) {
if (drawerItem != null) { if (drawerItem != null) {
PrimaryDrawerItem item = (PrimaryDrawerItem) drawerItem;
Intent intent = null; Intent intent = null;
switch (drawerItem.getIdentifier()) { switch ((int) item.getIdentifier()) {
case ID_KEYS: case ID_KEYS:
onKeysSelected(); onKeysSelected();
break; break;
@@ -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;
}
}
@@ -46,8 +46,6 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.ViewAnimator; import android.widget.ViewAnimator;
import com.github.pinball83.maskededittext.MaskedEditText;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; 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.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
import org.sufficientlysecure.keychain.remote.CryptoInputParcelCacheService;
import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; 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 { public static class PassphraseDialogFragment extends DialogFragment implements TextView.OnEditorActionListener {
private EditText mPassphraseEditText; private EditText mPassphraseEditText;
private TextView mPassphraseText; private TextView mPassphraseText;
private MaskedEditText mBackupCodeEditText; private EditText[] mBackupCodeEditText;
private boolean mIsCancelled = false; private boolean mIsCancelled = false;
private RequiredInputParcel mRequiredInput; private RequiredInputParcel mRequiredInput;
@@ -184,13 +181,15 @@ public class PassphraseDialogActivity extends FragmentActivity {
View view = inflater.inflate(R.layout.passphrase_dialog_backup_code, null); View view = inflater.inflate(R.layout.passphrase_dialog_backup_code, null);
alert.setView(view); alert.setView(view);
mBackupCodeEditText = (MaskedEditText) view.findViewById(R.id.backup_code); mBackupCodeEditText = new EditText[6];
// NOTE: order of these method calls matter, see setupAutomaticLinebreak() mBackupCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1);
mBackupCodeEditText.setInputType( mBackupCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2);
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); mBackupCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3);
setupAutomaticLinebreak(mBackupCodeEditText); mBackupCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4);
mBackupCodeEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE); mBackupCodeEditText[4] = (EditText) view.findViewById(R.id.backup_code_5);
mBackupCodeEditText.setOnEditorActionListener(this); mBackupCodeEditText[5] = (EditText) view.findViewById(R.id.backup_code_6);
setupEditTextFocusNext(mBackupCodeEditText);
AlertDialog dialog = alert.create(); AlertDialog dialog = alert.create();
dialog.setButton(DialogInterface.BUTTON_POSITIVE, dialog.setButton(DialogInterface.BUTTON_POSITIVE,
@@ -281,28 +280,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
mPassphraseText.setText(message); mPassphraseText.setText(message);
mPassphraseEditText.setHint(hint); mPassphraseEditText.setHint(hint);
// Hack to open keyboard. openKeyboard(mPassphraseEditText);
// 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();
mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE); mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
mPassphraseEditText.setOnEditorActionListener(this); mPassphraseEditText.setOnEditorActionListener(this);
@@ -325,17 +303,62 @@ public class PassphraseDialogActivity extends FragmentActivity {
} }
/** /**
* Automatic line break with max 6 lines for smaller displays * Hack to open keyboard.
* <p/> * This is the only method that I found to work across all Android versions
* NOTE: I was not able to get this behaviour using XML! * http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/
* Looks like the order of these method calls matter, see http://stackoverflow.com/a/11171307 * 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) { private void openKeyboard(final TextView textView) {
textview.setSingleLine(true); textView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
textview.setMaxLines(6); @Override
textview.setHorizontallyScrolling(false); 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 @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
@@ -347,8 +370,17 @@ public class PassphraseDialogActivity extends FragmentActivity {
public void onClick(View v) { public void onClick(View v) {
if (mRequiredInput.mType == RequiredInputType.BACKUP_CODE) { if (mRequiredInput.mType == RequiredInputType.BACKUP_CODE) {
Passphrase passphrase = StringBuilder backupCodeInput = new StringBuilder(26);
new Passphrase(mBackupCodeEditText.getText().toString()); 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); finishCaching(passphrase);
return; return;
@@ -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();
}
}
@@ -3,6 +3,7 @@
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* Copyright (C) 2013-2014 Signe Rüsch * Copyright (C) 2013-2014 Signe Rüsch
* Copyright (C) 2013-2014 Philipp Jakubeit * 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 * 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 * 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.pgp.CanonicalizedSecretKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.securitytoken.KeyType;
import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; 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.ui.util.ThemeChanger;
import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.OrientationUtils; import org.sufficientlysecure.keychain.util.OrientationUtils;
@@ -55,7 +58,7 @@ import nordpol.android.NfcGuideView;
* NFC devices. * NFC devices.
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf * 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_REQUIRED_INPUT = "required_input";
public static final String EXTRA_CRYPTO_INPUT = "crypto_input"; public static final String EXTRA_CRYPTO_INPUT = "crypto_input";
@@ -69,8 +72,6 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
private RequiredInputParcel mRequiredInput; 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; private CryptoInputParcel mInputParcel;
@Override @Override
@@ -137,9 +138,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
private void obtainPassphraseIfRequired() { private void obtainPassphraseIfRequired() {
// obtain passphrase for this subkey // obtain passphrase for this subkey
if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD
&& mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_RESET_CARD) { && mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_RESET_CARD) {
obtainSecurityTokenPin(mRequiredInput); 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 @Override
public void onNfcPreExecute() { public void onSecurityTokenPreExecute() {
// start with indeterminate progress // start with indeterminate progress
vAnimator.setDisplayedChild(1); vAnimator.setDisplayedChild(1);
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.TRANSFERRING); nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.TRANSFERRING);
} }
@Override @Override
protected void doNfcInBackground() throws IOException { protected void doSecurityTokenInBackground() throws IOException {
switch (mRequiredInput.mType) { 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++) { for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] encryptedSessionKey = mRequiredInput.mInputData[i]; byte[] encryptedSessionKey = mRequiredInput.mInputData[i];
byte[] decryptedSessionKey = nfcDecryptSessionKey(encryptedSessionKey); byte[] decryptedSessionKey = mSecurityTokenHelper.decryptSessionKey(encryptedSessionKey);
mInputParcel.addCryptoData(encryptedSessionKey, decryptedSessionKey); mInputParcel.addCryptoData(encryptedSessionKey, decryptedSessionKey);
} }
break; 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); mInputParcel.addSignatureTime(mRequiredInput.mSignatureTime);
for (int i = 0; i < mRequiredInput.mInputData.length; i++) { for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] hash = mRequiredInput.mInputData[i]; byte[] hash = mRequiredInput.mInputData[i];
int algo = mRequiredInput.mSignAlgos[i]; int algo = mRequiredInput.mSignAlgos[i];
byte[] signedHash = nfcCalculateSignature(hash, algo); byte[] signedHash = mSecurityTokenHelper.calculateSignature(hash, algo);
mInputParcel.addCryptoData(hash, signedHash); mInputParcel.addCryptoData(hash, signedHash);
} }
break; 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 // TODO: assume PIN and Admin PIN to be default for this operation
mPin = new Passphrase("123456"); mSecurityTokenHelper.setPin(new Passphrase("123456"));
mAdminPin = new Passphrase("12345678"); mSecurityTokenHelper.setAdminPin(new Passphrase("12345678"));
ProviderHelper providerHelper = new ProviderHelper(this); ProviderHelper providerHelper = new ProviderHelper(this);
CanonicalizedSecretKeyRing secretKeyRing; CanonicalizedSecretKeyRing secretKeyRing;
@@ -202,11 +241,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
long subkeyId = buf.getLong(); long subkeyId = buf.getLong();
CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId); CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId);
byte[] tokenSerialNumber = Arrays.copyOf(mSecurityTokenHelper.getAid(), 16);
long keyGenerationTimestampMillis = key.getCreationTime().getTime();
long keyGenerationTimestamp = keyGenerationTimestampMillis / 1000;
byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
byte[] tokenSerialNumber = Arrays.copyOf(nfcGetAid(), 16);
Passphrase passphrase; Passphrase passphrase;
try { try {
@@ -216,46 +251,20 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
throw new IOException("Unable to get cached passphrase!"); throw new IOException("Unable to get cached passphrase!");
} }
if (key.canSign() || key.canCertify()) { mSecurityTokenHelper.changeKey(key, passphrase);
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.");
}
// TODO: Is this really used anywhere? // TODO: Is this really used anywhere?
mInputParcel.addCryptoData(subkeyBytes, tokenSerialNumber); mInputParcel.addCryptoData(subkeyBytes, tokenSerialNumber);
} }
// change PINs afterwards // change PINs afterwards
nfcModifyPIN(0x81, newPin); mSecurityTokenHelper.modifyPin(0x81, newPin);
nfcModifyPIN(0x83, newAdminPin); mSecurityTokenHelper.modifyPin(0x83, newAdminPin);
break; break;
} }
case NFC_RESET_CARD: { case SECURITY_TOKEN_RESET_CARD: {
nfcResetCard(); mSecurityTokenHelper.resetAndWipeToken();
break; break;
} }
@@ -267,7 +276,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
} }
@Override @Override
protected final void onNfcPostExecute() { protected final void onSecurityTokenPostExecute() {
handleResult(mInputParcel); handleResult(mInputParcel);
// show finish // show finish
@@ -275,28 +284,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.DONE); nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.DONE);
new AsyncTask<Void, Void, Void>() { if (mSecurityTokenHelper.isPersistentConnectionAllowed()) {
@Override // Just close
protected Void doInBackground(Void... params) { finish();
// check all 200ms if Security Token has been taken away } else {
while (true) { new AsyncTask<Void, Void, Void>() {
if (isNfcConnected()) { @Override
try { protected Void doInBackground(Void... params) {
Thread.sleep(200); // check all 200ms if Security Token has been taken away
} catch (InterruptedException ignored) { while (true) {
if (isSecurityTokenConnected()) {
try {
Thread.sleep(200);
} catch (InterruptedException ignored) {
}
} else {
return null;
} }
} else {
return null;
} }
} }
}
@Override @Override
protected void onPostExecute(Void result) { protected void onPostExecute(Void result) {
super.onPostExecute(result); super.onPostExecute(result);
finish(); finish();
} }
}.execute(); }.execute();
}
} }
/** /**
@@ -311,7 +325,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
} }
@Override @Override
protected void onNfcError(String error) { protected void onSecurityTokenError(String error) {
pauseTagHandling(); pauseTagHandling();
vErrorText.setText(error + "\n\n" + getString(R.string.security_token_nfc_try_again_text)); vErrorText.setText(error + "\n\n" + getString(R.string.security_token_nfc_try_again_text));
@@ -321,31 +335,11 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
} }
@Override @Override
public void onNfcPinError(String error) { public void onSecurityTokenPinError(String error) {
onNfcError(error); onSecurityTokenError(error);
// clear (invalid) passphrase // clear (invalid) passphrase
PassphraseCacheService.clearCachedPassphrase( PassphraseCacheService.clearCachedPassphrase(
this, mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId()); 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;
}
} }
@@ -19,8 +19,6 @@
package org.sufficientlysecure.keychain.ui; package org.sufficientlysecure.keychain.ui;
import java.util.List;
import android.Manifest; import android.Manifest;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
@@ -49,6 +47,7 @@ import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.KeychainApplication;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.AppCompatPreferenceActivity; import org.sufficientlysecure.keychain.compatibility.AppCompatPreferenceActivity;
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; 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.Preferences;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
import java.util.List;
public class SettingsActivity extends AppCompatPreferenceActivity { public class SettingsActivity extends AppCompatPreferenceActivity {
public static final int REQUEST_CODE_KEYSERVER_PREF = 0x00007005; 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 { public static class SyncPrefsFragment extends PresetPreferenceFragment {
@@ -422,8 +423,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
super.onResume(); super.onResume();
// this needs to be done in onResume since the user can change sync values from Android // 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 // settings and we need to reflect that change when the user navigates back
AccountManager manager = AccountManager.get(getActivity()); final Account account = KeychainApplication.createAccountIfNecessary(getActivity());
final Account account = manager.getAccountsByType(Constants.ACCOUNT_TYPE)[0];
// for keyserver sync // for keyserver sync
initializeSyncCheckBox( initializeSyncCheckBox(
(SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER), (SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER),
@@ -441,8 +441,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
private void initializeSyncCheckBox(final SwitchPreference syncCheckBox, private void initializeSyncCheckBox(final SwitchPreference syncCheckBox,
final Account account, final Account account,
final String authority) { final String authority) {
boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority) // account is null if it could not be created for some reason
&& checkContactsPermission(authority); boolean syncEnabled =
account != null
&& ContentResolver.getSyncAutomatically(account, authority)
&& checkContactsPermission(authority);
syncCheckBox.setChecked(syncEnabled); syncCheckBox.setChecked(syncEnabled);
setSummary(syncCheckBox, authority, syncEnabled); setSummary(syncCheckBox, authority, syncEnabled);
@@ -464,6 +467,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
return false; return false;
} }
} else { } else {
if (account == null) {
// if account could not be created for some reason,
// we can't have our sync
return false;
}
// disable syncs // disable syncs
ContentResolver.setSyncAutomatically(account, authority, false); ContentResolver.setSyncAutomatically(account, authority, false);
// immediately delete any linked contacts // immediately delete any linked contacts
@@ -40,6 +40,7 @@ import android.widget.TextView;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.dialog.AddEditKeyserverDialogFragment; 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.ItemTouchHelperAdapter;
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperViewHolder; import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperViewHolder;
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperDragCallback; import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperDragCallback;
@@ -312,19 +313,19 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC
public void showAsSelectedKeyserver() { public void showAsSelectedKeyserver() {
isSelectedKeyserver = true; isSelectedKeyserver = true;
selectedServerLabel.setVisibility(View.VISIBLE); selectedServerLabel.setVisibility(View.VISIBLE);
outerLayout.setBackgroundColor(getResources().getColor(R.color.android_green_dark)); outerLayout.setBackgroundColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorPrimaryDark));
} }
public void showAsUnselectedKeyserver() { public void showAsUnselectedKeyserver() {
isSelectedKeyserver = false; isSelectedKeyserver = false;
selectedServerLabel.setVisibility(View.GONE); selectedServerLabel.setVisibility(View.GONE);
outerLayout.setBackgroundColor(Color.WHITE); outerLayout.setBackgroundColor(0);
} }
@Override @Override
public void onItemSelected() { public void onItemSelected() {
selectedServerLabel.setVisibility(View.GONE); selectedServerLabel.setVisibility(View.GONE);
itemView.setBackgroundColor(Color.LTGRAY); itemView.setBackgroundColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorBrightToolbar));
} }
@Override @Override
@@ -31,10 +31,7 @@ import android.widget.Spinner;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.UploadResult; 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;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
@@ -53,7 +50,6 @@ public class UploadKeyActivity extends BaseActivity
// CryptoOperationHelper.Callback vars // CryptoOperationHelper.Callback vars
private String mKeyserver; private String mKeyserver;
private long mMasterKeyId;
private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper; private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper;
@Override @Override
@@ -63,6 +59,10 @@ public class UploadKeyActivity extends BaseActivity
mUploadButton = findViewById(R.id.upload_key_action_upload); mUploadButton = findViewById(R.id.upload_key_action_upload);
mKeyServerSpinner = (Spinner) findViewById(R.id.upload_key_keyserver); 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, ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, Preferences.getPreferences(this) android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
.getKeyServers() .getKeyServers()
@@ -89,15 +89,6 @@ public class UploadKeyActivity extends BaseActivity
return; 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 @Override
@@ -136,7 +127,9 @@ public class UploadKeyActivity extends BaseActivity
@Override @Override
public UploadKeyringParcel createOperationInput() { public UploadKeyringParcel createOperationInput() {
return new UploadKeyringParcel(mKeyserver, mMasterKeyId); long[] masterKeyIds = getIntent().getLongArrayExtra(MultiUserIdsFragment.EXTRA_KEY_IDS);
return new UploadKeyringParcel(mKeyserver, masterKeyIds[0]);
} }
@Override @Override
@@ -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();
}
}
@@ -32,6 +32,7 @@ import android.app.ActivityOptions;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.net.Uri; import android.net.Uri;
import android.nfc.NfcAdapter; import android.nfc.NfcAdapter;
import android.os.AsyncTask; 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.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType; 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.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
@@ -102,7 +103,7 @@ import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.Preferences;
public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements public class ViewKeyActivity extends BaseSecurityTokenActivity implements
LoaderManager.LoaderCallbacks<Cursor>, LoaderManager.LoaderCallbacks<Cursor>,
CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> { CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> {
@@ -129,8 +130,8 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
private String mKeyserver; private String mKeyserver;
private ArrayList<ParcelableKeyRing> mKeyList; private ArrayList<ParcelableKeyRing> mKeyList;
private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper; private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper;
private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mEditOpHelper; private CryptoOperationHelper<ChangeUnlockParcel, EditKeyResult> mEditOpHelper;
private SaveKeyringParcel mSaveKeyringParcel; private ChangeUnlockParcel mChangeUnlockParcel;
private TextView mStatusText; private TextView mStatusText;
private ImageView mStatusImage; private ImageView mStatusImage;
@@ -170,9 +171,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
private byte[] mFingerprint; private byte[] mFingerprint;
private String mFingerprintString; private String mFingerprintString;
private byte[] mNfcFingerprints; private byte[] mSecurityTokenFingerprints;
private String mNfcUserId; private String mSecurityTokenUserId;
private byte[] mNfcAid; private byte[] mSecurityTokenAid;
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@Override @Override
@@ -428,13 +429,11 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
} }
private void changePassword() { private void changePassword() {
mSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint); CryptoOperationHelper.Callback<ChangeUnlockParcel, EditKeyResult> editKeyCallback
= new CryptoOperationHelper.Callback<ChangeUnlockParcel, EditKeyResult>() {
CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> editKeyCallback
= new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() {
@Override @Override
public SaveKeyringParcel createOperationInput() { public ChangeUnlockParcel createOperationInput() {
return mSaveKeyringParcel; return mChangeUnlockParcel;
} }
@Override @Override
@@ -468,9 +467,10 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
Bundle data = message.getData(); Bundle data = message.getData();
// use new passphrase! // use new passphrase!
mSaveKeyringParcel.mNewUnlock = new SaveKeyringParcel.ChangeUnlockParcel( mChangeUnlockParcel = new ChangeUnlockParcel(
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE), mMasterKeyId,
null mFingerprint,
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE)
); );
mEditOpHelper.cryptoOperation(); mEditOpHelper.cryptoOperation();
@@ -646,17 +646,17 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
} }
@Override @Override
protected void doNfcInBackground() throws IOException { protected void doSecurityTokenInBackground() throws IOException {
mNfcFingerprints = nfcGetFingerprints(); mSecurityTokenFingerprints = mSecurityTokenHelper.getFingerprints();
mNfcUserId = nfcGetUserId(); mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
mNfcAid = nfcGetAid(); mSecurityTokenAid = mSecurityTokenHelper.getAid();
} }
@Override @Override
protected void onNfcPostExecute() { protected void onSecurityTokenPostExecute() {
long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints); long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mSecurityTokenFingerprints);
try { 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 the master key of that key matches this one, just show the token dialog
if (KeyFormattingUtils.convertFingerprintToHex(candidateFp).equals(mFingerprintString)) { if (KeyFormattingUtils.convertFingerprintToHex(candidateFp).equals(mFingerprintString)) {
showSecurityTokenFragment(mNfcFingerprints, mNfcUserId, mNfcAid); showSecurityTokenFragment(mSecurityTokenFingerprints, mSecurityTokenUserId, mSecurityTokenAid);
return; return;
} }
@@ -680,9 +680,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
Intent intent = new Intent( Intent intent = new Intent(
ViewKeyActivity.this, ViewKeyActivity.class); ViewKeyActivity.this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid); intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId); intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints); intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent); startActivity(intent);
finish(); finish();
} }
@@ -695,9 +695,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
public void onAction() { public void onAction() {
Intent intent = new Intent( Intent intent = new Intent(
ViewKeyActivity.this, CreateKeyActivity.class); ViewKeyActivity.this, CreateKeyActivity.class);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid); intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId); intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints); intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent); startActivity(intent);
finish(); finish();
} }
@@ -924,6 +924,7 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
} }
mPhoto.setImageBitmap(photo); mPhoto.setImageBitmap(photo);
mPhoto.setColorFilter(getResources().getColor(R.color.toolbar_photo_tint), PorterDuff.Mode.SRC_ATOP);
mPhotoLayout.setVisibility(View.VISIBLE); mPhotoLayout.setVisibility(View.VISIBLE);
} }
}; };
@@ -238,7 +238,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
// let user choose application // let user choose application
Intent sendIntent = new Intent(Intent.ACTION_SEND); 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 // NOTE: Don't use Intent.EXTRA_TEXT to send the key
// better send it via a Uri! // better send it via a Uri!
@@ -455,8 +455,19 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
} }
private void uploadToKeyserver() { 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); Intent uploadIntent = new Intent(getActivity(), UploadKeyActivity.class);
uploadIntent.setData(mDataUri); uploadIntent.setData(mDataUri);
uploadIntent.putExtra(MultiUserIdsFragment.EXTRA_KEY_IDS, new long[]{keyId});
startActivityForResult(uploadIntent, 0); startActivityForResult(uploadIntent, 0);
} }
@@ -39,6 +39,7 @@ import android.widget.AdapterView;
import android.widget.ListView; import android.widget.ListView;
import android.widget.ViewAnimator; import android.widget.ViewAnimator;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
@@ -346,50 +347,45 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements
} }
break; break;
} }
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: { case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN: {
// TODO: enable later when Admin PIN handling is resolved SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
Notify.create(getActivity(), if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
"This feature will be available in an upcoming OpenKeychain version.", secretKeyType == SecretKeyType.GNU_DUMMY) {
Notify.Style.WARN).show(); Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_stripped, Notify.Style.ERROR)
break; .show();
break;
}
// Activity activity = EditKeyFragment.this.getActivity(); int algorithm = mSubkeysAdapter.getAlgorithm(position);
// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position); if (algorithm != PublicKeyAlgorithmTags.RSA_GENERAL
// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD || && algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT
// secretKeyType == SecretKeyType.GNU_DUMMY) { && algorithm != PublicKeyAlgorithmTags.RSA_SIGN) {
// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR) Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_algo, Notify.Style.ERROR)
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); .show();
// break; break;
// } }
// int algorithm = mSubkeysAdapter.getAlgorithm(position);
// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN if (mSubkeysAdapter.getKeySize(position) != 2048) {
// if (algorithm != 1 && algorithm != 2 && algorithm != 3) { Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_size, Notify.Style.ERROR)
// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR) .show();
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); break;
// break; }
// }
// if (mSubkeysAdapter.getKeySize(position) != 2048) { SubkeyChange change;
// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR) change = mEditModeSaveKeyringParcel.getSubkeyChange(keyId);
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar)); if (change == null) {
// break; mEditModeSaveKeyringParcel.mChangeSubKeys.add(
// } new SubkeyChange(keyId, false, true)
// );
// break;
// SubkeyChange change; }
// change = mSaveKeyringParcel.getSubkeyChange(keyId); // toggle
// if (change == null) { change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
// mSaveKeyringParcel.mChangeSubKeys.add( if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
// new SubkeyChange(keyId, false, true) // User had chosen to strip key, but now wants to divert it.
// ); change.mDummyStrip = false;
// break; }
// } 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(); getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad();
@@ -141,6 +141,7 @@ public class ImportKeysListLoader
OperationResult.OperationLog log = new OperationResult.OperationLog(); OperationResult.OperationLog log = new OperationResult.OperationLog();
log.add(OperationResult.LogType.MSG_GET_NO_VALID_KEYS, 0); log.add(OperationResult.LogType.MSG_GET_NO_VALID_KEYS, 0);
GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_ERROR_NO_VALID_KEYS, log); GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_ERROR_NO_VALID_KEYS, log);
mData.clear();
mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult); mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult);
} }
} }
@@ -66,6 +66,7 @@ public class KeyAdapter extends CursorAdapter {
KeyRings.HAS_DUPLICATE_USER_ID, KeyRings.HAS_DUPLICATE_USER_ID,
KeyRings.FINGERPRINT, KeyRings.FINGERPRINT,
KeyRings.CREATION, KeyRings.CREATION,
KeyRings.HAS_ENCRYPT
}; };
public static final int INDEX_MASTER_KEY_ID = 1; 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_HAS_DUPLICATE_USER_ID = 7;
public static final int INDEX_FINGERPRINT = 8; public static final int INDEX_FINGERPRINT = 8;
public static final int INDEX_CREATION = 9; public static final int INDEX_CREATION = 9;
public static final int INDEX_HAS_ENCRYPT = 10;
public KeyAdapter(Context context, Cursor c, int flags) { public KeyAdapter(Context context, Cursor c, int flags) {
super(context, c, flags); super(context, c, flags);
@@ -289,6 +291,7 @@ public class KeyAdapter extends CursorAdapter {
public final KeyRing.UserId mUserId; public final KeyRing.UserId mUserId;
public final long mKeyId; public final long mKeyId;
public final boolean mHasDuplicate; public final boolean mHasDuplicate;
public final boolean mHasEncrypt;
public final Date mCreation; public final Date mCreation;
public final String mFingerprint; public final String mFingerprint;
public final boolean mIsSecret, mIsRevoked, mIsExpired, mIsVerified; public final boolean mIsSecret, mIsRevoked, mIsExpired, mIsVerified;
@@ -299,6 +302,7 @@ public class KeyAdapter extends CursorAdapter {
mUserIdFull = userId; mUserIdFull = userId;
mKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); mKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
mHasDuplicate = cursor.getLong(INDEX_HAS_DUPLICATE_USER_ID) > 0; mHasDuplicate = cursor.getLong(INDEX_HAS_DUPLICATE_USER_ID) > 0;
mHasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0;
mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000); mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000);
mFingerprint = KeyFormattingUtils.convertFingerprintToHex( mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
cursor.getBlob(INDEX_FINGERPRINT)); cursor.getBlob(INDEX_FINGERPRINT));
@@ -315,6 +319,7 @@ public class KeyAdapter extends CursorAdapter {
mUserIdFull = userId; mUserIdFull = userId;
mKeyId = ring.getMasterKeyId(); mKeyId = ring.getMasterKeyId();
mHasDuplicate = false; mHasDuplicate = false;
mHasEncrypt = key.getKeyRing().getEncryptIds().size() > 0;
mCreation = key.getCreationTime(); mCreation = key.getCreationTime();
mFingerprint = KeyFormattingUtils.convertFingerprintToHex( mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
ring.getFingerprint()); ring.getFingerprint());
@@ -333,14 +338,6 @@ public class KeyAdapter extends CursorAdapter {
return mUserId.email; 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) { public static String[] getProjectionWith(String[] projection) {
@@ -39,6 +39,7 @@ import java.util.ArrayList;
public class MultiUserIdsAdapter extends CursorAdapter { public class MultiUserIdsAdapter extends CursorAdapter {
private LayoutInflater mInflater; private LayoutInflater mInflater;
private final ArrayList<Boolean> mCheckStates; private final ArrayList<Boolean> mCheckStates;
private boolean checkboxVisibility = true;
public MultiUserIdsAdapter(Context context, Cursor c, int flags, ArrayList<Boolean> preselectStates) { public MultiUserIdsAdapter(Context context, Cursor c, int flags, ArrayList<Boolean> preselectStates) {
super(context, c, flags); super(context, c, flags);
@@ -46,6 +47,11 @@ public class MultiUserIdsAdapter extends CursorAdapter {
mCheckStates = preselectStates == null ? new ArrayList<Boolean>() : preselectStates; 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 @Override
public Cursor swapCursor(Cursor newCursor) { public Cursor swapCursor(Cursor newCursor) {
if (newCursor != null) { if (newCursor != null) {
@@ -138,6 +144,7 @@ public class MultiUserIdsAdapter extends CursorAdapter {
} }
}); });
vCheckBox.setClickable(false); vCheckBox.setClickable(false);
vCheckBox.setVisibility(checkboxVisibility?View.VISIBLE:View.GONE);
View vUidBody = view.findViewById(R.id.user_id_body); View vUidBody = view.findViewById(R.id.user_id_body);
vUidBody.setClickable(true); vUidBody.setClickable(true);
@@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.ui.adapter;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.support.v4.app.FragmentActivity;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -32,6 +33,7 @@ import android.widget.TextView;
import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.bcpg.sig.KeyFlags;
import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.ui.dialog.AddSubkeyDialogFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import java.util.Calendar; import java.util.Calendar;
@@ -65,10 +67,11 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
public SaveKeyringParcel.SubkeyAdd mModel; public SaveKeyringParcel.SubkeyAdd mModel;
} }
public View getView(final int position, View convertView, ViewGroup parent) { public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) { if (convertView == null) {
// Not recycled, inflate a new view // 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(); final ViewHolder holder = new ViewHolder();
holder.vKeyId = (TextView) convertView.findViewById(R.id.subkey_item_key_id); holder.vKeyId = (TextView) convertView.findViewById(R.id.subkey_item_key_id);
holder.vKeyDetails = (TextView) convertView.findViewById(R.id.subkey_item_details); 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); vStatus.setVisibility(View.GONE);
convertView.setTag(holder); 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(); final ViewHolder holder = (ViewHolder) convertView.getTag();
// save reference to model item // save reference to model item
@@ -113,8 +108,41 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
boolean isMasterKey = mNewKeyring && position == 0; boolean isMasterKey = mNewKeyring && position == 0;
if (isMasterKey) { if (isMasterKey) {
holder.vKeyId.setTypeface(null, Typeface.BOLD); 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 { } else {
holder.vKeyId.setTypeface(null, Typeface.NORMAL); 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); holder.vKeyId.setText(R.string.edit_key_new_subkey);
@@ -26,6 +26,7 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.view.Gravity; import android.view.Gravity;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
@@ -45,8 +46,8 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
initTheme();
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
initTheme();
initLayout(); initLayout();
initToolbar(); 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) { public static void onResumeChecks(Context context) {
KeyserverSyncAdapterService.cancelUpdates(context); KeyserverSyncAdapterService.cancelUpdates(context);
// in case user has disabled sync from Android account settings // in case user has disabled sync from Android account settings
@@ -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();
}
}
@@ -130,9 +130,9 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
switch (requiredInput.mType) { switch (requiredInput.mType) {
// always use CryptoOperationHelper.startActivityForResult! // always use CryptoOperationHelper.startActivityForResult!
case NFC_MOVE_KEY_TO_CARD: case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
case NFC_DECRYPT: case SECURITY_TOKEN_DECRYPT:
case NFC_SIGN: { case SECURITY_TOKEN_SIGN: {
Intent intent = new Intent(activity, SecurityTokenOperationActivity.class); Intent intent = new Intent(activity, SecurityTokenOperationActivity.class);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput); intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel); intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel);
@@ -50,14 +50,13 @@ import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener; 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.Constants;
import org.sufficientlysecure.keychain.R; 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.Log;
import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.TlsHelper; import org.sufficientlysecure.keychain.util.TlsHelper;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
@@ -354,19 +353,15 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
Log.d("Converted URL", newKeyserver.toString()); 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 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."); Log.w(Constants.TAG, "No pinned certificate for this host in OpenKeychain's assets.");
reason = FailureReason.NO_PINNED_CERTIFICATE; reason = FailureReason.NO_PINNED_CERTIFICATE;
return reason; return reason;
} }
OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(newKeyserver.toURL(), proxy);
client.newCall(new Request.Builder().url(newKeyserver.toURL()).build()).execute(); client.newCall(new Request.Builder().url(newKeyserver.toURL()).build()).execute();
} catch (TlsHelper.TlsHelperException e) { } catch (TlsHelper.TlsHelperException e) {
reason = FailureReason.CONNECTION_FAILED; reason = FailureReason.CONNECTION_FAILED;
@@ -17,25 +17,26 @@
package org.sufficientlysecure.keychain.ui.dialog; package org.sufficientlysecure.keychain.ui.dialog;
import android.annotation.TargetApi; import android.annotation.SuppressLint;
import android.app.Dialog; import android.app.Dialog;
import android.os.Build; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment; import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.text.Editable; import android.text.Html;
import android.text.TextWatcher;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.CompoundButton; import android.widget.CompoundButton;
import android.widget.DatePicker; import android.widget.DatePicker;
import android.widget.EditText; import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TableRow; import android.widget.TableRow;
import android.widget.TextView; import android.widget.TextView;
@@ -49,14 +50,18 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve;
import org.sufficientlysecure.keychain.util.Choice; import org.sufficientlysecure.keychain.util.Choice;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.List;
import java.util.TimeZone; import java.util.TimeZone;
public class AddSubkeyDialogFragment extends DialogFragment { public class AddSubkeyDialogFragment extends DialogFragment {
public interface OnAlgorithmSelectedListener { 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"; 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 CheckBox mNoExpiryCheckBox;
private TableRow mExpiryRow; private TableRow mExpiryRow;
private DatePicker mExpiryDatePicker; private DatePicker mExpiryDatePicker;
private Spinner mAlgorithmSpinner; private Spinner mKeyTypeSpinner;
private View mKeySizeRow; private RadioGroup mUsageRadioGroup;
private Spinner mKeySizeSpinner; private RadioButton mUsageNone;
private View mCurveRow; private RadioButton mUsageSign;
private Spinner mCurveSpinner; private RadioButton mUsageEncrypt;
private TextView mCustomKeyTextView; private RadioButton mUsageSignAndEncrypt;
private EditText mCustomKeyEditText;
private TextView mCustomKeyInfoTextView;
private CheckBox mFlagCertify;
private CheckBox mFlagSign;
private CheckBox mFlagEncrypt;
private CheckBox mFlagAuthenticate;
private boolean mWillBeMasterKey; private boolean mWillBeMasterKey;
@@ -96,6 +95,8 @@ public class AddSubkeyDialogFragment extends DialogFragment {
return frag; return frag;
} }
@NonNull
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
final FragmentActivity context = getActivity(); final FragmentActivity context = getActivity();
@@ -106,25 +107,27 @@ public class AddSubkeyDialogFragment extends DialogFragment {
CustomAlertDialogBuilder dialog = new CustomAlertDialogBuilder(context); CustomAlertDialogBuilder dialog = new CustomAlertDialogBuilder(context);
@SuppressLint("InflateParams")
View view = mInflater.inflate(R.layout.add_subkey_dialog, null); View view = mInflater.inflate(R.layout.add_subkey_dialog, null);
dialog.setView(view); dialog.setView(view);
dialog.setTitle(R.string.title_add_subkey);
mNoExpiryCheckBox = (CheckBox) view.findViewById(R.id.add_subkey_no_expiry); mNoExpiryCheckBox = (CheckBox) view.findViewById(R.id.add_subkey_no_expiry);
mExpiryRow = (TableRow) view.findViewById(R.id.add_subkey_expiry_row); mExpiryRow = (TableRow) view.findViewById(R.id.add_subkey_expiry_row);
mExpiryDatePicker = (DatePicker) view.findViewById(R.id.add_subkey_expiry_date_picker); mExpiryDatePicker = (DatePicker) view.findViewById(R.id.add_subkey_expiry_date_picker);
mAlgorithmSpinner = (Spinner) view.findViewById(R.id.add_subkey_algorithm); mKeyTypeSpinner = (Spinner) view.findViewById(R.id.add_subkey_type);
mKeySizeSpinner = (Spinner) view.findViewById(R.id.add_subkey_size); mUsageRadioGroup = (RadioGroup) view.findViewById(R.id.add_subkey_usage_group);
mCurveSpinner = (Spinner) view.findViewById(R.id.add_subkey_curve); mUsageNone = (RadioButton) view.findViewById(R.id.add_subkey_usage_none);
mKeySizeRow = view.findViewById(R.id.add_subkey_row_size); mUsageSign = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign);
mCurveRow = view.findViewById(R.id.add_subkey_row_curve); mUsageEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_encrypt);
mCustomKeyTextView = (TextView) view.findViewById(R.id.add_subkey_custom_key_size_label); mUsageSignAndEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign_and_encrypt);
mCustomKeyEditText = (EditText) view.findViewById(R.id.add_subkey_custom_key_size_input);
mCustomKeyInfoTextView = (TextView) view.findViewById(R.id.add_subkey_custom_key_size_info); if(mWillBeMasterKey) {
mFlagCertify = (CheckBox) view.findViewById(R.id.add_subkey_flag_certify); dialog.setTitle(R.string.title_change_master_key);
mFlagSign = (CheckBox) view.findViewById(R.id.add_subkey_flag_sign); mUsageNone.setVisibility(View.VISIBLE);
mFlagEncrypt = (CheckBox) view.findViewById(R.id.add_subkey_flag_encrypt); mUsageNone.setChecked(true);
mFlagAuthenticate = (CheckBox) view.findViewById(R.id.add_subkey_flag_authenticate); } else {
dialog.setTitle(R.string.title_add_subkey);
}
mNoExpiryCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { mNoExpiryCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override @Override
@@ -143,65 +146,24 @@ public class AddSubkeyDialogFragment extends DialogFragment {
mExpiryDatePicker.setMinDate(minDateCal.getTime().getTime()); mExpiryDatePicker.setMinDate(minDateCal.getTime().getTime());
{ {
ArrayList<Choice<Algorithm>> choices = new ArrayList<>(); ArrayList<Choice<SupportedKeyType>> choices = new ArrayList<>();
choices.add(new Choice<>(Algorithm.DSA, getResources().getString( choices.add(new Choice<>(SupportedKeyType.RSA_2048, getResources().getString(
R.string.dsa))); R.string.rsa_2048), getResources().getString(R.string.rsa_2048_description_html)));
if (!mWillBeMasterKey) { choices.add(new Choice<>(SupportedKeyType.RSA_3072, getResources().getString(
choices.add(new Choice<>(Algorithm.ELGAMAL, getResources().getString( R.string.rsa_3072), getResources().getString(R.string.rsa_3072_description_html)));
R.string.elgamal))); 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<>(Algorithm.RSA, getResources().getString( choices.add(new Choice<>(SupportedKeyType.ECC_P256, getResources().getString(
R.string.rsa))); R.string.ecc_p256), getResources().getString(R.string.ecc_p256_description_html)));
choices.add(new Choice<>(Algorithm.ECDSA, getResources().getString( choices.add(new Choice<>(SupportedKeyType.ECC_P521, getResources().getString(
R.string.ecdsa))); R.string.ecc_p521), getResources().getString(R.string.ecc_p521_description_html)));
choices.add(new Choice<>(Algorithm.ECDH, getResources().getString( TwoLineArrayAdapter adapter = new TwoLineArrayAdapter(context,
R.string.ecdh)));
ArrayAdapter<Choice<Algorithm>> adapter = new ArrayAdapter<>(context,
android.R.layout.simple_spinner_item, choices); android.R.layout.simple_spinner_item, choices);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mKeyTypeSpinner.setAdapter(adapter);
mAlgorithmSpinner.setAdapter(adapter); // make RSA 3072 the default
// make RSA the default
for (int i = 0; i < choices.size(); ++i) { for (int i = 0; i < choices.size(); ++i) {
if (choices.get(i).getId() == Algorithm.RSA) { if (choices.get(i).getId() == SupportedKeyType.RSA_3072) {
mAlgorithmSpinner.setSelection(i); mKeyTypeSpinner.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);
break; break;
} }
} }
@@ -215,45 +177,35 @@ public class AddSubkeyDialogFragment extends DialogFragment {
final AlertDialog alertDialog = dialog.show(); final AlertDialog alertDialog = dialog.show();
mCustomKeyEditText.addTextChangedListener(new TextWatcher() { mKeyTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
setOkButtonAvailability(alertDialog);
}
});
mKeySizeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
setCustomKeyVisibility(); // noinspection unchecked
setOkButtonAvailability(alertDialog); SupportedKeyType keyType = ((Choice<SupportedKeyType>) parent.getSelectedItem()).getId();
// RadioGroup.getCheckedRadioButtonId() gives the wrong RadioButton checked
// when programmatically unchecking children radio buttons. Clearing all is the only option.
mUsageRadioGroup.clearCheck();
if(mWillBeMasterKey) {
mUsageNone.setChecked(true);
}
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 @Override
public void onNothingSelected(AdapterView<?> parent) { public void onNothingSelected(AdapterView<?> parent) {}
}
});
mAlgorithmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
updateUiForAlgorithm(((Choice<Algorithm>) parent.getSelectedItem()).getId());
setCustomKeyVisibility();
setOkButtonAvailability(alertDialog);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
}); });
return alertDialog; return alertDialog;
@@ -269,36 +221,74 @@ public class AddSubkeyDialogFragment extends DialogFragment {
positiveButton.setOnClickListener(new View.OnClickListener() { positiveButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (!mFlagCertify.isChecked() && !mFlagSign.isChecked() if (mUsageRadioGroup.getCheckedRadioButtonId() == -1) {
&& !mFlagEncrypt.isChecked() && !mFlagAuthenticate.isChecked()) { Toast.makeText(getActivity(), R.string.edit_key_select_usage, Toast.LENGTH_LONG).show();
Toast.makeText(getActivity(), R.string.edit_key_select_flag, Toast.LENGTH_LONG).show();
return; return;
} }
Algorithm algorithm = ((Choice<Algorithm>) mAlgorithmSpinner.getSelectedItem()).getId(); // noinspection unchecked
SupportedKeyType keyType = ((Choice<SupportedKeyType>) mKeyTypeSpinner.getSelectedItem()).getId();
Curve curve = null; Curve curve = null;
Integer keySize = null; Integer keySize = null;
// For EC keys, add a curve Algorithm algorithm = null;
if (algorithm == Algorithm.ECDH || algorithm == Algorithm.ECDSA) {
curve = ((Choice<Curve>) mCurveSpinner.getSelectedItem()).getId(); // set keysize & curve, for RSA & ECC respectively
// Otherwise, get a keysize switch (keyType) {
} else { case RSA_2048: {
keySize = getProperKeyLength(algorithm, getSelectedKeyLength()); 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; int flags = 0;
if (mFlagCertify.isChecked()) { if (mWillBeMasterKey) {
flags |= KeyFlags.CERTIFY_OTHER; flags |= KeyFlags.CERTIFY_OTHER;
} }
if (mFlagSign.isChecked()) { if (mUsageSign.isChecked()) {
flags |= KeyFlags.SIGN_DATA; flags |= KeyFlags.SIGN_DATA;
} } else if (mUsageEncrypt.isChecked()) {
if (mFlagEncrypt.isChecked()) {
flags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE; 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; long expiry;
if (mNoExpiryCheckBox.isChecked()) { if (mNoExpiryCheckBox.isChecked()) {
@@ -332,206 +322,29 @@ public class AddSubkeyDialogFragment extends DialogFragment {
} }
} }
private int getSelectedKeyLength() { private class TwoLineArrayAdapter extends ArrayAdapter<Choice<SupportedKeyType>> {
final String selectedItemString = (String) mKeySizeSpinner.getSelectedItem(); public TwoLineArrayAdapter(Context context, int resource, List<Choice<SupportedKeyType>> objects) {
final String customLengthString = getResources().getString(R.string.key_size_custom); super(context, resource, objects);
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> @Override
* <p>for RSA algorithm, key length must be greater than 2048. Possibility to generate keys bigger public View getDropDownView(int position, View convertView, ViewGroup parent) {
* than 8192 bits is currently disabled, because it's almost impossible to generate them on a mobile device (check // inflate view if not given one
* <a href="http://www.javamex.com/tutorials/cryptography/rsa_key_length.shtml">RSA key length plot</a> and if (convertView == null) {
* <a href="http://www.keylength.com/">Cryptographic Key Length Recommendation</a>). Also, key length must be a convertView = getActivity().getLayoutInflater()
* multiplicity of 8.</p> .inflate(R.layout.two_line_spinner_dropdown_item, parent, false);
* <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;
}
}
return properKeyLength;
}
private void setOkButtonAvailability(AlertDialog alertDialog) { Choice c = this.getItem(position);
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() { TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
final String selectedItemString = (String) mKeySizeSpinner.getSelectedItem(); TextView text2 = (TextView) convertView.findViewById(android.R.id.text2);
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); text1.setText(c.getName());
mCustomKeyTextView.setVisibility(visibility); text2.setText(Html.fromHtml(c.getDescription()));
mCustomKeyInfoTextView.setVisibility(visibility);
// hide keyboard after setting visibility to gone return convertView;
if (visibility == View.GONE) {
InputMethodManager imm = (InputMethodManager)
getActivity().getSystemService(FragmentActivity.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mCustomKeyEditText.getWindowToken(), 0);
}
}
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);
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();
}
@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);
}
} }
} }
@@ -35,7 +35,11 @@ public class EditSubkeyDialogFragment extends DialogFragment {
public static final int MESSAGE_CHANGE_EXPIRY = 1; public static final int MESSAGE_CHANGE_EXPIRY = 1;
public static final int MESSAGE_REVOKE = 2; public static final int MESSAGE_REVOKE = 2;
public static final int MESSAGE_STRIP = 3; 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; private Messenger mMessenger;
@@ -68,17 +72,17 @@ public class EditSubkeyDialogFragment extends DialogFragment {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
switch (which) { switch (which) {
case 0: case SUBKEY_MENU_CHANGE_EXPIRY:
sendMessageToHandler(MESSAGE_CHANGE_EXPIRY, null); sendMessageToHandler(MESSAGE_CHANGE_EXPIRY, null);
break; break;
case 1: case SUBKEY_MENU_REVOKE_SUBKEY:
sendMessageToHandler(MESSAGE_REVOKE, null); sendMessageToHandler(MESSAGE_REVOKE, null);
break; break;
case 2: case SUBKEY_MENU_STRIP_SUBKEY:
sendMessageToHandler(MESSAGE_STRIP, null); showAlertDialog();
break; break;
case 3: case SUBKEY_MENU_MOVE_TO_SECURITY_TOKEN:
sendMessageToHandler(MESSAGE_MOVE_KEY_TO_CARD, null); sendMessageToHandler(MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN, null);
break; break;
default: default:
break; break;
@@ -95,6 +99,25 @@ public class EditSubkeyDialogFragment extends DialogFragment {
return builder.show(); 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 * Send message back to handler which is initialized in a activity
* *

Some files were not shown because too many files have changed in this diff Show More