diff options
22 files changed, 1015 insertions, 268 deletions
diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java index 2457eeb51..08dcacea5 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java @@ -44,6 +44,7 @@ import org.spongycastle.openpgp.operator.PGPContentSignerBuilder; import org.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.spongycastle.util.Strings; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.service.results.OperationResultParcel; import org.sufficientlysecure.keychain.service.results.EditKeyResult; @@ -157,7 +158,7 @@ public class UncachedKeyringCanonicalizeTest { @Test public void testUidSignature() throws Exception { UncachedPublicKey masterKey = ring.getPublicKey(); - final WrappedSignature sig = masterKey.getSignaturesForId("twi").next(); + final WrappedSignature sig = masterKey.getSignaturesForRawId(Strings.toUTF8ByteArray("twi")).next(); byte[] raw = sig.getEncoded(); // destroy the signature diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java index 3ffdb8262..8cfed42e4 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java @@ -31,6 +31,7 @@ import org.spongycastle.bcpg.S2K; import org.spongycastle.bcpg.SecretKeyPacket; import org.spongycastle.bcpg.sig.KeyFlags; import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.spongycastle.util.Strings; import org.sufficientlysecure.keychain.service.results.OperationResultParcel; import org.sufficientlysecure.keychain.service.results.EditKeyResult; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; @@ -296,7 +297,7 @@ public class UncachedKeyringMergeTest { { byte[] sig = KeyringTestingHelper.getNth( - modified.getPublicKey().getSignaturesForId("twi"), 1).getEncoded(); + modified.getPublicKey().getSignaturesForRawId(Strings.toUTF8ByteArray("twi")), 1).getEncoded(); // inject the (foreign!) signature into subkey signature position UncachedKeyRing moreModified = KeyringTestingHelper.injectPacket(modified, sig, 1); diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java index 44f3f9145..c431dff1b 100644 --- a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java @@ -25,14 +25,18 @@ import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.shadows.ShadowLog; +import org.spongycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.service.results.SaveKeyringResult; +import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.ProgressScaler; +import java.util.Arrays; + @RunWith(RobolectricTestRunner.class) @org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19 public class ProviderHelperSaveTest { @@ -102,7 +106,6 @@ public class ProviderHelperSaveTest { SaveKeyringResult result; - // insert both keys, second should fail result = mProviderHelper.saveSecretKeyRing(sec, new ProgressScaler()); Assert.assertTrue("import of secret keyring should succeed", result.success()); @@ -119,6 +122,32 @@ public class ProviderHelperSaveTest { } + @Test public void testImportBadEncodedUserId() throws Exception { + + UncachedKeyRing key = + readRingFromResource("/test-keys/bad_user_id_encoding.asc"); + long keyId = key.getMasterKeyId(); + + SaveKeyringResult result; + + result = mProviderHelper.savePublicKeyRing(key, new ProgressScaler()); + Assert.assertTrue("import of keyring should succeed", result.success()); + + // make sure both the CanonicalizedSecretKeyRing as well as the CachedPublicKeyRing correctly + // indicate the secret key type + CanonicalizedPublicKeyRing ring = mProviderHelper.getCanonicalizedPublicKeyRing(keyId); + boolean found = false; + byte[] badUserId = Hex.decode("436c61757320467261656e6b656c203c436c6175732e4672e46e6b656c4068616c696661782e727774682d61616368656e2e64653e"); + for (byte[] rawUserId : new IterableIterator<byte[]>( + ring.getUnorderedRawUserIds().iterator())) { + if (Arrays.equals(rawUserId, badUserId)) { + found = true; + } + } + + Assert.assertTrue("import of the badly encoded user id should succeed", found); + } + UncachedKeyRing readRingFromResource(String name) throws Exception { return UncachedKeyRing.fromStream(ProviderHelperSaveTest.class.getResourceAsStream(name)).next(); } diff --git a/OpenKeychain-Test/src/test/resources/test-keys/bad_user_id_encoding.asc b/OpenKeychain-Test/src/test/resources/test-keys/bad_user_id_encoding.asc new file mode 100644 index 000000000..332747f84 --- /dev/null +++ b/OpenKeychain-Test/src/test/resources/test-keys/bad_user_id_encoding.asc @@ -0,0 +1,60 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: SKS 1.1.5 +Comment: Hostname: keyserver.codinginfinity.com + +mQGiBDhOth4RBADOAh3T2bWx+os6LBE+C55EBykg2WdC5AOnRe457ro5X1YuhZomY31hcxvH +m+hro4Lh43SP2xTcyAz4G2UcLJch9qdgJ7W32wb+kOyEi/WTkfLpU2OiXRS975euqex4kARK +l2alJfmu8FI4p1WinbGDDPyb6DyiqhW3KP/i3ukQwQCg/zOQEvnI0RgHwFSi1V+dvvCR5ZUD +/0WCch+cD8Aje0EUtztBx2DL/JKXRA0B/9jivwd/QgQy2hIYYXL5+OFrRBWcpPNFKqhZLyXn +82OEaZZpJGHEpfGeb8uHZ3fv8NTaPxNGwYx99AwiOVatNX88gCnlQ2ZBWQuuN1rrK/oKduPK +35yZbNSR3nT8fJsWg/+h1L2eB00qA/0cgovkdYnjVzq3ztt43jXQEzrZzYMRJCK9q1CYSeZ4 +g/zjFwCROgQoVnVcLYfJDCJWM/3zJvlqfgGikhKcT4UepGadLD0ACJ9gchsc0Zrx+gUlgr/K +aWGdRh+j4i5ykc0HNO/TFo5ESBBrGx3WtyxI1ob+2MAH8RAa18HZq2M1irQgQ2xhdXMgRnJh +ZW5rZWwgPGFsaWVuQG9ubGluZS5kZT6IRgQQEQIABgUCOFQy1gAKCRA81crJrp6K4xFhAJ93 +JV8PJRdgcHrLrlgKrG9zSpjqRQCgvbImT0pBuZIj058FoklOw7OiBjaISwQQEQIACwUCOE62 +7AQLAwIBAAoJELXPGfg2YSW2elEAn1hrbuR8/I3/OBxReVor0wzQ3tYTAJ44UFUL3VmHweHB +t+kfPDVl93bMY7QiQ2xhdXMgRnJhZW5rZWwgPGNmcmFlbmtlbEBnbXgubmV0PohGBBARAgAG +BQI4VDKRAAoJEDzVysmunorjI3AAn2nfyP1nsRpSMzPs8Vm+KUyvTY68AJ9q3evrzGG1Qbtl +VZgtcbprQKnaX4hGBBARAgAGBQI6uRZFAAoJEDxoxauoLKe5e6sAn3ZDhTsVaNOgFrpzNZQ6 +meeoJTjgAKDdSqyUit+pSzK//pqEdKJUJ2N93ohGBBARAgAGBQI8Niv5AAoJEIPL/xBv5hfH +GgEAoKMcltZIsfaagIV86tlQsETsleJeAJ9Puwxt/n455EQWHkAQz109noNIVIhLBBARAgAL +BQI4TrYeBAsDAgEACgkQtc8Z+DZhJbbveQCfUOPT34HElA7KEPTchx4/nBINqKAAoMIk8G+r +UFzHJo3w7mjeS2OEHCoutCJDbGF1cyBGcmFlbmtlbCA8Y2ZyYWVua2VsQHVzYS5uZXQ+iEYE +EBECAAYFAjhUMp4ACgkQPNXKya6eiuMuqgCg1WcZAJYFF3/jUA85GJ4BVLdQkpsAoK37bsBz +tvhUQE/dObJduMsK9462iEsEEBECAAsFAjhOtj4ECwMCAQAKCRC1zxn4NmElts+/AKD9cnve +ShBQe95eF98yomHFJ8CgHQCfTNyhyJgLhTfYd2wI8jrTePr2zhi0JENsYXVzIEZyYWVua2Vs +IDxjY19mYXN0amFja0BnbXgubmV0PohGBBARAgAGBQI4VDKsAAoJEDzVysmunorj2mEAoLf9 +yqSFRla7rfVSD2cTSozHrGt4AJ9L4VDELJjDoaTC0ZTxwuVaSPxSxIhLBBARAgALBQI4TrZZ +BAsDAgEACgkQtc8Z+DZhJbZC2gCgvjEKlK22N+RSKbPEN4XyjrNLh3QAoPVHXqRght5VG3+k +XzA/zDYWQ+/2tDBDbGF1cyBGcmFlbmtlbCA8ZmFzdGphY2tAaGFsaWZheC5yd3RoLWFhY2hl +bi5kZT6IRgQQEQIABgUCOFQyyQAKCRA81crJrp6K41yrAKDrCcfzexdQCDFtfbxuwyeUibST +zgCbBGPP4qeXfAf0l07kY5Of1/PyniiISwQQEQIACwUCOE62uwQLAwIBAAoJELXPGfg2YSW2 +nhoAnRu89DXOd1zOUagpwcqLmj542JwOAKDhN736B2Zd9xtxaw2mT46Qu6YTULQ1Q2xhdXMg +RnJhZW5rZWwgPENsYXVzLkZy5G5rZWxAaGFsaWZheC5yd3RoLWFhY2hlbi5kZT6IRgQQEQIA +BgUCOFQy6QAKCRA81crJrp6K47VGAKDvlogjYHaN6t650M6CteLCXh5SqQCghfGa7tk327WE +St4WER8cXbADd62ISwQQEQIACwUCOE63LwQLAwIBAAoJELXPGfg2YSW2FuUAmQGG/lfpeNad +fgB61x37ugaFgE9oAJ0QgfdXhalOPRjHnBksxYzzrWFCVLkEDQQ4TrYeEBAA+RigfloGYXpD +kJXcBWyHhuxh7M1FHw7Y4KN5xsncegus5D/jRpS2MEpT13wCFkiAtRXlKZmpnwd00//jocWW +IE6YZbjYDe4QXau2FxxR2FDKIldDKb6V6FYrOHhcC9v4TE3V46pGzPvOF+gqnRRh44SpT9GD +hKh5tu+Pp0NGCMbMHXdXJDhK4sTw6I4TZ5dOkhNh9tvrJQ4X/faY98h8ebByHTh1+/bBc8SD +ESYrQ2DD4+jWCv2hKCYLrqmus2UPogBTAaB81qujEh76DyrOH3SET8rzF/OkQOnX0ne2Qi0C +NsEmy2henXyYCQqNfi3t5F159dSST5sYjvwqp0t8MvZCV7cIfwgXcqK61qlC8wXo+VMROU+2 +8W65Szgg2gGnVqMU6Y9AVfPQB8bLQ6mUrfdMZIZJ+AyDvWXpF9Sh01D49Vlf3HZSTz09jdvO +meFXklnN/biudE/F/Ha8g8VHMGHOfMlm/xX5u/2RXscBqtNbno2gpXI61Brwv0YAWCvl9Ij9 +WE5J280gtJ3kkQc2azNsOA1FHQ98iLMcfFstjvbzySPAQ/ClWxiNjrtVjLhdONM0/XwXV0Oj +HRhs3jMhLLUq/zzhsSlAGBGNfISnCnLWhsQDGcgHKXrKlQzZlp+r0ApQmwJG0wg9ZqRdQZ+c +fL2JSyIZJrqrol7DVes91hcAAgIQAIbS27fz+cIoDbWDQ4UhfHFGqPK51uixRfw2fIWmXsV0 +yFChqjaPRieERsP4Y26IGL7jB+yr+Q8fNp+YcevH2YWluMcXAZMXkEMi8Lvz9lDUVZxrgILm +ZHyBycHXXhn8mFZJufUnn33cr8JC/6D+QBHdF1lly40s1877Om10W14bHTo1SBNN7PoAO/fC +w+GL0QEIK3iTqEI+bEs+vxLqBKZa1+NkKg/s5L6OXMgBTFuXqZvzWAHLK7wR1QT3DgKgY4go +YZeJkSzGmmQkZRCVBLZ0MQx1x6CiqMI9VN/5/7h5jA7QuhXhHZHuNmucITU05wHQty2yOH+D +nWsPCcWRjaXVe0pFqD6pJGBZB58B1N/7uGdn+PkR3TSDOt9IdXqWoY/dvLNc38uoL31FXko8 +ITwzua5LzwGhjwYoNiM31Cehx/zfzzLsQlzIvVjgdNX4rZcMTfi3cJ/RkVue2bOlrRSvCCzk +58lW+3PsHJ/Rk4yGngOimEK1okcjgSrVQamuK1kG0aKKv//ql1Ehd0WibKZTlq/n644Yb3Yd +HrVXK2JhdJ1opTmYHbGDS9rE6HUc+cq9DsL4ZZDHwPnsi5FXtjCs3q3k5d2y9P1szFgVXm54 +vNbolRY2J3/shMf0382KfyVNWLjqlC1DEtxNP+Xfk9XJsBe+PD3ClLfbPxG989BJiEYEGBEC +AAYFAjhOth4ACgkQtc8Z+DZhJbZvegCeJEWNkQYaPDTAx19s+GBBZJo1K6AAmwc006dQTrDf +ykezF64bvFWd+vuO +=5FoN +-----END PGP PUBLIC KEY BLOCK----- + diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 35f66c012..a83992d09 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -585,8 +585,10 @@ </activity> <activity android:name=".ui.ConsolidateDialogActivity" - android:theme="@android:style/Theme.NoDisplay" - android:label="@string/app_name" /> + android:theme="@android:style/Theme.NoDisplay" /> + <activity + android:name=".ui.PassphraseDialogActivity" + android:theme="@android:style/Theme.NoDisplay" /> <activity android:name=".ui.HelpActivity" android:label="@string/title_help" /> diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/KeyUpdateHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/KeyUpdateHelper.java new file mode 100644 index 000000000..9ca6d5c5b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/KeyUpdateHelper.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2014 Daniel Albert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.helper; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Messenger; + +import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; +import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; +import org.sufficientlysecure.keychain.keyimport.Keyserver; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; +import java.util.List; + +public class KeyUpdateHelper { + + public void updateAllKeys(Context context, KeychainIntentServiceHandler finishedHandler) { + UpdateTask updateTask = new UpdateTask(context, finishedHandler); + updateTask.execute(); + } + + private class UpdateTask extends AsyncTask<Void, Void, Void> { + private Context mContext; + private KeychainIntentServiceHandler mHandler; + + public UpdateTask(Context context, KeychainIntentServiceHandler handler) { + this.mContext = context; + this.mHandler = handler; + } + + @Override + protected Void doInBackground(Void... voids) { + ProviderHelper providerHelper = new ProviderHelper(mContext); + List<ImportKeysListEntry> keys = new ArrayList<ImportKeysListEntry>(); + String[] servers = Preferences.getPreferences(mContext).getKeyServers(); + + if (servers != null && servers.length > 0) { + // Load all the fingerprints in the database and prepare to import them + for (String fprint : providerHelper.getAllFingerprints(KeychainContract.KeyRings.buildUnifiedKeyRingsUri())) { + ImportKeysListEntry key = new ImportKeysListEntry(); + key.setFingerprintHex(fprint); + key.setBitStrength(1337); + key.setOrigin(servers[0]); + keys.add(key); + } + + // Start the service and update the keys + Intent importIntent = new Intent(mContext, KeychainIntentService.class); + importIntent.setAction(KeychainIntentService.ACTION_DOWNLOAD_AND_IMPORT_KEYS); + + Bundle importData = new Bundle(); + importData.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST, + new ArrayList<ImportKeysListEntry>(keys)); + importIntent.putExtra(KeychainIntentService.EXTRA_DATA, importData); + + importIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, new Messenger(mHandler)); + + mContext.startService(importIntent); + } + return null; + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java index 0d3e5676a..554899843 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java @@ -59,6 +59,10 @@ public abstract class CanonicalizedKeyRing extends KeyRing { return getPublicKey().getPrimaryUserIdWithFallback(); } + public ArrayList<byte[]> getUnorderedRawUserIds() { + return getPublicKey().getUnorderedRawUserIds(); + } + public ArrayList<String> getUnorderedUserIds() { return getPublicKey().getUnorderedUserIds(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java index 4239a01dc..fe4e7ffa7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java @@ -33,6 +33,8 @@ import org.spongycastle.openpgp.PGPSignature; import org.spongycastle.openpgp.PGPSignatureList; import org.spongycastle.openpgp.PGPUtil; import org.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.spongycastle.util.Strings; +import org.spongycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.service.results.OperationResultParcel.LogLevel; @@ -361,156 +363,159 @@ public class UncachedKeyRing { } } - ArrayList<String> processedUserIds = new ArrayList<String>(); - for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) { + ArrayList<byte[]> processedUserIds = new ArrayList<byte[]>(); + for (byte[] rawUserId : new IterableIterator<byte[]>(masterKey.getRawUserIDs())) { + String userId = Strings.fromUTF8ByteArray(rawUserId); + // check for duplicate user ids - if (processedUserIds.contains(userId)) { + if (processedUserIds.contains(rawUserId)) { log.add(LogLevel.WARN, LogType.MSG_KC_UID_DUP, indent, userId); // strip out the first found user id with this name - modified = PGPPublicKey.removeCertification(modified, userId); + modified = PGPPublicKey.removeCertification(modified, rawUserId); } - processedUserIds.add(userId); + processedUserIds.add(rawUserId); PGPSignature selfCert = null; revocation = null; - // look through signatures for this specific key - for (PGPSignature zert : new IterableIterator<PGPSignature>( - masterKey.getSignaturesForID(userId))) { - WrappedSignature cert = new WrappedSignature(zert); - long certId = cert.getKeyId(); - - int type = zert.getSignatureType(); - if (type != PGPSignature.DEFAULT_CERTIFICATION - && type != PGPSignature.NO_CERTIFICATION - && type != PGPSignature.CASUAL_CERTIFICATION - && type != PGPSignature.POSITIVE_CERTIFICATION - && type != PGPSignature.CERTIFICATION_REVOCATION) { - log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD_TYPE, - indent, "0x" + Integer.toString(zert.getSignatureType(), 16)); - modified = PGPPublicKey.removeCertification(modified, userId, zert); - badCerts += 1; - continue; - } - - if (cert.getCreationTime().after(now)) { - // Creation date in the future? No way! - log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD_TIME, indent); - modified = PGPPublicKey.removeCertification(modified, userId, zert); - badCerts += 1; - continue; - } - - if (cert.isLocal()) { - // Creation date in the future? No way! - log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD_LOCAL, indent); - modified = PGPPublicKey.removeCertification(modified, userId, zert); - badCerts += 1; - continue; - } + // look through signatures for this specific user id + @SuppressWarnings("unchecked") + Iterator<PGPSignature> signaturesIt = masterKey.getSignaturesForID(rawUserId); + if (signaturesIt != null) { + for (PGPSignature zert : new IterableIterator<PGPSignature>(signaturesIt)) { + WrappedSignature cert = new WrappedSignature(zert); + long certId = cert.getKeyId(); + + int type = zert.getSignatureType(); + if (type != PGPSignature.DEFAULT_CERTIFICATION + && type != PGPSignature.NO_CERTIFICATION + && type != PGPSignature.CASUAL_CERTIFICATION + && type != PGPSignature.POSITIVE_CERTIFICATION + && type != PGPSignature.CERTIFICATION_REVOCATION) { + log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD_TYPE, + indent, "0x" + Integer.toString(zert.getSignatureType(), 16)); + modified = PGPPublicKey.removeCertification(modified, rawUserId, zert); + badCerts += 1; + continue; + } - // If this is a foreign signature, ... - if (certId != masterKeyId) { - // never mind any further for public keys, but remove them from secret ones - if (isSecret()) { - log.add(LogLevel.WARN, LogType.MSG_KC_UID_FOREIGN, - indent, PgpKeyHelper.convertKeyIdToHex(certId)); - modified = PGPPublicKey.removeCertification(modified, userId, zert); + if (cert.getCreationTime().after(now)) { + // Creation date in the future? No way! + log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD_TIME, indent); + modified = PGPPublicKey.removeCertification(modified, rawUserId, zert); badCerts += 1; + continue; } - continue; - } - // Otherwise, first make sure it checks out - try { - cert.init(masterKey); - if (!cert.verifySignature(masterKey, userId)) { - log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD, - indent, userId); - modified = PGPPublicKey.removeCertification(modified, userId, zert); + if (cert.isLocal()) { + // Creation date in the future? No way! + log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD_LOCAL, indent); + modified = PGPPublicKey.removeCertification(modified, rawUserId, zert); badCerts += 1; continue; } - } catch (PgpGeneralException e) { - log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD_ERR, - indent, userId); - modified = PGPPublicKey.removeCertification(modified, userId, zert); - badCerts += 1; - continue; - } - switch (type) { - case PGPSignature.DEFAULT_CERTIFICATION: - case PGPSignature.NO_CERTIFICATION: - case PGPSignature.CASUAL_CERTIFICATION: - case PGPSignature.POSITIVE_CERTIFICATION: - if (selfCert == null) { - selfCert = zert; - } else if (selfCert.getCreationTime().before(cert.getCreationTime())) { - log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_CERT_DUP, - indent, userId); - modified = PGPPublicKey.removeCertification(modified, userId, selfCert); - redundantCerts += 1; - selfCert = zert; - } else { - log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_CERT_DUP, - indent, userId); - modified = PGPPublicKey.removeCertification(modified, userId, zert); - redundantCerts += 1; - } - // If there is a revocation certificate, and it's older than this, drop it - if (revocation != null - && revocation.getCreationTime().before(selfCert.getCreationTime())) { - log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_REVOKE_OLD, - indent, userId); - modified = PGPPublicKey.removeCertification(modified, userId, revocation); - revocation = null; - redundantCerts += 1; + // If this is a foreign signature, ... + if (certId != masterKeyId) { + // never mind any further for public keys, but remove them from secret ones + if (isSecret()) { + log.add(LogLevel.WARN, LogType.MSG_KC_UID_FOREIGN, + indent, PgpKeyHelper.convertKeyIdToHex(certId)); + modified = PGPPublicKey.removeCertification(modified, rawUserId, zert); + badCerts += 1; } - break; + continue; + } - case PGPSignature.CERTIFICATION_REVOCATION: - // If this is older than the (latest) self cert, drop it - if (selfCert != null && selfCert.getCreationTime().after(zert.getCreationTime())) { - log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_REVOKE_OLD, + // Otherwise, first make sure it checks out + try { + cert.init(masterKey); + if (!cert.verifySignature(masterKey, rawUserId)) { + log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD, indent, userId); - modified = PGPPublicKey.removeCertification(modified, userId, zert); - redundantCerts += 1; + modified = PGPPublicKey.removeCertification(modified, rawUserId, zert); + badCerts += 1; continue; } - // first revocation? remember it. - if (revocation == null) { - revocation = zert; - // more revocations? at least one is superfluous, then. - } else if (revocation.getCreationTime().before(cert.getCreationTime())) { - log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_REVOKE_DUP, - indent, userId); - modified = PGPPublicKey.removeCertification(modified, userId, revocation); - redundantCerts += 1; - revocation = zert; - } else { - log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_REVOKE_DUP, - indent, userId); - modified = PGPPublicKey.removeCertification(modified, userId, zert); - redundantCerts += 1; - } - break; + } catch (PgpGeneralException e) { + log.add(LogLevel.WARN, LogType.MSG_KC_UID_BAD_ERR, + indent, userId); + modified = PGPPublicKey.removeCertification(modified, rawUserId, zert); + badCerts += 1; + continue; + } + switch (type) { + case PGPSignature.DEFAULT_CERTIFICATION: + case PGPSignature.NO_CERTIFICATION: + case PGPSignature.CASUAL_CERTIFICATION: + case PGPSignature.POSITIVE_CERTIFICATION: + if (selfCert == null) { + selfCert = zert; + } else if (selfCert.getCreationTime().before(cert.getCreationTime())) { + log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_CERT_DUP, + indent, userId); + modified = PGPPublicKey.removeCertification(modified, rawUserId, selfCert); + redundantCerts += 1; + selfCert = zert; + } else { + log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_CERT_DUP, + indent, userId); + modified = PGPPublicKey.removeCertification(modified, rawUserId, zert); + redundantCerts += 1; + } + // If there is a revocation certificate, and it's older than this, drop it + if (revocation != null + && revocation.getCreationTime().before(selfCert.getCreationTime())) { + log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_REVOKE_OLD, + indent, userId); + modified = PGPPublicKey.removeCertification(modified, rawUserId, revocation); + revocation = null; + redundantCerts += 1; + } + break; + + case PGPSignature.CERTIFICATION_REVOCATION: + // If this is older than the (latest) self cert, drop it + if (selfCert != null && selfCert.getCreationTime().after(zert.getCreationTime())) { + log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_REVOKE_OLD, + indent, userId); + modified = PGPPublicKey.removeCertification(modified, rawUserId, zert); + redundantCerts += 1; + continue; + } + // first revocation? remember it. + if (revocation == null) { + revocation = zert; + // more revocations? at least one is superfluous, then. + } else if (revocation.getCreationTime().before(cert.getCreationTime())) { + log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_REVOKE_DUP, + indent, userId); + modified = PGPPublicKey.removeCertification(modified, rawUserId, revocation); + redundantCerts += 1; + revocation = zert; + } else { + log.add(LogLevel.DEBUG, LogType.MSG_KC_UID_REVOKE_DUP, + indent, userId); + modified = PGPPublicKey.removeCertification(modified, rawUserId, zert); + redundantCerts += 1; + } + break; + } } - } // If no valid certificate (if only a revocation) remains, drop it if (selfCert == null && revocation == null) { log.add(LogLevel.ERROR, LogType.MSG_KC_UID_REMOVE, indent, userId); - modified = PGPPublicKey.removeCertification(modified, userId); + modified = PGPPublicKey.removeCertification(modified, rawUserId); } } // If NO user ids remain, error out! - if (!modified.getUserIDs().hasNext()) { + if (modified == null || !modified.getUserIDs().hasNext()) { log.add(LogLevel.ERROR, LogType.MSG_KC_ERROR_NO_UID, indent); return null; } @@ -816,8 +821,14 @@ public class UncachedKeyRing { } // Copy over all user id certificates - for (String userId : new IterableIterator<String>(key.getUserIDs())) { - for (PGPSignature cert : new IterableIterator<PGPSignature>(key.getSignaturesForID(userId))) { + for (byte[] rawUserId : new IterableIterator<byte[]>(key.getRawUserIDs())) { + @SuppressWarnings("unchecked") + Iterator<PGPSignature> signaturesIt = key.getSignaturesForID(rawUserId); + // no signatures for this User ID, skip it + if (signaturesIt == null) { + continue; + } + for (PGPSignature cert : new IterableIterator<PGPSignature>(signaturesIt)) { // Don't merge foreign stuff into secret keys if (cert.getKeyID() != masterKeyId && isSecret()) { continue; @@ -829,7 +840,7 @@ public class UncachedKeyRing { } newCerts += 1; certs.add(encoded); - modified = PGPPublicKey.addCertification(modified, userId, cert); + modified = PGPPublicKey.addCertification(modified, rawUserId, cert); } } // If anything changed, save the updated (sub)key diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java index 2224d7391..e27190bc7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java @@ -28,11 +28,13 @@ import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPSignature; import org.spongycastle.openpgp.PGPSignatureSubpacketVector; import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.spongycastle.util.Strings; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.Log; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -126,12 +128,20 @@ public class UncachedPublicKey { * */ public String getPrimaryUserId() { - String found = null; + byte[] found = null; PGPSignature foundSig = null; - for (String userId : new IterableIterator<String>(mPublicKey.getUserIDs())) { + // noinspection unchecked + for (byte[] rawUserId : new IterableIterator<byte[]>(mPublicKey.getRawUserIDs())) { PGPSignature revocation = null; - for (PGPSignature sig : new IterableIterator<PGPSignature>(mPublicKey.getSignaturesForID(userId))) { + @SuppressWarnings("unchecked") + Iterator<PGPSignature> signaturesIt = mPublicKey.getSignaturesForID(rawUserId); + // no signatures for this User ID + if (signaturesIt == null) { + continue; + } + + for (PGPSignature sig : new IterableIterator<PGPSignature>(signaturesIt)) { try { // if this is a revocation, this is not the user id @@ -139,10 +149,10 @@ public class UncachedPublicKey { // make sure it's actually valid sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider( Constants.BOUNCY_CASTLE_PROVIDER_NAME), mPublicKey); - if (!sig.verifyCertification(userId, mPublicKey)) { + if (!sig.verifyCertification(rawUserId, mPublicKey)) { continue; } - if (found != null && found.equals(userId)) { + if (found != null && Arrays.equals(found, rawUserId)) { found = null; } revocation = sig; @@ -161,8 +171,8 @@ public class UncachedPublicKey { // make sure it's actually valid sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider( Constants.BOUNCY_CASTLE_PROVIDER_NAME), mPublicKey); - if (sig.verifyCertification(userId, mPublicKey)) { - found = userId; + if (sig.verifyCertification(rawUserId, mPublicKey)) { + found = rawUserId; foundSig = sig; // this one can't be relevant anymore at this point revocation = null; @@ -174,7 +184,11 @@ public class UncachedPublicKey { } } } - return found; + if (found != null) { + return Strings.fromUTF8ByteArray(found); + } else { + return null; + } } /** @@ -196,6 +210,14 @@ public class UncachedPublicKey { return userIds; } + public ArrayList<byte[]> getUnorderedRawUserIds() { + ArrayList<byte[]> userIds = new ArrayList<byte[]>(); + for (byte[] userId : new IterableIterator<byte[]>(mPublicKey.getRawUserIDs())) { + userIds.add(userId); + } + return userIds; + } + public boolean isElGamalEncrypt() { return getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT; } @@ -312,19 +334,23 @@ public class UncachedPublicKey { }; } - public Iterator<WrappedSignature> getSignaturesForId(String userId) { - final Iterator<PGPSignature> it = mPublicKey.getSignaturesForID(userId); - return new Iterator<WrappedSignature>() { - public void remove() { - it.remove(); - } - public WrappedSignature next() { - return new WrappedSignature(it.next()); - } - public boolean hasNext() { - return it.hasNext(); - } - }; + public Iterator<WrappedSignature> getSignaturesForRawId(byte[] rawUserId) { + final Iterator<PGPSignature> it = mPublicKey.getSignaturesForID(rawUserId); + if (it != null) { + return new Iterator<WrappedSignature>() { + public void remove() { + it.remove(); + } + public WrappedSignature next() { + return new WrappedSignature(it.next()); + } + public boolean hasNext() { + return it.hasNext(); + } + }; + } else { + return null; + } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java index f24259ba7..4d4c0e5d1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java @@ -187,8 +187,16 @@ public class WrappedSignature { } } - public boolean verifySignature(UncachedPublicKey key, String uid) throws PgpGeneralException { - return verifySignature(key.getPublicKey(), uid); + boolean verifySignature(PGPPublicKey key, byte[] rawUserId) throws PgpGeneralException { + try { + return mSig.verifyCertification(rawUserId, key); + } catch (PGPException e) { + throw new PgpGeneralException("Error!", e); + } + } + + public boolean verifySignature(UncachedPublicKey key, byte[] rawUserId) throws PgpGeneralException { + return verifySignature(key.getPublicKey(), rawUserId); } public boolean verifySignature(CanonicalizedPublicKey key, String uid) throws PgpGeneralException { return verifySignature(key.getPublicKey(), uid); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java index f71fcbd80..fdf8c1f38 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -28,6 +28,7 @@ import android.net.Uri; import android.os.RemoteException; import android.support.v4.util.LongSparseArray; +import org.spongycastle.util.Strings; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.helper.Preferences; @@ -154,7 +155,7 @@ public class ProviderHelper { } public HashMap<String, Object> getGenericData(Uri uri, String[] proj, int[] types) - throws NotFoundException { + throws NotFoundException { return getGenericData(uri, proj, types, null); } @@ -208,7 +209,7 @@ public class ProviderHelper { KeyRings.HAS_ANY_SECRET, KeyRings.VERIFIED, // and of course, ring data KeyRings.PUBKEY_DATA - }, KeyRings.HAS_ANY_SECRET + " = 1", null, null); + }, KeyRings.HAS_ANY_SECRET + " = 1", null, null); try { LongSparseArray<CanonicalizedPublicKey> result = new LongSparseArray<CanonicalizedPublicKey>(); @@ -406,11 +407,11 @@ public class ProviderHelper { values.put(Keys.EXPIRY, expiryDate.getTime() / 1000); if (key.isExpired()) { log(LogLevel.DEBUG, keyId == masterKeyId ? - LogType.MSG_IP_MASTER_EXPIRED : LogType.MSG_IP_SUBKEY_EXPIRED, + LogType.MSG_IP_MASTER_EXPIRED : LogType.MSG_IP_SUBKEY_EXPIRED, expiryDate.toString()); } else { log(LogLevel.DEBUG, keyId == masterKeyId ? - LogType.MSG_IP_MASTER_EXPIRES : LogType.MSG_IP_SUBKEY_EXPIRES, + LogType.MSG_IP_MASTER_EXPIRES : LogType.MSG_IP_SUBKEY_EXPIRES, expiryDate.toString()); } } @@ -434,8 +435,11 @@ public class ProviderHelper { } mIndent += 1; List<UserIdItem> uids = new ArrayList<UserIdItem>(); - for (String userId : new IterableIterator<String>( - masterKey.getUnorderedUserIds().iterator())) { + for (byte[] rawUserId : new IterableIterator<byte[]>( + masterKey.getUnorderedRawUserIds().iterator())) { + String userId = Strings.fromUTF8ByteArray(rawUserId); + Log.d(Constants.TAG, "userId: "+userId); + UserIdItem item = new UserIdItem(); uids.add(item); item.userId = userId; @@ -446,7 +450,7 @@ public class ProviderHelper { mIndent += 1; // look through signatures for this specific key for (WrappedSignature cert : new IterableIterator<WrappedSignature>( - masterKey.getSignaturesForId(userId))) { + masterKey.getSignaturesForRawId(rawUserId))) { long certId = cert.getKeyId(); try { // self signature @@ -469,7 +473,7 @@ public class ProviderHelper { if (trustedKeys.indexOfKey(certId) >= 0) { CanonicalizedPublicKey trustedKey = trustedKeys.get(certId); cert.init(trustedKey); - if (cert.verifySignature(masterKey, userId)) { + if (cert.verifySignature(masterKey, rawUserId)) { item.trustedCerts.add(cert); log(LogLevel.INFO, LogType.MSG_IP_UID_CERT_GOOD, PgpKeyHelper.convertKeyIdToHexShort(trustedKey.getKeyId()) @@ -1308,6 +1312,27 @@ public class ProviderHelper { return keyIds; } + public Set<String> getAllFingerprints(Uri uri) { + Set<String> fingerprints = new HashSet<String>(); + String[] projection = new String[]{KeyRings.FINGERPRINT}; + Cursor cursor = mContentResolver.query(uri, projection, null, null, null); + try { + if(cursor != null) { + int fingerprintColumn = cursor.getColumnIndex(KeyRings.FINGERPRINT); + while(cursor.moveToNext()) { + fingerprints.add( + PgpKeyHelper.convertFingerprintToHex(cursor.getBlob(fingerprintColumn)) + ); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return fingerprints; + } + public byte[] getApiAppSignature(String packageName) { Uri queryUri = ApiApps.buildByPackageNameUri(packageName); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java index 7e8de7f01..ce5534cf4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/RemoteServiceActivity.java @@ -28,8 +28,6 @@ import android.support.v7.app.ActionBarActivity; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; -import android.text.SpannedString; -import android.text.TextUtils; import android.text.style.BulletSpan; import android.text.style.StyleSpan; import android.view.View; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileActivity.java index 581145f8d..96f8bd983 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFileActivity.java @@ -452,27 +452,11 @@ public class EncryptFileActivity extends DrawerActivity implements EncryptActivi * Android's Action */ + // When sending to OpenKeychain Encrypt via share menu if (Intent.ACTION_SEND.equals(action) && type != null) { - // When sending to OpenKeychain Encrypt via share menu - /* if ("text/plain".equals(type)) { - // TODO handle, maybe forward to "encrypt text" activity? - - Plain text - String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); - if (sharedText != null) { - // handle like normal text encryption, override action and extras to later - // executeServiceMethod ACTION_ENCRYPT_TEXT in main actions - extras.putString(EXTRA_TEXT, sharedText); - extras.putBoolean(EXTRA_ASCII_ARMOR, true); - action = ACTION_ENCRYPT_TEXT; - } - - } else */ - { - // Files via content provider, override uri and action - uris.clear(); - uris.add(intent.<Uri>getParcelableExtra(Intent.EXTRA_STREAM)); - } + // Files via content provider, override uri and action + uris.clear(); + uris.add(intent.<Uri>getParcelableExtra(Intent.EXTRA_STREAM)); } if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java index bc0d3d387..6e6e52562 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java @@ -409,16 +409,29 @@ public class EncryptTextActivity extends DrawerActivity implements EncryptActivi private void handleActions(Intent intent) { String action = intent.getAction(); Bundle extras = intent.getExtras(); - // Should always be text/plain - // String type = intent.getType(); - ArrayList<Uri> uris = new ArrayList<Uri>(); + String type = intent.getType(); if (extras == null) { extras = new Bundle(); } - if (intent.getData() != null) { - uris.add(intent.getData()); + /* + * Android's Action + */ + + // When sending to OpenKeychain Encrypt via share menu + if (Intent.ACTION_SEND.equals(action) && type != null) { + // When sending to OpenKeychain Encrypt via share menu + if ("text/plain".equals(type)) { + String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); + if (sharedText != null) { + // handle like normal text encryption, override action and extras to later + // executeServiceMethod ACTION_ENCRYPT_TEXT in main actions + extras.putString(EXTRA_TEXT, sharedText); + action = ACTION_ENCRYPT_TEXT; + } + + } } String textData = extras.getString(EXTRA_TEXT); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index e7edc6058..4fda4cede 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -35,6 +35,7 @@ import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.CursorAdapter; +import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.ActionBarActivity; import android.support.v7.widget.SearchView; import android.view.ActionMode; @@ -55,9 +56,13 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.helper.ExportHelper; +import org.sufficientlysecure.keychain.helper.KeyUpdateHelper; +import org.sufficientlysecure.keychain.helper.Preferences; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; +import org.sufficientlysecure.keychain.ui.widget.ListAwareSwipeRefreshLayout; import org.sufficientlysecure.keychain.util.Highlighter; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Notify; @@ -74,10 +79,11 @@ import se.emilsjolander.stickylistheaders.StickyListHeadersListView; */ public class KeyListFragment extends LoaderFragment implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener, - LoaderManager.LoaderCallbacks<Cursor> { + LoaderManager.LoaderCallbacks<Cursor>, SwipeRefreshLayout.OnRefreshListener { private KeyListAdapter mAdapter; private StickyListHeadersListView mStickyList; + private ListAwareSwipeRefreshLayout mSwipeRefreshLayout; // saves the mode object for multiselect, needed for reset at some point private ActionMode mActionMode = null; @@ -120,9 +126,25 @@ public class KeyListFragment extends LoaderFragment } }); + mSwipeRefreshLayout = (ListAwareSwipeRefreshLayout) view.findViewById(R.id.key_list_swipe_container); + mSwipeRefreshLayout.setOnRefreshListener(this); + mSwipeRefreshLayout.setColorScheme( + R.color.android_purple_dark, + R.color.android_purple_light, + R.color.android_purple_dark, + R.color.android_purple_light); + mSwipeRefreshLayout.setStickyListHeadersListView(mStickyList); + return root; } + @Override + public void onResume() { + String[] servers = Preferences.getPreferences(getActivity()).getKeyServers(); + mSwipeRefreshLayout.setIsLocked(servers == null || servers.length == 0 || servers[0] == null); + super.onResume(); + } + /** * Define Adapter and Loader on create of Activity */ @@ -690,4 +712,16 @@ public class KeyListFragment extends LoaderFragment } + /** + * Implements OnRefreshListener for drag-to-refresh + */ + public void onRefresh() { + KeyUpdateHelper updateHelper = new KeyUpdateHelper(); + KeychainIntentServiceHandler finishedHandler = new KeychainIntentServiceHandler(getActivity()) { + public void handleMessage(Message message) { + mSwipeRefreshLayout.setRefreshing(false); + } + }; + updateHelper.updateAllKeys(getActivity(), finishedHandler); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java new file mode 100644 index 000000000..fa9444862 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Messenger; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; +import org.sufficientlysecure.keychain.util.Log; + +/** + * We can not directly create a dialog on the application context. + * This activity encapsulates a DialogFragment to emulate a dialog. + */ +public class PassphraseDialogActivity extends FragmentActivity { + public static final String MESSAGE_DATA_PASSPHRASE = "passphrase"; + + public static final String EXTRA_SECRET_KEY_ID = "secret_key_id"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // do not allow screenshots of passphrase input + // to prevent "too easy" passphrase theft by root apps + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { + getWindow().setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ); + } + + // this activity itself has no content view (see manifest) + + long keyId = getIntent().getLongExtra(EXTRA_SECRET_KEY_ID, 0); + + show(this, keyId); + } + + /** + * Shows passphrase dialog to cache a new passphrase the user enters for using it later for + * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks + * for a symmetric passphrase + */ + public static void show(final FragmentActivity context, final long keyId) { + DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { + public void run() { + // do NOT check if the key even needs a passphrase. that's not our job here. + PassphraseDialogFragment frag = new PassphraseDialogFragment(); + Bundle args = new Bundle(); + args.putLong(EXTRA_SECRET_KEY_ID, keyId); + + frag.setArguments(args); + + frag.show(context.getSupportFragmentManager(), "passphraseDialog"); + } + }); + } + + public static class PassphraseDialogFragment extends DialogFragment implements TextView.OnEditorActionListener { + private Messenger mMessenger; + private EditText mPassphraseEditText; + private View mInput, mProgress; + + CanonicalizedSecretKeyRing mSecretRing = null; + boolean mIsCancelled = false; + long mSubKeyId; + + /** + * Creates dialog + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity activity = getActivity(); + mSubKeyId = getArguments().getLong(EXTRA_SECRET_KEY_ID); + + CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(activity); + + alert.setTitle(R.string.title_authentication); + + String userId; + + if (mSubKeyId == Constants.key.symmetric || mSubKeyId == Constants.key.none) { + alert.setMessage(R.string.passphrase_for_symmetric_encryption); + } else { + String message; + try { + ProviderHelper helper = new ProviderHelper(activity); + mSecretRing = helper.getCanonicalizedSecretKeyRing( + KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(mSubKeyId)); + // yes the inner try/catch block is necessary, otherwise the final variable + // above can't be statically verified to have been set in all cases because + // the catch clause doesn't return. + try { + userId = mSecretRing.getPrimaryUserIdWithFallback(); + } catch (PgpGeneralException e) { + userId = null; + } + + /* Get key type for message */ + // find a master key id for our key + long masterKeyId = new ProviderHelper(getActivity()).getMasterKeyId(mSubKeyId); + CachedPublicKeyRing keyRing = new ProviderHelper(getActivity()).getCachedPublicKeyRing(masterKeyId); + // get the type of key (from the database) + CanonicalizedSecretKey.SecretKeyType keyType = keyRing.getSecretKeyType(mSubKeyId); + switch (keyType) { + case PASSPHRASE: + message = getString(R.string.passphrase_for, userId); + break; + case DIVERT_TO_CARD: + message = getString(R.string.yubikey_pin, userId); + break; + default: + message = "This should not happen!"; + break; + } + + } catch (ProviderHelper.NotFoundException e) { + alert.setTitle(R.string.title_key_not_found); + alert.setMessage(getString(R.string.key_not_found, mSubKeyId)); + alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }); + alert.setCancelable(false); + return alert.create(); + } + + alert.setMessage(message); + } + + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.passphrase_dialog, null); + alert.setView(view); + + mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase); + mInput = view.findViewById(R.id.input); + mProgress = view.findViewById(R.id.progress); + + alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + + // Hack to open keyboard. + // This is the only method that I found to work across all Android versions + // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/ + // Notes: * onCreateView can't be used because we want to add buttons to the dialog + // * opening in onActivityCreated does not work on Android 4.4 + mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + mPassphraseEditText.post(new Runnable() { + @Override + public void run() { + 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.setOnEditorActionListener(this); + + AlertDialog dialog = alert.create(); + dialog.setButton(DialogInterface.BUTTON_POSITIVE, + activity.getString(android.R.string.ok), (DialogInterface.OnClickListener) null); + + return dialog; + } + + @Override + public void onStart() { + super.onStart(); + + // Override the default behavior so the dialog is NOT dismissed on click + final Button positive = ((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_POSITIVE); + positive.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final String passphrase = mPassphraseEditText.getText().toString(); + + // Early breakout if we are dealing with a symmetric key + if (mSecretRing == null) { + PassphraseCacheService.addCachedPassphrase(getActivity(), Constants.key.symmetric, + passphrase, getString(R.string.passp_cache_notif_pwd)); + // also return passphrase back to activity + Intent returnIntent = new Intent(); + returnIntent.putExtra(MESSAGE_DATA_PASSPHRASE, passphrase); + getActivity().setResult(RESULT_OK, returnIntent); + dismiss(); + getActivity().finish(); + return; + } + + mInput.setVisibility(View.GONE); + mProgress.setVisibility(View.VISIBLE); + positive.setEnabled(false); + + new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(Void... params) { + try { + // wait some 100ms here, give the user time to appreciate the progress bar + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // never mind + } + // make sure this unlocks + return mSecretRing.getSecretKey(mSubKeyId).unlock(passphrase); + } catch (PgpGeneralException e) { + Toast.makeText(getActivity(), R.string.error_could_not_extract_private_key, + Toast.LENGTH_SHORT).show(); + + getActivity().setResult(RESULT_CANCELED); + dismiss(); + getActivity().finish(); + return false; + } + } + + /** Handle a good or bad passphrase. This happens in the UI thread! */ + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + + // if we were cancelled in the meantime, the result isn't relevant anymore + if (mIsCancelled) { + return; + } + + // if the passphrase was wrong, reset and re-enable the dialogue + if (!result) { + mPassphraseEditText.setText(""); + mPassphraseEditText.setError(getString(R.string.wrong_passphrase)); + mInput.setVisibility(View.VISIBLE); + mProgress.setVisibility(View.GONE); + positive.setEnabled(true); + return; + } + + // cache the new passphrase + Log.d(Constants.TAG, "Everything okay! Caching entered passphrase"); + + try { + PassphraseCacheService.addCachedPassphrase(getActivity(), mSubKeyId, + passphrase, mSecretRing.getPrimaryUserIdWithFallback()); + } catch (PgpGeneralException e) { + Log.e(Constants.TAG, "adding of a passphrase failed", e); + } + + // also return passphrase back to activity + Intent returnIntent = new Intent(); + returnIntent.putExtra(MESSAGE_DATA_PASSPHRASE, passphrase); + getActivity().setResult(RESULT_OK, returnIntent); + dismiss(); + getActivity().finish(); + } + }.execute(); + } + }); + + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + + // note we need no synchronization here, this variable is only accessed in the ui thread + mIsCancelled = true; + + // dismiss the dialogue + getActivity().setResult(RESULT_CANCELED); + dismiss(); + getActivity().finish(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + Log.d(Constants.TAG, "onDismiss"); + + // hide keyboard on dismiss + hideKeyboard(); + } + + private void hideKeyboard() { + InputMethodManager inputManager = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + + //check if no view has focus: + View v = getActivity().getCurrentFocus(); + if (v == null) + return; + + inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + + /** + * Associate the "done" button on the soft keyboard with the okay button in the view + */ + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (EditorInfo.IME_ACTION_DONE == actionId) { + AlertDialog dialog = ((AlertDialog) getDialog()); + Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + + bt.performClick(); + return true; + } + return false; + } + + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java index e4b455481..f48006f08 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java @@ -59,6 +59,7 @@ public class ViewKeyMainFragment extends LoaderFragment implements private View mActionEditDivider; private View mActionEncryptFiles; private View mActionEncryptText; + private View mActionEncryptTextText; private View mActionCertify; private View mActionCertifyText; private ImageView mActionCertifyImage; @@ -85,6 +86,7 @@ public class ViewKeyMainFragment extends LoaderFragment implements mActionEdit = view.findViewById(R.id.view_key_action_edit); mActionEditDivider = view.findViewById(R.id.view_key_action_edit_divider); mActionEncryptText = view.findViewById(R.id.view_key_action_encrypt_text); + mActionEncryptTextText = view.findViewById(R.id.view_key_action_encrypt_text_text); mActionEncryptFiles = view.findViewById(R.id.view_key_action_encrypt_files); mActionCertify = view.findViewById(R.id.view_key_action_certify); mActionCertifyText = view.findViewById(R.id.view_key_action_certify_text); @@ -235,6 +237,8 @@ public class ViewKeyMainFragment extends LoaderFragment implements mActionEdit.setEnabled(false); mActionCertify.setEnabled(false); mActionCertifyText.setEnabled(false); + mActionEncryptText.setEnabled(false); + mActionEncryptTextText.setEnabled(false); mActionEncryptFiles.setEnabled(false); } else { mActionEdit.setEnabled(true); @@ -243,10 +247,14 @@ public class ViewKeyMainFragment extends LoaderFragment implements if (!data.isNull(INDEX_UNIFIED_EXPIRY) && expiryDate.before(new Date())) { mActionCertify.setEnabled(false); mActionCertifyText.setEnabled(false); + mActionEncryptText.setEnabled(false); + mActionEncryptTextText.setEnabled(false); mActionEncryptFiles.setEnabled(false); } else { mActionCertify.setEnabled(true); mActionCertifyText.setEnabled(true); + mActionEncryptText.setEnabled(true); + mActionEncryptTextText.setEnabled(true); mActionEncryptFiles.setEnabled(true); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java index f25c49508..38ba675e9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java @@ -22,8 +22,11 @@ import android.content.res.ColorStateList; import android.database.Cursor; import android.graphics.Typeface; import android.support.v4.widget.CursorAdapter; -import android.text.Html; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; import android.text.format.DateFormat; +import android.text.style.StyleSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -143,29 +146,37 @@ public class SubkeysAdapter extends CursorAdapter { long keyId = cursor.getLong(INDEX_KEY_ID); String keyIdStr = PgpKeyHelper.convertKeyIdToHex(keyId); + vKeyId.setText(keyIdStr); // may be set with additional "stripped" later on - String algorithmStr = PgpKeyHelper.getAlgorithmInfo( + SpannableStringBuilder algorithmStr = new SpannableStringBuilder(); + algorithmStr.append(PgpKeyHelper.getAlgorithmInfo( context, cursor.getInt(INDEX_ALGORITHM), cursor.getInt(INDEX_KEY_SIZE), cursor.getString(INDEX_KEY_CURVE_OID) - ); - - vKeyId.setText(keyIdStr); + )); if (mSaveKeyringParcel != null && mSaveKeyringParcel.mStripSubKeys.contains(keyId)) { - algorithmStr += ", <b>" + context.getString(R.string.key_stripped) + "</b>"; + algorithmStr.append(", "); + final SpannableString boldStripped = new SpannableString( + context.getString(R.string.key_stripped) + ); + boldStripped.setSpan(new StyleSpan(Typeface.BOLD), 0, boldStripped.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + algorithmStr.append(boldStripped); } else { switch (SecretKeyType.fromNum(cursor.getInt(INDEX_HAS_SECRET))) { case GNU_DUMMY: - algorithmStr += ", " + context.getString(R.string.key_stripped); + algorithmStr.append(", "); + algorithmStr.append(context.getString(R.string.key_stripped)); break; case DIVERT_TO_CARD: - algorithmStr += ", " + context.getString(R.string.key_divert); + algorithmStr.append(", "); + algorithmStr.append(context.getString(R.string.key_divert)); break; case PASSPHRASE_EMPTY: - algorithmStr += ", " + context.getString(R.string.key_no_passphrase); + algorithmStr.append(", "); + algorithmStr.append(context.getString(R.string.key_no_passphrase)); break; case UNAVAILABLE: // don't show this on pub keys @@ -173,7 +184,7 @@ public class SubkeysAdapter extends CursorAdapter { break; } } - vKeyDetails.setText(Html.fromHtml(algorithmStr)); + vKeyDetails.setText(algorithmStr, TextView.BufferType.SPANNABLE); boolean isMasterKey = cursor.getInt(INDEX_RANK) == 0; if (isMasterKey) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ListAwareSwipeRefreshLayout.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ListAwareSwipeRefreshLayout.java new file mode 100644 index 000000000..58e8e81e9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ListAwareSwipeRefreshLayout.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2014 Daniel Albert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui.widget; + +import android.content.Context; +import android.support.v4.widget.SwipeRefreshLayout; +import android.util.AttributeSet; + +import org.sufficientlysecure.keychain.util.Log; + +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +public class ListAwareSwipeRefreshLayout extends SwipeRefreshLayout { + + + private StickyListHeadersListView mStickyListHeadersListView = null; + private boolean mIsLocked = false; + + /** + * Constructors + */ + public ListAwareSwipeRefreshLayout(Context context) { + super(context); + } + public ListAwareSwipeRefreshLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * Getters / Setters + */ + public void setStickyListHeadersListView(StickyListHeadersListView stickyListHeadersListView) { + mStickyListHeadersListView = stickyListHeadersListView; + } + public StickyListHeadersListView getStickyListHeadersListView() { + return mStickyListHeadersListView; + } + + public void setIsLocked(boolean locked) { + mIsLocked = locked; + Log.d("ListAwareSwipeRefreshLayout", (mIsLocked ? "is locked" : "not locked")); + } + public boolean getIsLocked() { + return mIsLocked; + } + + @Override + public boolean canChildScrollUp() { + if (mStickyListHeadersListView == null) + return super.canChildScrollUp(); + + return ( + mIsLocked + || + ( + mStickyListHeadersListView.getWrappedList().getChildCount() > 0 + && + ( + mStickyListHeadersListView.getTop() > 0 + || + mStickyListHeadersListView.getFirstVisiblePosition() > 0 + ) + ) + ); + } +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/key_list_fragment.xml b/OpenKeychain/src/main/res/layout/key_list_fragment.xml index f1da19b72..6af40106d 100644 --- a/OpenKeychain/src/main/res/layout/key_list_fragment.xml +++ b/OpenKeychain/src/main/res/layout/key_list_fragment.xml @@ -1,82 +1,78 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<org.sufficientlysecure.keychain.ui.widget.ListAwareSwipeRefreshLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/key_list_swipe_container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> - <!--rebuild functionality of ListFragment --> - - <se.emilsjolander.stickylistheaders.StickyListHeadersListView - android:id="@+id/key_list_list" + <FrameLayout android:layout_width="match_parent" - android:layout_height="match_parent" - android:clipToPadding="false" - android:drawSelectorOnTop="true" - android:fastScrollEnabled="true" - android:paddingBottom="16dp" - android:paddingLeft="16dp" - android:paddingRight="32dp" - android:scrollbarStyle="outsideOverlay" /> - - <LinearLayout - android:id="@+id/key_list_empty" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:gravity="center" - android:orientation="vertical" - android:visibility="visible"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - android:text="@string/key_list_empty_text1" - android:textAppearance="?android:attr/textAppearanceLarge" /> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - android:text="" - android:textAppearance="?android:attr/textAppearanceLarge" /> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="4dp" - android:gravity="center" - android:text="@string/key_list_empty_text2" - android:textAppearance="?android:attr/textAppearanceSmall" /> - - <Button - android:id="@+id/key_list_empty_button_create" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="4dp" - android:textSize="14sp" - android:text="@string/key_list_empty_button_create" - android:drawableLeft="@drawable/ic_action_new_account" - android:drawablePadding="8dp" - android:background="@drawable/button_edgy"/> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="4dp" + android:layout_height="match_parent"> + <se.emilsjolander.stickylistheaders.StickyListHeadersListView + android:id="@+id/key_list_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:drawSelectorOnTop="true" + android:fastScrollEnabled="true" + android:paddingBottom="16dp" + android:paddingLeft="16dp" + android:paddingRight="32dp" + android:scrollbarStyle="outsideOverlay" /> + <LinearLayout + android:id="@+id/key_list_empty" + android:layout_width="match_parent" + android:layout_height="match_parent" android:gravity="center" - android:text="@string/key_list_empty_text3" - android:textAppearance="?android:attr/textAppearanceSmall" /> - - <Button - android:id="@+id/key_list_empty_button_import" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="4dp" - android:textSize="14sp" - android:text="@string/key_list_empty_button_import" - android:drawableLeft="@drawable/ic_action_collection" - android:drawablePadding="8dp" - android:background="@drawable/button_edgy" /> - </LinearLayout> - -</FrameLayout> + android:orientation="vertical" + android:visibility="visible"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:text="@string/key_list_empty_text1" + android:textAppearance="?android:attr/textAppearanceLarge" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:text="" + android:textAppearance="?android:attr/textAppearanceLarge" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:gravity="center" + android:text="@string/key_list_empty_text2" + android:textAppearance="?android:attr/textAppearanceSmall" /> + <Button + android:id="@+id/key_list_empty_button_create" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:textSize="14sp" + android:text="@string/key_list_empty_button_create" + android:drawableLeft="@drawable/ic_action_new_account" + android:drawablePadding="8dp" + android:background="@drawable/button_edgy"/> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:gravity="center" + android:text="@string/key_list_empty_text3" + android:textAppearance="?android:attr/textAppearanceSmall" /> + <Button + android:id="@+id/key_list_empty_button_import" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:textSize="14sp" + android:text="@string/key_list_empty_button_import" + android:drawableLeft="@drawable/ic_action_collection" + android:drawablePadding="8dp" + android:background="@drawable/button_edgy" /> + </LinearLayout> + </FrameLayout> +</org.sufficientlysecure.keychain.ui.widget.ListAwareSwipeRefreshLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/view_key_main_fragment.xml b/OpenKeychain/src/main/res/layout/view_key_main_fragment.xml index 918e50dc4..6bcb216d7 100644 --- a/OpenKeychain/src/main/res/layout/view_key_main_fragment.xml +++ b/OpenKeychain/src/main/res/layout/view_key_main_fragment.xml @@ -103,6 +103,7 @@ android:orientation="horizontal"> <TextView + android:id="@+id/view_key_action_encrypt_text_text" android:paddingLeft="8dp" android:paddingRight="8dp" android:textAppearance="?android:attr/textAppearanceMedium" diff --git a/settings.gradle b/settings.gradle index 038e9da23..997dcc838 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,3 +15,4 @@ include ':extern:KeybaseLib:Lib' include ':extern:TokenAutoComplete:library' include ':extern:openpgp-card-nfc-lib:library' include ':extern:safeslinger-exchange' +include ':OpenKeychain-Test' |