diff options
Diffstat (limited to 'OpenKeychain/src/main/java/org')
117 files changed, 5526 insertions, 3351 deletions
diff --git a/OpenKeychain/src/main/java/org/spongycastle/openpgp/jcajce/JcaSkipMarkerPGPObjectFactory.java b/OpenKeychain/src/main/java/org/spongycastle/openpgp/jcajce/JcaSkipMarkerPGPObjectFactory.java new file mode 100644 index 000000000..f1cf9791a --- /dev/null +++ b/OpenKeychain/src/main/java/org/spongycastle/openpgp/jcajce/JcaSkipMarkerPGPObjectFactory.java @@ -0,0 +1,32 @@ +package org.spongycastle.openpgp.jcajce; + + +import java.io.IOException; +import java.io.InputStream; + +import org.spongycastle.openpgp.PGPMarker; + +/** This class wraps the regular PGPObjectFactory, changing its behavior to + * ignore all PGPMarker packets it encounters while reading. These packets + * carry no semantics of their own, and should be ignored according to + * RFC 4880. + * + * @see https://tools.ietf.org/html/rfc4880#section-5.8 + * @see org.spongycastle.openpgp.PGPMarker + * + */ +public class JcaSkipMarkerPGPObjectFactory extends JcaPGPObjectFactory { + + public JcaSkipMarkerPGPObjectFactory(InputStream in) { + super(in); + } + + @Override + public Object nextObject() throws IOException { + Object o = super.nextObject(); + while (o instanceof PGPMarker) { + o = super.nextObject(); + } + return o; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index 6a9656b28..69f1862ce 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -28,6 +28,7 @@ public final class Constants { public static final boolean DEBUG = BuildConfig.DEBUG; public static final boolean DEBUG_LOG_DB_QUERIES = false; + public static final boolean DEBUG_EXPLAIN_QUERIES = false; public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false; public static final boolean DEBUG_KEYSERVER_SYNC = false; @@ -40,22 +41,31 @@ public final class Constants { public static final String CUSTOM_CONTACT_DATA_MIME_TYPE = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.key"; public static final String PROVIDER_AUTHORITY = BuildConfig.PROVIDER_CONTENT_AUTHORITY; - public static final String TEMPSTORAGE_AUTHORITY = BuildConfig.APPLICATION_ID + ".tempstorage"; + public static final String TEMP_FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".tempstorage"; public static final String CLIPBOARD_LABEL = "Keychain"; - // as defined in http://tools.ietf.org/html/rfc3156, section 7 - public static final String NFC_MIME = "application/pgp-keys"; - // as defined in http://tools.ietf.org/html/rfc3156 - // we don't use application/pgp-encrypted as it only holds the version number - public static final String ENCRYPTED_FILES_MIME = "application/octet-stream"; - public static final String ENCRYPTED_TEXT_MIME = "text/plain"; - - public static final String FILE_EXTENSION_PGP_MAIN = ".gpg"; - public static final String FILE_EXTENSION_PGP_ALTERNATE = ".pgp"; + public static final String MIME_TYPE_KEYS = "application/pgp-keys"; + // NOTE: don't use application/pgp-encrypted It only holds the version number! + public static final String MIME_TYPE_ENCRYPTED = "application/octet-stream"; + // NOTE: Non-standard alternative, better use this, because application/octet-stream is too unspecific! + // also see https://tools.ietf.org/html/draft-bray-pgp-message-00 + public static final String MIME_TYPE_ENCRYPTED_ALTERNATE = "application/pgp-message"; + public static final String MIME_TYPE_TEXT = "text/plain"; + + public static final String FILE_EXTENSION_PGP_MAIN = ".pgp"; + public static final String FILE_EXTENSION_PGP_ALTERNATE = ".gpg"; public static final String FILE_EXTENSION_ASC = ".asc"; + public static final String FILE_BACKUP_PREFIX = "backup_"; + public static final String FILE_EXTENSION_BACKUP_SECRET = ".sec.asc"; + public static final String FILE_EXTENSION_BACKUP_PUBLIC = ".pub.asc"; + public static final String FILE_ENCRYPTED_BACKUP_PREFIX = "backup_"; + // actually it is ASCII Armor, so .asc would be more accurate, but Android displays a nice icon for .pgp files! + public static final String FILE_EXTENSION_ENCRYPTED_BACKUP_SECRET = ".sec.pgp"; + public static final String FILE_EXTENSION_ENCRYPTED_BACKUP_PUBLIC = ".pub.pgp"; + // used by QR Codes (Guardian Project, Monkeysphere compatiblity) public static final String FINGERPRINT_SCHEME = "openpgp4fpr"; @@ -70,11 +80,13 @@ public final class Constants { public static final int TEMPFILE_TTL = 24 * 60 * 60 * 1000; // 1 day + // the maximal length of plaintext to read in encrypt/decrypt text activities + public static final int TEXT_LENGTH_LIMIT = 1024 * 50; + public static final String SAFESLINGER_SERVER = "safeslinger-openpgp.appspot.com"; public static final class Path { public static final File APP_DIR = new File(Environment.getExternalStorageDirectory(), "OpenKeychain"); - public static final File APP_DIR_FILE = new File(APP_DIR, "export.asc"); } public static final class Notification { @@ -92,7 +104,6 @@ public final class Constants { public static final String CACHED_CONSOLIDATE = "cachedConsolidate"; public static final String SEARCH_KEYSERVER = "search_keyserver_pref"; public static final String SEARCH_KEYBASE = "search_keybase_pref"; - public static final String USE_DEFAULT_YUBIKEY_PIN = "useDefaultYubikeyPin"; public static final String USE_NUMKEYPAD_FOR_YUBIKEY_PIN = "useNumKeypadForYubikeyPin"; public static final String ENCRYPT_FILENAMES = "encryptFilenames"; public static final String FILE_USE_COMPRESSION = "useFileCompression"; @@ -142,6 +153,7 @@ public final class Constants { public static final class key { public static final int none = 0; public static final int symmetric = -1; + public static final int backup_code = -2; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java index 311ef2d3b..5d97dac8a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java @@ -33,7 +33,7 @@ import android.widget.Toast; import org.spongycastle.jce.provider.BouncyCastleProvider; import org.sufficientlysecure.keychain.provider.KeychainDatabase; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; @@ -91,16 +91,18 @@ public class KeychainApplication extends Application { } brandGlowEffect(getApplicationContext(), - FormattingUtils.getColorFromAttr(getApplicationContext(), R.attr.colorPrimary)); + FormattingUtils.getColorFromAttr(getApplicationContext(), R.attr.colorPrimary)); setupAccountAsNeeded(this); // Update keyserver list as needed Preferences.getPreferences(this).upgradePreferences(this); - TlsHelper.addStaticCA("pool.sks-keyservers.net", getAssets(), "sks-keyservers.netCA.cer"); + TlsHelper.addPinnedCertificate("hkps.pool.sks-keyservers.net", getAssets(), "hkps.pool.sks-keyservers.net.CA.cer"); + TlsHelper.addPinnedCertificate("pgp.mit.edu", getAssets(), "pgp.mit.edu.cer"); + TlsHelper.addPinnedCertificate("api.keybase.io", getAssets(), "api.keybase.io.CA.cer"); - TemporaryStorageProvider.cleanUp(this); + TemporaryFileProvider.cleanUp(this); if (!checkConsolidateRecovery()) { // force DB upgrade, https://github.com/open-keychain/open-keychain/issues/1334 diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/experimental/BitInputStream.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/experimental/BitInputStream.java new file mode 100644 index 000000000..111e0b366 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/experimental/BitInputStream.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) Andreas Jakl + * + * 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.experimental; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * The BitInputStream allows reading individual bits from a + * general Java InputStream. + * Like the various Stream-classes from Java, the BitInputStream + * has to be created based on another Input stream. It provides + * a function to read the next bit from the stream, as well as to read multiple + * bits at once and write the resulting data into an integer value. + * <p/> + * source: http://developer.nokia.com/Community/Wiki/Bit_Input/Output_Stream_utility_classes_for_efficient_data_transfer + */ +public class BitInputStream { + /** + * The Java InputStream this class is working on. + */ + private InputStream iIs; + + /** + * The buffer containing the currently processed + * byte of the input stream. + */ + private int iBuffer; + + /** + * Next bit of the current byte value that the user will + * get. If it's 8, the next bit will be read from the + * next byte of the InputStream. + */ + private int iNextBit = 8; + + /** + * Create a new bit input stream based on an existing Java InputStream. + * + * @param aIs the input stream this class should read the bits from. + */ + public BitInputStream(InputStream aIs) { + iIs = aIs; + } + + /** + * Read a specified number of bits and return them combined as + * an integer value. The bits are written to the integer + * starting at the highest bit ( << aNumberOfBits ), going down + * to the lowest bit ( << 0 ) + * + * @param aNumberOfBits defines how many bits to read from the stream. + * @return integer value containing the bits read from the stream. + * @throws IOException + */ + synchronized public int readBits(final int aNumberOfBits, boolean stuffIfEnd) + throws IOException { + int value = 0; + for (int i = aNumberOfBits - 1; i >= 0; i--) { + value |= (readBit(stuffIfEnd) << i); + } + return value; + } + + synchronized public int available() { + try { + return (8 - iNextBit) + iIs.available() * 8; // bytestream to bitstream available + } catch (Exception e) { + return 0; + } + } + + /** + * Read the next bit from the stream. + * + * @return 0 if the bit is 0, 1 if the bit is 1. + * @throws IOException + */ + synchronized public int readBit(boolean stuffIfEnd) throws IOException { + if (iIs == null) + throw new IOException("Already closed"); + + if (iNextBit == 8) { + iBuffer = iIs.read(); + + if (iBuffer == -1) { + if (stuffIfEnd) { + return 1; + } else { + throw new EOFException(); + } + } + + iNextBit = 0; + } + + int bit = iBuffer & (1 << iNextBit); + iNextBit++; + + bit = (bit == 0) ? 0 : 1; + + return bit; + } + + /** + * Close the underlying input stream. + * + * @throws IOException + */ + public void close() throws IOException { + iIs.close(); + iIs = null; + } +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/experimental/SentenceConfirm.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/experimental/SentenceConfirm.java new file mode 100644 index 000000000..0374d878c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/experimental/SentenceConfirm.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2014 Jake McGinty (Open Whisper Systems) + * + * 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.experimental; + +import android.content.Context; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +/** + * From https://github.com/mcginty/TextSecure/tree/mnemonic-poem + */ +public class SentenceConfirm { + Context context; + List<String> n, vi, vt, adj, adv, p, art; + + public SentenceConfirm(Context context) { + this.context = context; + try { + n = readFile(R.raw.fp_sentence_nouns); + vi = readFile(R.raw.fp_sentence_verbs_i); + vt = readFile(R.raw.fp_sentence_verbs_t); + adj = readFile(R.raw.fp_sentence_adjectives); + adv = readFile(R.raw.fp_sentence_adverbs); + p = readFile(R.raw.fp_sentence_prepositions); + art = readFile(R.raw.fp_sentence_articles); + } catch (IOException e) { + Log.e(Constants.TAG, "Reading sentence files failed", e); + } + } + + List<String> readFile(int resId) throws IOException { + if (context.getApplicationContext() == null) { + throw new AssertionError("app context can't be null"); + } + + BufferedReader in = new BufferedReader(new InputStreamReader( + context.getApplicationContext() + .getResources() + .openRawResource(resId))); + List<String> words = new ArrayList<>(); + String word = in.readLine(); + while (word != null) { + words.add(word); + word = in.readLine(); + } + in.close(); + return words; + } + + public String fromBytes(final byte[] bytes, int desiredBytes) throws IOException { + BitInputStream bin = new BitInputStream(new ByteArrayInputStream(bytes)); + EntropyString fingerprint = new EntropyString(); + + while (fingerprint.getBits() < (desiredBytes * 8)) { + if (!fingerprint.isEmpty()) { + fingerprint.append("\n\n"); + } + try { + fingerprint.append(getSentence(bin)); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException when creating the sentence"); + throw e; + } + } + return fingerprint.toString(); + } + + /** + * Grab a word for a list of them using the necessary bits to choose from a BitInputStream + * + * @param words the list of words to select from + * @param bin the bit input stream to encode from + * @return A Pair of the word and the number of bits consumed from the stream + */ + private EntropyString getWord(List<String> words, BitInputStream bin) throws IOException { + final int neededBits = log(words.size(), 2); + Log.d(Constants.TAG, "need: " + neededBits + " bits of entropy"); + Log.d(Constants.TAG, "available: " + bin.available() + " bits"); + if (neededBits > bin.available()) { + Log.d(Constants.TAG, "stuffed with " + (neededBits - bin.available()) + " ones!"); + } + int bits = bin.readBits(neededBits, true); + Log.d(Constants.TAG, "got word " + words.get(bits) + " with " + neededBits + " bits of entropy"); + return new EntropyString(words.get(bits), neededBits); + } + + private EntropyString getNounPhrase(BitInputStream bits) throws IOException { + final EntropyString phrase = new EntropyString(); + phrase.append(getWord(art, bits)).append(" "); + if (bits.readBit(true) != 0) { + phrase.append(getWord(adj, bits)).append(" "); + } + phrase.incBits(); + + phrase.append(getWord(n, bits)); + Log.d(Constants.TAG, "got phrase " + phrase + " with " + phrase.getBits() + " bits of entropy"); + return phrase; + } + + EntropyString getSentence(BitInputStream bits) throws IOException { + final EntropyString sentence = new EntropyString(); + sentence.append(getNounPhrase(bits)); // Subject + if (bits.readBit(true) != 0) { + sentence.append(" ").append(getWord(vt, bits)); // Transitive verb + sentence.append(" ").append(getNounPhrase(bits)); // Object of transitive verb + } else { + sentence.append(" ").append(getWord(vi, bits)); // Intransitive verb + } + sentence.incBits(); + + if (bits.readBit(true) != 0) { + sentence.append(" ").append(getWord(adv, bits)); // Adverb + } + + sentence.incBits(); + if (bits.readBit(true) != 0) { + sentence.append(" ").append(getWord(p, bits)); // Preposition + sentence.append(" ").append(getNounPhrase(bits)); // Object of preposition + } + sentence.incBits(); + Log.d(Constants.TAG, "got sentence '" + sentence + "' with " + sentence.getBits() + " bits of entropy"); + + // uppercase first character, end with dot (without increasing the bits) + sentence.getBuilder().replace(0, 1, + Character.toString(Character.toUpperCase(sentence.getBuilder().charAt(0)))); + sentence.getBuilder().append("."); + + return sentence; + } + + public static class EntropyString { + private StringBuilder builder; + private int bits; + + public EntropyString(String phrase, int bits) { + this.builder = new StringBuilder(phrase); + this.bits = bits; + } + + public EntropyString() { + this("", 0); + } + + public StringBuilder getBuilder() { + return builder; + } + + public boolean isEmpty() { + return builder.length() == 0; + } + + public EntropyString append(EntropyString phrase) { + builder.append(phrase); + bits += phrase.getBits(); + return this; + } + + public EntropyString append(String string) { + builder.append(string); + return this; + } + + public int getBits() { + return bits; + } + + public void setBits(int bits) { + this.bits = bits; + } + + public void incBits() { + bits += 1; + } + + @Override + public String toString() { + return builder.toString(); + } + } + + private static int log(int x, int base) { + return (int) (Math.log(x) / Math.log(base)); + } + +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/experimental/WordConfirm.java index 43ccac24f..daf63ea9e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/experimental/WordConfirm.java @@ -15,12 +15,13 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package org.sufficientlysecure.keychain.ui.util; +package org.sufficientlysecure.keychain.experimental; import android.content.Context; import org.spongycastle.util.Arrays; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.util.Log; import java.io.BufferedReader; @@ -29,7 +30,7 @@ import java.io.InputStreamReader; import java.util.ArrayList; import java.util.BitSet; -public class ExperimentalWordConfirm { +public class WordConfirm { public static String getWords(Context context, byte[] fingerprintBlob) { ArrayList<String> words = new ArrayList<>(); @@ -37,7 +38,7 @@ public class ExperimentalWordConfirm { BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader( - context.getAssets().open("word_confirm_list.txt"), + context.getResources().openRawResource(R.raw.fp_word_list), "UTF-8" )); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java index d91dd28bc..869d107ab 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/CloudSearch.java @@ -24,6 +24,9 @@ import java.net.Proxy; import java.util.ArrayList; import java.util.Vector; +import android.support.annotation.NonNull; + + /** * Search two or more types of server for online keys. */ @@ -31,8 +34,8 @@ public class CloudSearch { private final static long SECONDS = 1000; - public static ArrayList<ImportKeysListEntry> search(final String query, Preferences.CloudSearchPrefs cloudPrefs, - final Proxy proxy) + public static ArrayList<ImportKeysListEntry> search( + @NonNull final String query, Preferences.CloudSearchPrefs cloudPrefs, @NonNull Proxy proxy) throws Keyserver.CloudSearchFailureException { final ArrayList<Keyserver> servers = new ArrayList<>(); @@ -40,10 +43,10 @@ public class CloudSearch { final Vector<Keyserver.CloudSearchFailureException> problems = new Vector<>(); if (cloudPrefs.searchKeyserver) { - servers.add(new HkpKeyserver(cloudPrefs.keyserver)); + servers.add(new HkpKeyserver(cloudPrefs.keyserver, proxy)); } if (cloudPrefs.searchKeybase) { - servers.add(new KeybaseKeyserver()); + servers.add(new KeybaseKeyserver(proxy)); } final ImportKeysList results = new ImportKeysList(servers.size()); @@ -53,7 +56,7 @@ public class CloudSearch { @Override public void run() { try { - results.addAll(keyserver.search(query, proxy)); + results.addAll(keyserver.search(query)); } catch (Keyserver.CloudSearchFailureException e) { problems.add(e); } @@ -68,7 +71,7 @@ public class CloudSearch { // wait for either all the searches to come back, or 10 seconds. If using proxy, wait 30 seconds. synchronized (results) { try { - if (proxy != null) { + if (proxy == Proxy.NO_PROXY) { results.wait(30 * SECONDS); } else { results.wait(10 * SECONDS); @@ -77,7 +80,7 @@ public class CloudSearch { // kill threads that haven't returned yet thread.interrupt(); } - } catch (InterruptedException e) { + } catch (InterruptedException ignored) { } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java index 558b8ce7d..c2190318b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java @@ -23,6 +23,7 @@ import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; + import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.pgp.PgpHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; @@ -45,6 +46,8 @@ import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; +import android.support.annotation.NonNull; + import de.measite.minidns.Client; import de.measite.minidns.Question; import de.measite.minidns.Record; @@ -73,6 +76,7 @@ public class HkpKeyserver extends Keyserver { private String mHost; private short mPort; + private Proxy mProxy; private boolean mSecure; /** @@ -149,17 +153,17 @@ public class HkpKeyserver extends Keyserver { * connect using {@link #PORT_DEFAULT}. However, port may be specified after colon * ("<code>hostname:port</code>", eg. "<code>p80.pool.sks-keyservers.net:80</code>"). */ - public HkpKeyserver(String hostAndPort) { + public HkpKeyserver(String hostAndPort, Proxy proxy) { String host = hostAndPort; short port = PORT_DEFAULT; boolean secure = false; String[] parts = hostAndPort.split(":"); if (parts.length > 1) { if (!parts[0].contains(".")) { // This is not a domain or ip, so it must be a protocol name - if (parts[0].equalsIgnoreCase("hkps") || parts[0].equalsIgnoreCase("https")) { + if ("hkps".equalsIgnoreCase(parts[0]) || "https".equalsIgnoreCase(parts[0])) { secure = true; port = PORT_DEFAULT_HKPS; - } else if (!parts[0].equalsIgnoreCase("hkp") && !parts[0].equalsIgnoreCase("http")) { + } else if (!"hkp".equalsIgnoreCase(parts[0]) && !"http".equalsIgnoreCase(parts[0])) { throw new IllegalArgumentException("Protocol " + parts[0] + " is unknown"); } host = parts[1]; @@ -176,16 +180,18 @@ public class HkpKeyserver extends Keyserver { } mHost = host; mPort = port; + mProxy = proxy; mSecure = secure; } - public HkpKeyserver(String host, short port) { - this(host, port, false); + public HkpKeyserver(String host, short port, Proxy proxy) { + this(host, port, proxy, false); } - public HkpKeyserver(String host, short port, boolean secure) { + public HkpKeyserver(String host, short port, Proxy proxy, boolean secure) { mHost = host; mPort = port; + mProxy = proxy; mSecure = secure; } @@ -196,19 +202,23 @@ public class HkpKeyserver extends Keyserver { /** * returns a client with pinned certificate if necessary * - * @param url url to be queried by client + * @param url url to be queried by client * @param proxy proxy to be used by client - * @return client with a pinned certificate if necesary + * @return client with a pinned certificate if necessary */ public static OkHttpClient getClient(URL url, Proxy proxy) throws IOException { OkHttpClient client = new OkHttpClient(); try { - TlsHelper.pinCertificateIfNecessary(client, url); + 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); @@ -221,14 +231,14 @@ public class HkpKeyserver extends Keyserver { return client; } - private String query(String request, Proxy proxy) throws QueryFailedException, HttpError { + private String query(String request, @NonNull Proxy proxy) throws QueryFailedException, HttpError { try { URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + request); Log.d(Constants.TAG, "hkp keyserver query: " + url + " Proxy: " + proxy); OkHttpClient client = getClient(url, proxy); 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 if (response.isSuccessful()) { return responseBody; @@ -238,20 +248,15 @@ public class HkpKeyserver extends Keyserver { } catch (IOException e) { Log.e(Constants.TAG, "IOException at HkpKeyserver", e); throw new QueryFailedException("Keyserver '" + mHost + "' is unavailable. Check your Internet connection!" + - proxy == null?"":" Using proxy " + proxy); + (proxy == Proxy.NO_PROXY ? "" : " Using proxy " + proxy)); } } /** * Results are sorted by creation date of key! - * - * @param query - * @return - * @throws QueryFailedException - * @throws QueryNeedsRepairException */ @Override - public ArrayList<ImportKeysListEntry> search(String query, Proxy proxy) throws QueryFailedException, + public ArrayList<ImportKeysListEntry> search(String query) throws QueryFailedException, QueryNeedsRepairException { ArrayList<ImportKeysListEntry> results = new ArrayList<>(); @@ -269,7 +274,7 @@ public class HkpKeyserver extends Keyserver { String data; try { - data = query(request, proxy); + data = query(request, mProxy); } catch (HttpError e) { if (e.getData() != null) { Log.d(Constants.TAG, "returned error data: " + e.getData().toLowerCase(Locale.ENGLISH)); @@ -299,30 +304,46 @@ public class HkpKeyserver extends Keyserver { entry.setQuery(query); entry.addOrigin(getUrlPrefix() + mHost + ":" + mPort); - int bitSize = Integer.parseInt(matcher.group(3)); - entry.setBitStrength(bitSize); - int algorithmId = Integer.decode(matcher.group(2)); - entry.setAlgorithm(KeyFormattingUtils.getAlgorithmInfo(algorithmId, bitSize, null)); - // group 1 contains the full fingerprint (v4) or the long key id if available // see https://bitbucket.org/skskeyserver/sks-keyserver/pull-request/12/fixes-for-machine-readable-indexes/diff String fingerprintOrKeyId = matcher.group(1).toLowerCase(Locale.ENGLISH); - if (fingerprintOrKeyId.length() > 16) { + if (fingerprintOrKeyId.length() == 40) { entry.setFingerprintHex(fingerprintOrKeyId); entry.setKeyIdHex("0x" + fingerprintOrKeyId.substring(fingerprintOrKeyId.length() - 16, fingerprintOrKeyId.length())); - } else { + } else if (fingerprintOrKeyId.length() == 16) { // set key id only entry.setKeyIdHex("0x" + fingerprintOrKeyId); + } else { + Log.e(Constants.TAG, "Wrong length for fingerprint/long key id."); + // skip this key + continue; } - final long creationDate = Long.parseLong(matcher.group(4)); - final GregorianCalendar tmpGreg = new GregorianCalendar(TimeZone.getTimeZone("UTC")); - tmpGreg.setTimeInMillis(creationDate * 1000); - entry.setDate(tmpGreg.getTime()); + try { + int bitSize = Integer.parseInt(matcher.group(3)); + entry.setBitStrength(bitSize); + int algorithmId = Integer.decode(matcher.group(2)); + entry.setAlgorithm(KeyFormattingUtils.getAlgorithmInfo(algorithmId, bitSize, null)); + + final long creationDate = Long.parseLong(matcher.group(4)); + final GregorianCalendar tmpGreg = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + tmpGreg.setTimeInMillis(creationDate * 1000); + entry.setDate(tmpGreg.getTime()); + } catch (NumberFormatException e) { + Log.e(Constants.TAG, "Conversation for bit size, algorithm, or creation date failed.", e); + // skip this key + continue; + } - entry.setRevoked(matcher.group(6).contains("r")); - entry.setExpired(matcher.group(6).contains("e")); + try { + entry.setRevoked(matcher.group(6).contains("r")); + entry.setExpired(matcher.group(6).contains("e")); + } catch (NullPointerException e) { + Log.e(Constants.TAG, "Check for revocation or expiry failed.", e); + // skip this key + continue; + } ArrayList<String> userIds = new ArrayList<>(); final String uidLines = matcher.group(7); @@ -340,6 +361,10 @@ public class HkpKeyserver extends Keyserver { tmp = URLDecoder.decode(tmp, "UTF8"); } catch (UnsupportedEncodingException ignored) { // will never happen, because "UTF8" is supported + } catch (IllegalArgumentException e) { + Log.e(Constants.TAG, "User ID encoding broken", e); + // skip this user id + continue; } } userIds.add(tmp); @@ -353,25 +378,28 @@ public class HkpKeyserver extends Keyserver { } @Override - public String get(String keyIdHex, Proxy proxy) throws QueryFailedException { + public String get(String keyIdHex) throws QueryFailedException { String request = "/pks/lookup?op=get&options=mr&search=" + keyIdHex; - Log.d(Constants.TAG, "hkp keyserver get: " + request + " using Proxy: " + proxy); + Log.d(Constants.TAG, "hkp keyserver get: " + request + " using Proxy: " + mProxy); String data; try { - data = query(request, proxy); + data = query(request, mProxy); } catch (HttpError httpError) { Log.d(Constants.TAG, "Failed to get key at HkpKeyserver", httpError); throw new QueryFailedException("not found"); } + if (data == null) { + throw new QueryFailedException("data is null"); + } Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(data); if (matcher.find()) { return matcher.group(1); } - return null; + throw new QueryFailedException("data is null"); } @Override - public void add(String armoredKey, Proxy proxy) throws AddKeyException { + public void add(String armoredKey) throws AddKeyException { try { String path = "/pks/add"; String params; @@ -382,7 +410,7 @@ public class HkpKeyserver extends Keyserver { } URL url = new URL(getUrlPrefix() + mHost + ":" + mPort + path); - Log.d(Constants.TAG, "hkp keyserver add: " + url.toString()); + Log.d(Constants.TAG, "hkp keyserver add: " + url); Log.d(Constants.TAG, "params: " + params); RequestBody body = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), params); @@ -394,7 +422,7 @@ public class HkpKeyserver extends Keyserver { .post(body) .build(); - Response response = getClient(url, proxy).newCall(request).execute(); + Response response = getClient(url, mProxy).newCall(request).execute(); Log.d(Constants.TAG, "response code: " + response.code()); Log.d(Constants.TAG, "answer: " + response.body().string()); @@ -411,16 +439,15 @@ public class HkpKeyserver extends Keyserver { @Override public String toString() { - return mHost + ":" + mPort; + return getUrlPrefix() + mHost + ":" + mPort; } /** * Tries to find a server responsible for a given domain * * @return A responsible Keyserver or null if not found. - * TODO: PHILIP Add proxy functionality */ - public static HkpKeyserver resolve(String domain) { + public static HkpKeyserver resolve(String domain, Proxy proxy) { try { Record[] records = new Client().query(new Question("_hkp._tcp." + domain, Record.TYPE.SRV)).getAnswers(); if (records.length > 0) { @@ -435,7 +462,7 @@ public class HkpKeyserver extends Keyserver { Record record = records[0]; // This is our best choice if (record.getPayload().getType() == Record.TYPE.SRV) { return new HkpKeyserver(((SRV) record.getPayload()).getName(), - (short) ((SRV) record.getPayload()).getPort()); + (short) ((SRV) record.getPayload()).getPort(), proxy); } } } catch (Exception ignored) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java index c2865410e..e4cd6738b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/KeybaseKeyserver.java @@ -19,12 +19,13 @@ package org.sufficientlysecure.keychain.keyimport; import com.textuality.keybase.lib.KeybaseException; import com.textuality.keybase.lib.Match; -import com.textuality.keybase.lib.Search; +import com.textuality.keybase.lib.KeybaseQuery; import com.textuality.keybase.lib.User; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.OkHttpKeybaseClient; import java.net.Proxy; import java.util.ArrayList; @@ -32,10 +33,15 @@ import java.util.List; public class KeybaseKeyserver extends Keyserver { public static final String ORIGIN = "keybase:keybase.io"; - private String mQuery; + + Proxy mProxy; + + public KeybaseKeyserver(Proxy proxy) { + mProxy = proxy; + } @Override - public ArrayList<ImportKeysListEntry> search(String query, Proxy proxy) throws QueryFailedException, + public ArrayList<ImportKeysListEntry> search(String query) throws QueryFailedException, QueryNeedsRepairException { ArrayList<ImportKeysListEntry> results = new ArrayList<>(); @@ -46,12 +52,13 @@ public class KeybaseKeyserver extends Keyserver { if (query.isEmpty()) { throw new QueryTooShortException(); } - mQuery = query; try { - Iterable<Match> matches = Search.search(query, proxy); + KeybaseQuery keybaseQuery = new KeybaseQuery(new OkHttpKeybaseClient()); + keybaseQuery.setProxy(mProxy); + Iterable<Match> matches = keybaseQuery.search(query); for (Match match : matches) { - results.add(makeEntry(match)); + results.add(makeEntry(match, query)); } } catch (KeybaseException e) { Log.e(Constants.TAG, "keybase result parsing error", e); @@ -61,9 +68,9 @@ public class KeybaseKeyserver extends Keyserver { return results; } - private ImportKeysListEntry makeEntry(Match match) throws KeybaseException { + private ImportKeysListEntry makeEntry(Match match, String query) throws KeybaseException { final ImportKeysListEntry entry = new ImportKeysListEntry(); - entry.setQuery(mQuery); + entry.setQuery(query); entry.addOrigin(ORIGIN); entry.setRevoked(false); // keybase doesn’t say anything about revoked keys @@ -99,16 +106,18 @@ public class KeybaseKeyserver extends Keyserver { } @Override - public String get(String id, Proxy proxy) throws QueryFailedException { + public String get(String id) throws QueryFailedException { try { - return User.keyForUsername(id, proxy); + KeybaseQuery keybaseQuery = new KeybaseQuery(new OkHttpKeybaseClient()); + keybaseQuery.setProxy(mProxy); + return User.keyForUsername(keybaseQuery, id); } catch (KeybaseException e) { throw new QueryFailedException(e.getMessage()); } } @Override - public void add(String armoredKey, Proxy proxy) throws AddKeyException { + public void add(String armoredKey) throws AddKeyException { throw new AddKeyException(); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java index 640b39f44..00e8d6ac5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/Keyserver.java @@ -62,19 +62,19 @@ public abstract class Keyserver { * query too short _or_ too many responses */ public static class QueryTooShortOrTooManyResponsesException extends QueryNeedsRepairException { - private static final long serialVersionUID = 2703768928624654514L; + private static final long serialVersionUID = 2703768928624654518L; } public static class AddKeyException extends Exception { private static final long serialVersionUID = -507574859137295530L; } - public abstract List<ImportKeysListEntry> search(String query, Proxy proxy) throws QueryFailedException, - QueryNeedsRepairException; + public abstract List<ImportKeysListEntry> search(String query) + throws QueryFailedException, QueryNeedsRepairException; - public abstract String get(String keyIdHex, Proxy proxy) throws QueryFailedException; + public abstract String get(String keyIdHex) throws QueryFailedException; - public abstract void add(String armoredKey, Proxy proxy) throws AddKeyException; + public abstract void add(String armoredKey) throws AddKeyException; public static String readAll(InputStream in, String encoding) throws IOException { ByteArrayOutputStream raw = new ByteArrayOutputStream(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java index 998ec3ad4..e5a128e32 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java @@ -1,5 +1,7 @@ package org.sufficientlysecure.keychain.linked; +import android.content.Context; + import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; @@ -8,7 +10,6 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.BasicHttpParams; import org.json.JSONException; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.linked.resources.DnsResource; import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource; import org.sufficientlysecure.keychain.linked.resources.GithubResource; import org.sufficientlysecure.keychain.linked.resources.TwitterResource; @@ -32,8 +33,6 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import android.content.Context; - public abstract class LinkedTokenResource extends LinkedResource { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BackupOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BackupOperation.java new file mode 100644 index 000000000..ae9a2c180 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BackupOperation.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.operations; + + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; +import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel; +import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; +import org.sufficientlysecure.keychain.service.BackupKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; + + +/** + * An operation class which implements high level backup + * operations. + * This class receives a source and/or destination of keys as input and performs + * all steps for this backup. + * + * @see org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter#getSelectedEntries() + * For the backup operation, the input consists of a set of key ids and + * either the name of a file or an output uri to write to. + */ +public class BackupOperation extends BaseOperation<BackupKeyringParcel> { + + private static final String[] PROJECTION = new String[] { + KeyRings.MASTER_KEY_ID, + KeyRings.PUBKEY_DATA, + KeyRings.PRIVKEY_DATA, + KeyRings.HAS_ANY_SECRET + }; + private static final int INDEX_MASTER_KEY_ID = 0; + private static final int INDEX_PUBKEY_DATA = 1; + private static final int INDEX_SECKEY_DATA = 2; + private static final int INDEX_HAS_ANY_SECRET = 3; + + public BackupOperation(Context context, ProviderHelper providerHelper, Progressable + progressable) { + super(context, providerHelper, progressable); + } + + public BackupOperation(Context context, ProviderHelper providerHelper, + Progressable progressable, AtomicBoolean cancelled) { + super(context, providerHelper, progressable, cancelled); + } + + @NonNull + public ExportResult execute(@NonNull BackupKeyringParcel backupInput, @Nullable CryptoInputParcel cryptoInput) { + + OperationLog log = new OperationLog(); + if (backupInput.mMasterKeyIds != null) { + log.add(LogType.MSG_BACKUP, 0, backupInput.mMasterKeyIds.length); + } else { + log.add(LogType.MSG_BACKUP_ALL, 0); + } + + try { + + boolean nonEncryptedOutput = backupInput.mSymmetricPassphrase == null; + + Uri backupOutputUri = nonEncryptedOutput + ? backupInput.mOutputUri + : TemporaryFileProvider.createFile(mContext); + + int exportedDataSize; + + { // export key data, and possibly return if we don't encrypt + + DataOutputStream outStream = new DataOutputStream(new BufferedOutputStream( + mContext.getContentResolver().openOutputStream(backupOutputUri))); + + boolean backupSuccess = exportKeysToStream( + log, backupInput.mMasterKeyIds, backupInput.mExportSecret, outStream); + + exportedDataSize = outStream.size(); + + if (!backupSuccess) { + // if there was an error, it will be in the log so we just have to return + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + + if (nonEncryptedOutput) { + // log.add(LogType.MSG_EXPORT_NO_ENCRYPT, 1); + log.add(LogType.MSG_BACKUP_SUCCESS, 1); + return new ExportResult(ExportResult.RESULT_OK, log); + } + } + + PgpSignEncryptOperation pseOp = new PgpSignEncryptOperation(mContext, mProviderHelper, mProgressable, mCancelled); + + PgpSignEncryptInputParcel inputParcel = new PgpSignEncryptInputParcel(); + inputParcel.setSymmetricPassphrase(backupInput.mSymmetricPassphrase); + inputParcel.setEnableAsciiArmorOutput(true); + inputParcel.setAddBackupHeader(true); + + InputStream inStream = mContext.getContentResolver().openInputStream(backupOutputUri); + + String filename; + if (backupInput.mMasterKeyIds != null && backupInput.mMasterKeyIds.length == 1) { + filename = Constants.FILE_BACKUP_PREFIX + KeyFormattingUtils.convertKeyIdToHex(backupInput.mMasterKeyIds[0]); + } else { + filename = Constants.FILE_BACKUP_PREFIX + new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + } + filename += backupInput.mExportSecret ? Constants.FILE_EXTENSION_BACKUP_SECRET : Constants.FILE_EXTENSION_BACKUP_PUBLIC; + + InputData inputData = new InputData(inStream, exportedDataSize, filename); + + OutputStream outStream = mContext.getContentResolver().openOutputStream(backupInput.mOutputUri); + outStream = new BufferedOutputStream(outStream); + + PgpSignEncryptResult encryptResult = pseOp.execute(inputParcel, new CryptoInputParcel(), inputData, outStream); + if (!encryptResult.success()) { + log.addByMerge(encryptResult, 1); + // log.add(LogType.MSG_EXPORT_ERROR_ENCRYPT, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + } + + log.add(encryptResult, 1); + log.add(LogType.MSG_BACKUP_SUCCESS, 1); + return new ExportResult(ExportResult.RESULT_OK, log); + + } catch (FileNotFoundException e) { + log.add(LogType.MSG_BACKUP_ERROR_URI_OPEN, 1); + return new ExportResult(ExportResult.RESULT_ERROR, log); + + } + + } + + boolean exportKeysToStream(OperationLog log, long[] masterKeyIds, boolean exportSecret, OutputStream outStream) { + + // noinspection unused TODO use these in a log entry + int okSecret = 0, okPublic = 0; + + int progress = 0; + + Cursor cursor = queryForKeys(masterKeyIds); + + if (cursor == null || !cursor.moveToFirst()) { + log.add(LogType.MSG_BACKUP_ERROR_DB, 1); + return false; // new ExportResult(ExportResult.RESULT_ERROR, log); + } + + try { + + int numKeys = cursor.getCount(); + + updateProgress(mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, numKeys), + 0, numKeys); + + // For each public masterKey id + while (!cursor.isAfterLast()) { + + long keyId = cursor.getLong(INDEX_MASTER_KEY_ID); + log.add(LogType.MSG_BACKUP_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyId)); + + if (writePublicKeyToStream(log, outStream, cursor)) { + okPublic += 1; + + boolean hasSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) > 0; + if (exportSecret && hasSecret) { + log.add(LogType.MSG_BACKUP_SECRET, 2, KeyFormattingUtils.beautifyKeyId(keyId)); + if (writeSecretKeyToStream(log, outStream, cursor)) { + okSecret += 1; + } + } + } + + updateProgress(progress++, numKeys); + cursor.moveToNext(); + } + + updateProgress(R.string.progress_done, numKeys, numKeys); + + } catch (IOException e) { + log.add(LogType.MSG_BACKUP_ERROR_IO, 1); + return false; // new ExportResult(ExportResult.RESULT_ERROR, log); + } finally { + // Make sure the stream is closed + if (outStream != null) try { + outStream.close(); + } catch (Exception e) { + Log.e(Constants.TAG, "error closing stream", e); + } + cursor.close(); + } + + return true; + + } + + private boolean writePublicKeyToStream(OperationLog log, OutputStream outStream, Cursor cursor) + throws IOException { + + ArmoredOutputStream arOutStream = null; + + try { + arOutStream = new ArmoredOutputStream(outStream); + byte[] data = cursor.getBlob(INDEX_PUBKEY_DATA); + CanonicalizedKeyRing ring = UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); + ring.encode(arOutStream); + + } catch (PgpGeneralException e) { + log.add(LogType.MSG_UPLOAD_ERROR_IO, 2); + } finally { + if (arOutStream != null) { + arOutStream.close(); + } + } + return true; + } + + private boolean writeSecretKeyToStream(OperationLog log, OutputStream outStream, Cursor cursor) + throws IOException { + + ArmoredOutputStream arOutStream = null; + + try { + arOutStream = new ArmoredOutputStream(outStream); + byte[] data = cursor.getBlob(INDEX_SECKEY_DATA); + CanonicalizedKeyRing ring = UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); + ring.encode(arOutStream); + + } catch (PgpGeneralException e) { + log.add(LogType.MSG_UPLOAD_ERROR_IO, 2); + } finally { + if (arOutStream != null) { + arOutStream.close(); + } + } + return true; + } + + private Cursor queryForKeys(long[] masterKeyIds) { + + String selection = null, selectionArgs[] = null; + + if (masterKeyIds != null) { + // convert long[] to String[] + selectionArgs = new String[masterKeyIds.length]; + for (int i = 0; i < masterKeyIds.length; i++) { + selectionArgs[i] = Long.toString(masterKeyIds[i]); + } + + // generates ?,?,? as placeholders for selectionArgs + String placeholders = TextUtils.join(",", + Collections.nCopies(masterKeyIds.length, "?")); + + // put together selection string + selection = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + + " IN (" + placeholders + ")"; + } + + return mProviderHelper.getContentResolver().query( + KeyRings.buildUnifiedKeyRingsUri(), PROJECTION, selection, selectionArgs, + Tables.KEYS + "." + KeyRings.MASTER_KEY_ID + ); + + } + +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java index e4026eaaf..99d1768b1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BaseOperation.java @@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.operations; import android.content.Context; import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.StringRes; import org.sufficientlysecure.keychain.Constants.key; import org.sufficientlysecure.keychain.operations.results.OperationResult; @@ -81,7 +82,7 @@ public abstract class BaseOperation <T extends Parcelable> implements Passphrase @NonNull public abstract OperationResult execute(T input, CryptoInputParcel cryptoInput); - public void updateProgress(int message, int current, int total) { + public void updateProgress(@StringRes int message, int current, int total) { if (mProgressable != null) { mProgressable.setProgress(message, current, total); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BenchmarkOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BenchmarkOperation.java new file mode 100644 index 000000000..f6e157c74 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/BenchmarkOperation.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@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 java.util.Random; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.spongycastle.bcpg.HashAlgorithmTags; +import org.spongycastle.bcpg.S2K; +import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.spongycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.operations.results.BenchmarkResult; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.BenchmarkInputParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.Passphrase; +import org.sufficientlysecure.keychain.util.ProgressScaler; + + +public class BenchmarkOperation extends BaseOperation<BenchmarkInputParcel> { + + public BenchmarkOperation(Context context, ProviderHelper providerHelper, Progressable + progressable) { + super(context, providerHelper, progressable); + } + + @NonNull + @Override + public BenchmarkResult execute(BenchmarkInputParcel consolidateInputParcel, + CryptoInputParcel cryptoInputParcel) { + OperationLog log = new OperationLog(); + log.add(LogType.MSG_BENCH, 0); + + // random data + byte[] buf = new byte[1024*1024*5]; + new Random().nextBytes(buf); + + Passphrase passphrase = new Passphrase("a"); + + int numRepeats = 5; + long totalTime = 0; + + // encrypt + SignEncryptResult encryptResult; + int i = 0; + do { + SignEncryptOperation op = + new SignEncryptOperation(mContext, mProviderHelper, + new ProgressScaler(mProgressable, i*(50/numRepeats), (i+1)*(50/numRepeats), 100), mCancelled); + SignEncryptParcel input = new SignEncryptParcel(); + input.setSymmetricPassphrase(passphrase); + input.setBytes(buf); + encryptResult = op.execute(input, new CryptoInputParcel()); + log.add(encryptResult, 1); + log.add(LogType.MSG_BENCH_ENC_TIME, 2, + String.format("%.2f", encryptResult.getResults().get(0).mOperationTime / 1000.0)); + totalTime += encryptResult.getResults().get(0).mOperationTime; + } while (++i < numRepeats); + + long encryptionTime = totalTime / numRepeats; + totalTime = 0; + + // decrypt + i = 0; + do { + DecryptVerifyResult decryptResult; + PgpDecryptVerifyOperation op = + new PgpDecryptVerifyOperation(mContext, mProviderHelper, + new ProgressScaler(mProgressable, 50 +i*(50/numRepeats), 50 +(i+1)*(50/numRepeats), 100)); + PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(encryptResult.getResultBytes()); + input.setAllowSymmetricDecryption(true); + decryptResult = op.execute(input, new CryptoInputParcel(passphrase)); + log.add(decryptResult, 1); + log.add(LogType.MSG_BENCH_DEC_TIME, 2, String.format("%.2f", decryptResult.mOperationTime / 1000.0)); + totalTime += decryptResult.mOperationTime; + } while (++i < numRepeats); + + long decryptionTime = totalTime / numRepeats; + totalTime = 0; + + int iterationsFor100ms; + try { + PGPDigestCalculatorProvider digestCalcProvider = new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build(); + PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder( + digestCalcProvider).setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( + "".toCharArray()); + + byte[] iv = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + int iterations = 0; + while (iterations < 255 && totalTime < 100) { + iterations += 1; + + S2K s2k = new S2K(HashAlgorithmTags.SHA1, iv, iterations); + totalTime = System.currentTimeMillis(); + decryptorFactory.makeKeyFromPassPhrase(SymmetricKeyAlgorithmTags.AES_128, s2k); + totalTime = System.currentTimeMillis() -totalTime; + + if ((iterations % 10) == 0) { + log.add(LogType.MSG_BENCH_S2K_FOR_IT, 1, Integer.toString(iterations), Long.toString(totalTime)); + } + + } + iterationsFor100ms = iterations; + + } catch (PGPException e) { + Log.e(Constants.TAG, "internal error during benchmark", e); + log.add(LogType.MSG_INTERNAL_ERROR, 0); + return new BenchmarkResult(BenchmarkResult.RESULT_ERROR, log); + } + + log.add(LogType.MSG_BENCH_S2K_100MS_ITS, 1, Integer.toString(iterationsFor100ms)); + log.add(LogType.MSG_BENCH_ENC_TIME_AVG, 1, String.format("%.2f", encryptionTime/1000.0)); + log.add(LogType.MSG_BENCH_DEC_TIME_AVG, 1, String.format("%.2f", decryptionTime/1000.0)); + + log.add(LogType.MSG_BENCH_SUCCESS, 0); + return new BenchmarkResult(BenchmarkResult.RESULT_OK, log); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java index eeed24db0..7d11fa1f1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/CertifyOperation.java @@ -17,15 +17,20 @@ package org.sufficientlysecure.keychain.operations; + +import java.net.Proxy; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + import android.content.Context; import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; import org.sufficientlysecure.keychain.operations.results.CertifyResult; -import org.sufficientlysecure.keychain.operations.results.ExportResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; @@ -40,6 +45,7 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException import org.sufficientlysecure.keychain.service.CertifyActionsParcel; import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.NfcSignOperationsBuilder; @@ -48,10 +54,6 @@ import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; -import java.net.Proxy; -import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicBoolean; - /** * An operation which implements a high level user id certification operation. * <p/> @@ -204,23 +206,9 @@ public class CertifyOperation extends BaseOperation<CertifyActionsParcel> { } // these variables are used inside the following loop, but they need to be created only once - HkpKeyserver keyServer = null; - ExportOperation exportOperation = null; - Proxy proxy = null; + UploadOperation uploadOperation = null; if (parcel.keyServerUri != null) { - keyServer = new HkpKeyserver(parcel.keyServerUri); - exportOperation = new ExportOperation(mContext, mProviderHelper, mProgressable); - if (cryptoInput.getParcelableProxy() == null) { - // explicit proxy not set - if (!OrbotHelper.isOrbotInRequiredState(mContext)) { - return new CertifyResult(null, - RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); - } - proxy = Preferences.getPreferences(mContext).getProxyPrefs() - .parcelableProxy.getProxy(); - } else { - proxy = cryptoInput.getParcelableProxy().getProxy(); - } + uploadOperation = new UploadOperation(mContext, mProviderHelper, mProgressable, mCancelled); } // Write all certified keys into the database @@ -239,11 +227,10 @@ public class CertifyOperation extends BaseOperation<CertifyActionsParcel> { mProviderHelper.clearLog(); SaveKeyringResult result = mProviderHelper.savePublicKeyRing(certifiedKey); - if (exportOperation != null) { - ExportResult uploadResult = exportOperation.uploadKeyRingToServer( - keyServer, - certifiedKey, - proxy); + if (uploadOperation != null) { + UploadKeyringParcel uploadInput = + new UploadKeyringParcel(parcel.keyServerUri, certifiedKey.getMasterKeyId()); + UploadResult uploadResult = uploadOperation.execute(uploadInput, cryptoInput); log.add(uploadResult, 2); if (uploadResult.success()) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java index f5ba88502..3b2c484be 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java @@ -17,17 +17,20 @@ package org.sufficientlysecure.keychain.operations; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + 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.ExportResult; -import org.sufficientlysecure.keychain.operations.results.InputPendingResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing; import org.sufficientlysecure.keychain.pgp.PgpKeyOperation; import org.sufficientlysecure.keychain.pgp.Progressable; @@ -35,17 +38,14 @@ import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.service.ContactSyncAdapterService; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.ProgressScaler; -import java.io.IOException; -import java.util.concurrent.atomic.AtomicBoolean; - /** * An operation which implements a high level key edit operation. * <p/> @@ -72,7 +72,7 @@ public class EditKeyOperation extends BaseOperation<SaveKeyringParcel> { * @return the result of the operation */ @NonNull - public InputPendingResult execute(SaveKeyringParcel saveParcel, CryptoInputParcel cryptoInput) { + public EditKeyResult execute(SaveKeyringParcel saveParcel, CryptoInputParcel cryptoInput) { OperationLog log = new OperationLog(); log.add(LogType.MSG_ED, 0); @@ -99,7 +99,8 @@ public class EditKeyOperation extends BaseOperation<SaveKeyringParcel> { modifyResult = keyOperations.modifySecretKeyRing(secRing, cryptoInput, saveParcel); if (modifyResult.isPending()) { - return modifyResult; + log.add(modifyResult, 1); + return new EditKeyResult(log, modifyResult); } } catch (NotFoundException e) { @@ -134,32 +135,29 @@ public class EditKeyOperation extends BaseOperation<SaveKeyringParcel> { UncachedKeyRing ring = modifyResult.getRing(); if (saveParcel.isUpload()) { - UncachedKeyRing publicKeyRing; + byte[] keyringBytes; try { - publicKeyRing = ring.extractPublicKeyRing(); + UncachedKeyRing publicKeyRing = ring.extractPublicKeyRing(); + keyringBytes = publicKeyRing.getEncoded(); } catch (IOException e) { log.add(LogType.MSG_ED_ERROR_EXTRACTING_PUBLIC_UPLOAD, 1); return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null); } - ExportKeyringParcel exportKeyringParcel = - new ExportKeyringParcel(saveParcel.getUploadKeyserver(), - publicKeyRing); + UploadKeyringParcel exportKeyringParcel = + new UploadKeyringParcel(saveParcel.getUploadKeyserver(), keyringBytes); - ExportResult uploadResult = - new ExportOperation(mContext, mProviderHelper, mProgressable) + UploadResult uploadResult = + new UploadOperation(mContext, mProviderHelper, mProgressable, mCancelled) .execute(exportKeyringParcel, cryptoInput); + log.add(uploadResult, 2); + if (uploadResult.isPending()) { - return uploadResult; + return new EditKeyResult(log, uploadResult); } else if (!uploadResult.success() && saveParcel.isUploadAtomic()) { // if atomic, update fail implies edit operation should also fail and not save - log.add(uploadResult, 2); - return new EditKeyResult(log, RequiredInputParcel.createRetryUploadOperation(), - cryptoInput); - } else { - // upload succeeded or not atomic so we continue - log.add(uploadResult, 2); + return new EditKeyResult(log, RequiredInputParcel.createRetryUploadOperation(), cryptoInput); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java deleted file mode 100644 index 531ac01f2..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java +++ /dev/null @@ -1,385 +0,0 @@ -/* - * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> - * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.operations; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.net.Proxy; -import java.util.Collections; -import java.util.concurrent.atomic.AtomicBoolean; - -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.text.TextUtils; - -import org.spongycastle.bcpg.ArmoredOutputStream; -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; -import org.sufficientlysecure.keychain.keyimport.Keyserver.AddKeyException; -import org.sufficientlysecure.keychain.operations.results.ExportResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; -import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; -import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; -import org.sufficientlysecure.keychain.pgp.Progressable; -import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; -import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; -import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; -import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.Preferences; -import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; - -/** - * An operation class which implements high level export - * operations. - * This class receives a source and/or destination of keys as input and performs - * all steps for this export. - * - * @see org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter#getSelectedEntries() - * For the export operation, the input consists of a set of key ids and - * either the name of a file or an output uri to write to. - */ -public class ExportOperation extends BaseOperation<ExportKeyringParcel> { - - public ExportOperation(Context context, ProviderHelper providerHelper, Progressable - progressable) { - super(context, providerHelper, progressable); - } - - public ExportOperation(Context context, ProviderHelper providerHelper, - Progressable progressable, AtomicBoolean cancelled) { - super(context, providerHelper, progressable, cancelled); - } - - public ExportResult uploadKeyRingToServer(HkpKeyserver server, CanonicalizedPublicKeyRing keyring, - Proxy proxy) { - return uploadKeyRingToServer(server, keyring.getUncachedKeyRing(), proxy); - } - - public ExportResult uploadKeyRingToServer(HkpKeyserver server, UncachedKeyRing keyring, Proxy proxy) { - mProgressable.setProgress(R.string.progress_uploading, 0, 1); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ArmoredOutputStream aos = null; - OperationLog log = new OperationLog(); - log.add(LogType.MSG_EXPORT_UPLOAD_PUBLIC, 0, KeyFormattingUtils.convertKeyIdToHex( - keyring.getPublicKey().getKeyId() - )); - - try { - aos = new ArmoredOutputStream(bos); - keyring.encode(aos); - aos.close(); - - String armoredKey = bos.toString("UTF-8"); - server.add(armoredKey, proxy); - - log.add(LogType.MSG_EXPORT_UPLOAD_SUCCESS, 1); - return new ExportResult(ExportResult.RESULT_OK, log); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException", e); - - log.add(LogType.MSG_EXPORT_ERROR_KEY, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } catch (AddKeyException e) { - Log.e(Constants.TAG, "AddKeyException", e); - - log.add(LogType.MSG_EXPORT_ERROR_UPLOAD, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } finally { - mProgressable.setProgress(R.string.progress_uploading, 1, 1); - try { - if (aos != null) { - aos.close(); - } - bos.close(); - } catch (IOException e) { - // this is just a finally thing, no matter if it doesn't work out. - } - } - } - - public ExportResult exportToFile(long[] masterKeyIds, boolean exportSecret, String outputFile) { - - OperationLog log = new OperationLog(); - if (masterKeyIds != null) { - log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); - } else { - log.add(LogType.MSG_EXPORT_ALL, 0); - } - - // do we have a file name? - if (outputFile == null) { - log.add(LogType.MSG_EXPORT_ERROR_NO_FILE, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - log.add(LogType.MSG_EXPORT_FILE_NAME, 1, outputFile); - - // check if storage is ready - if (!FileHelper.isStorageMounted(outputFile)) { - log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - try { - OutputStream outStream = new FileOutputStream(outputFile); - try { - ExportResult result = exportKeyRings(log, masterKeyIds, exportSecret, outStream); - if (result.cancelled()) { - //noinspection ResultOfMethodCallIgnored - new File(outputFile).delete(); - } - return result; - } finally { - outStream.close(); - } - } catch (IOException e) { - log.add(LogType.MSG_EXPORT_ERROR_FOPEN, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - } - - public ExportResult exportToUri(long[] masterKeyIds, boolean exportSecret, Uri outputUri) { - - OperationLog log = new OperationLog(); - if (masterKeyIds != null) { - log.add(LogType.MSG_EXPORT, 0, masterKeyIds.length); - } else { - log.add(LogType.MSG_EXPORT_ALL, 0); - } - - // do we have a file name? - if (outputUri == null) { - log.add(LogType.MSG_EXPORT_ERROR_NO_URI, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - try { - OutputStream outStream = mProviderHelper.getContentResolver().openOutputStream - (outputUri); - return exportKeyRings(log, masterKeyIds, exportSecret, outStream); - } catch (FileNotFoundException e) { - log.add(LogType.MSG_EXPORT_ERROR_URI_OPEN, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - - } - - ExportResult exportKeyRings(OperationLog log, long[] masterKeyIds, boolean exportSecret, - OutputStream outStream) { - - /* TODO isn't this checked above, with the isStorageMounted call? - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - log.add(LogType.MSG_EXPORT_ERROR_STORAGE, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log); - } - */ - - if (!BufferedOutputStream.class.isInstance(outStream)) { - outStream = new BufferedOutputStream(outStream); - } - - int okSecret = 0, okPublic = 0, progress = 0; - - Cursor cursor = null; - try { - - String selection = null, selectionArgs[] = null; - - if (masterKeyIds != null) { - // convert long[] to String[] - selectionArgs = new String[masterKeyIds.length]; - for (int i = 0; i < masterKeyIds.length; i++) { - selectionArgs[i] = Long.toString(masterKeyIds[i]); - } - - // generates ?,?,? as placeholders for selectionArgs - String placeholders = TextUtils.join(",", - Collections.nCopies(masterKeyIds.length, "?")); - - // put together selection string - selection = Tables.KEYS + "." + KeyRings.MASTER_KEY_ID - + " IN (" + placeholders + ")"; - } - - cursor = mProviderHelper.getContentResolver().query( - KeyRings.buildUnifiedKeyRingsUri(), new String[]{ - KeyRings.MASTER_KEY_ID, KeyRings.PUBKEY_DATA, - KeyRings.PRIVKEY_DATA, KeyRings.HAS_ANY_SECRET - }, selection, selectionArgs, Tables.KEYS + "." + KeyRings.MASTER_KEY_ID - ); - - if (cursor == null || !cursor.moveToFirst()) { - log.add(LogType.MSG_EXPORT_ERROR_DB, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); - } - - int numKeys = cursor.getCount(); - - updateProgress( - mContext.getResources().getQuantityString(R.plurals.progress_exporting_key, - numKeys), 0, numKeys); - - // For each public masterKey id - while (!cursor.isAfterLast()) { - - long keyId = cursor.getLong(0); - ArmoredOutputStream arOutStream = null; - - // Create an output stream - try { - arOutStream = new ArmoredOutputStream(outStream); - - log.add(LogType.MSG_EXPORT_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(keyId)); - - byte[] data = cursor.getBlob(1); - CanonicalizedKeyRing ring = - UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); - ring.encode(arOutStream); - - okPublic += 1; - } catch (PgpGeneralException e) { - log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); - updateProgress(progress++, numKeys); - continue; - } finally { - // make sure this is closed - if (arOutStream != null) { - arOutStream.close(); - } - arOutStream = null; - } - - if (exportSecret && cursor.getInt(3) > 0) { - try { - arOutStream = new ArmoredOutputStream(outStream); - - // export secret key part - log.add(LogType.MSG_EXPORT_SECRET, 2, KeyFormattingUtils.beautifyKeyId - (keyId)); - byte[] data = cursor.getBlob(2); - CanonicalizedKeyRing ring = - UncachedKeyRing.decodeFromData(data).canonicalize(log, 2, true); - ring.encode(arOutStream); - - okSecret += 1; - } catch (PgpGeneralException e) { - log.add(LogType.MSG_EXPORT_ERROR_KEY, 2); - updateProgress(progress++, numKeys); - continue; - } finally { - // make sure this is closed - if (arOutStream != null) { - arOutStream.close(); - } - } - } - - updateProgress(progress++, numKeys); - - cursor.moveToNext(); - } - - updateProgress(R.string.progress_done, numKeys, numKeys); - - } catch (IOException e) { - log.add(LogType.MSG_EXPORT_ERROR_IO, 1); - return new ExportResult(ExportResult.RESULT_ERROR, log, okPublic, okSecret); - } finally { - // Make sure the stream is closed - if (outStream != null) try { - outStream.close(); - } catch (Exception e) { - Log.e(Constants.TAG, "error closing stream", e); - } - if (cursor != null) { - cursor.close(); - } - } - - - log.add(LogType.MSG_EXPORT_SUCCESS, 1); - return new ExportResult(ExportResult.RESULT_OK, log, okPublic, okSecret); - - } - - @NonNull - public ExportResult execute(ExportKeyringParcel exportInput, CryptoInputParcel cryptoInput) { - switch (exportInput.mExportType) { - case UPLOAD_KEYSERVER: { - Proxy proxy; - if (cryptoInput.getParcelableProxy() == null) { - // explicit proxy not set - if (!OrbotHelper.isOrbotInRequiredState(mContext)) { - return new ExportResult(null, - RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); - } - proxy = Preferences.getPreferences(mContext).getProxyPrefs() - .parcelableProxy.getProxy(); - } else { - proxy = cryptoInput.getParcelableProxy().getProxy(); - } - - HkpKeyserver hkpKeyserver = new HkpKeyserver(exportInput.mKeyserver); - try { - if (exportInput.mCanonicalizedPublicKeyringUri != null) { - CanonicalizedPublicKeyRing keyring - = mProviderHelper.getCanonicalizedPublicKeyRing( - exportInput.mCanonicalizedPublicKeyringUri); - return uploadKeyRingToServer(hkpKeyserver, keyring, proxy); - } else { - return uploadKeyRingToServer(hkpKeyserver, exportInput.mUncachedKeyRing, - proxy); - } - } catch (ProviderHelper.NotFoundException e) { - Log.e(Constants.TAG, "error uploading key", e); - return new ExportResult(ExportResult.RESULT_ERROR, new OperationLog()); - } - } - case EXPORT_FILE: { - return exportToFile(exportInput.mMasterKeyIds, exportInput.mExportSecret, - exportInput.mOutputFile); - } - case EXPORT_URI: { - return exportToUri(exportInput.mMasterKeyIds, exportInput.mExportSecret, - exportInput.mOutputUri); - } - default: { // can never happen, all enum types must be handled above - throw new AssertionError("must not happen, this is a bug!"); - } - } - } -}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java index 89575338f..19a05790f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java @@ -28,7 +28,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; -import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -82,6 +82,8 @@ import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; */ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { + public static final int MAX_THREADS = 10; + public ImportOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { super(context, providerHelper, progressable); @@ -133,7 +135,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { @NonNull private ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int num, String keyServerUri, Progressable progressable, - Proxy proxy) { + @NonNull Proxy proxy) { if (progressable != null) { progressable.setProgress(R.string.progress_importing, 0, 100); } @@ -188,7 +190,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { // Make sure we have the keyserver instance cached if (keyServer == null) { log.add(LogType.MSG_IMPORT_KEYSERVER, 1, keyServerUri); - keyServer = new HkpKeyserver(keyServerUri); + keyServer = new HkpKeyserver(keyServerUri, proxy); } try { @@ -197,11 +199,10 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { if (entry.mExpectedFingerprint != null) { log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER, 2, "0x" + entry.mExpectedFingerprint.substring(24)); - data = keyServer.get("0x" + entry.mExpectedFingerprint, proxy) - .getBytes(); + data = keyServer.get("0x" + entry.mExpectedFingerprint).getBytes(); } else { log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER, 2, entry.mKeyIdHex); - data = keyServer.get(entry.mKeyIdHex, proxy).getBytes(); + data = keyServer.get(entry.mKeyIdHex).getBytes(); } key = UncachedKeyRing.decodeFromData(data); if (key != null) { @@ -219,12 +220,12 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { if (entry.mKeybaseName != null) { // Make sure we have this cached if (keybaseServer == null) { - keybaseServer = new KeybaseKeyserver(); + keybaseServer = new KeybaseKeyserver(proxy); } try { log.add(LogType.MSG_IMPORT_FETCH_KEYBASE, 2, entry.mKeybaseName); - byte[] data = keybaseServer.get(entry.mKeybaseName, proxy).getBytes(); + byte[] data = keybaseServer.get(entry.mKeybaseName).getBytes(); UncachedKeyRing keybaseKey = UncachedKeyRing.decodeFromData(data); // If there already is a key, merge the two @@ -261,12 +262,6 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { continue; } - // Another check if we have been cancelled - if (checkCancelled()) { - cancelled = true; - break; - } - SaveKeyringResult result; // synchronizing prevents https://github.com/open-keychain/open-keychain/issues/1221 // and https://github.com/open-keychain/open-keychain/issues/1480 @@ -365,13 +360,15 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { } } - // Final log entry, it's easier to do this individually - if ((newKeys > 0 || updatedKeys > 0) && badKeys > 0) { - log.add(LogType.MSG_IMPORT_PARTIAL, 1); - } else if (newKeys > 0 || updatedKeys > 0) { - log.add(LogType.MSG_IMPORT_SUCCESS, 1); - } else { - log.add(LogType.MSG_IMPORT_ERROR, 1); + if (!cancelled) { + // Final log entry, it's easier to do this individually + if ((newKeys > 0 || updatedKeys > 0) && badKeys > 0) { + log.add(LogType.MSG_IMPORT_PARTIAL, 1); + } else if (newKeys > 0 || updatedKeys > 0) { + log.add(LogType.MSG_IMPORT_SUCCESS, 1); + } else { + log.add(LogType.MSG_IMPORT_ERROR, 1); + } } return new ImportKeyResult(resultType, log, newKeys, updatedKeys, badKeys, secret, @@ -400,8 +397,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { return new ImportKeyResult(null, RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); } - proxy = Preferences.getPreferences(mContext).getProxyPrefs().parcelableProxy - .getProxy(); + proxy = Preferences.getPreferences(mContext).getProxyPrefs().getProxy(); } else { proxy = cryptoInput.getParcelableProxy().getProxy(); } @@ -414,62 +410,61 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { } @NonNull - private ImportKeyResult multiThreadedKeyImport(Iterator<ParcelableKeyRing> keyListIterator, + private ImportKeyResult multiThreadedKeyImport(@NonNull Iterator<ParcelableKeyRing> keyListIterator, int totKeys, final String keyServer, final Proxy proxy) { Log.d(Constants.TAG, "Multi-threaded key import starting"); - if (keyListIterator != null) { - KeyImportAccumulator accumulator = new KeyImportAccumulator(totKeys, mProgressable); - - final ProgressScaler ignoreProgressable = new ProgressScaler(); + KeyImportAccumulator accumulator = new KeyImportAccumulator(totKeys, mProgressable); - final int maxThreads = 200; - ExecutorService importExecutor = new ThreadPoolExecutor(0, maxThreads, - 30L, TimeUnit.SECONDS, - new SynchronousQueue<Runnable>()); + final ProgressScaler ignoreProgressable = new ProgressScaler(); - ExecutorCompletionService<ImportKeyResult> importCompletionService = - new ExecutorCompletionService<>(importExecutor); + ExecutorService importExecutor = new ThreadPoolExecutor(0, MAX_THREADS, 30L, TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>()); - while (keyListIterator.hasNext()) { // submit all key rings to be imported + ExecutorCompletionService<ImportKeyResult> importCompletionService = + new ExecutorCompletionService<>(importExecutor); - final ParcelableKeyRing pkRing = keyListIterator.next(); + while (keyListIterator.hasNext()) { // submit all key rings to be imported - Callable<ImportKeyResult> importOperationCallable = new Callable<ImportKeyResult> - () { + final ParcelableKeyRing pkRing = keyListIterator.next(); - @Override - public ImportKeyResult call() { + Callable<ImportKeyResult> importOperationCallable = new Callable<ImportKeyResult> + () { - ArrayList<ParcelableKeyRing> list = new ArrayList<>(); - list.add(pkRing); + @Override + public ImportKeyResult call() { - return serialKeyRingImport(list.iterator(), 1, keyServer, - ignoreProgressable, proxy); + if (checkCancelled()) { + return null; } - }; - importCompletionService.submit(importOperationCallable); - } + ArrayList<ParcelableKeyRing> list = new ArrayList<>(); + list.add(pkRing); - while (!accumulator.isImportFinished()) { // accumulate the results of each import - try { - accumulator.accumulateKeyImport(importCompletionService.take().get()); - } catch (InterruptedException | ExecutionException e) { - Log.e(Constants.TAG, "A key could not be imported during multi-threaded " + - "import", e); - // do nothing? - if (e instanceof ExecutionException) { - // Since serialKeyRingImport does not throw any exceptions, this is what - // would have happened if - // we were importing the key on this thread - throw new RuntimeException(); - } + return serialKeyRingImport(list.iterator(), 1, keyServer, ignoreProgressable, proxy); + } + }; + + importCompletionService.submit(importOperationCallable); + } + + while (!accumulator.isImportFinished()) { // accumulate the results of each import + try { + accumulator.accumulateKeyImport(importCompletionService.take().get()); + } catch (InterruptedException | ExecutionException e) { + Log.e(Constants.TAG, "A key could not be imported during multi-threaded " + + "import", e); + // do nothing? + if (e instanceof ExecutionException) { + // Since serialKeyRingImport does not throw any exceptions, this is what + // would have happened if + // we were importing the key on this thread + throw new RuntimeException(); } } - return accumulator.getConsolidatedResult(); } - return new ImportKeyResult(ImportKeyResult.RESULT_FAIL_NOTHING, new OperationLog()); + return accumulator.getConsolidatedResult(); + } /** @@ -486,6 +481,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { private int mUpdatedKeys = 0; private int mSecret = 0; private int mResultType = 0; + private boolean mHasCancelledResult; /** * Accumulates keyring imports and updates the progressable whenever a new key is imported. @@ -503,14 +499,25 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { } } - public synchronized void accumulateKeyImport(ImportKeyResult result) { + public void accumulateKeyImport(ImportKeyResult result) { mImportedKeys++; + if (result == null) { + return; + } + if (mProgressable != null) { mProgressable.setProgress(mImportedKeys, mTotalKeys); } - mImportLog.addAll(result.getLog().toList());//accumulates log + boolean notCancelledOrFirstCancelled = !result.cancelled() || !mHasCancelledResult; + if (notCancelledOrFirstCancelled) { + mImportLog.addAll(result.getLog().toList()); //accumulates log + if (result.cancelled()) { + mHasCancelledResult = true; + } + } + mBadKeys += result.mBadKeys; mNewKeys += result.mNewKeys; mUpdatedKeys += result.mUpdatedKeys; @@ -533,7 +540,9 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { // adding required information to mResultType // special case,no keys requested for import - if (mBadKeys == 0 && mNewKeys == 0 && mUpdatedKeys == 0) { + if (mBadKeys == 0 && mNewKeys == 0 && mUpdatedKeys == 0 + && (mResultType & ImportKeyResult.RESULT_CANCELLED) + != ImportKeyResult.RESULT_CANCELLED) { mResultType = ImportKeyResult.RESULT_FAIL_NOTHING; } else { if (mNewKeys > 0) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java index 9170dc139..bb8d6ad73 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/InputDataOperation.java @@ -25,9 +25,12 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import android.content.ClipDescription; import android.content.Context; import android.net.Uri; import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.webkit.MimeTypeMap; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.codec.DecodeMonitor; @@ -47,7 +50,7 @@ import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.InputDataParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; @@ -100,32 +103,40 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> { decryptInput.setInputUri(input.getInputUri()); - currentInputUri = TemporaryStorageProvider.createFile(mContext); + currentInputUri = TemporaryFileProvider.createFile(mContext); decryptInput.setOutputUri(currentInputUri); decryptResult = op.execute(decryptInput, cryptoInput); if (decryptResult.isPending()) { return new InputDataResult(log, decryptResult); } - log.addByMerge(decryptResult, 2); + log.addByMerge(decryptResult, 1); - if (!decryptResult.success()) { - log.add(LogType.MSG_DATA_ERROR_OPENPGP, 1); + if ( ! decryptResult.success()) { return new InputDataResult(InputDataResult.RESULT_ERROR, log); } + // inform the storage provider about the mime type for this uri + if (decryptResult.getDecryptionMetadata() != null) { + TemporaryFileProvider.setMimeType(mContext, currentInputUri, + decryptResult.getDecryptionMetadata().getMimeType()); + } + } else { currentInputUri = input.getInputUri(); } - // don't even attempt if we know the data isn't suitable for mime content + // don't even attempt if we know the data isn't suitable for mime content, or if we have a filename boolean skipMimeParsing = false; if (decryptResult != null && decryptResult.getDecryptionMetadata() != null) { - String contentType = decryptResult.getDecryptionMetadata().getMimeType(); - if (contentType != null - && !contentType.startsWith("multipart/") - && !contentType.startsWith("text/") - && !contentType.startsWith("application/")) { + OpenPgpMetadata metadata = decryptResult.getDecryptionMetadata(); + String fileName = metadata.getFilename(); + String contentType = metadata.getMimeType(); + if (!TextUtils.isEmpty(fileName) + || contentType != null + && !contentType.startsWith("multipart/") + && !contentType.startsWith("text/") + && !"application/octet-stream".equals(contentType)) { skipMimeParsing = true; } } @@ -153,6 +164,7 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> { parser.setContentDecoding(true); parser.setRecurse(); parser.setContentHandler(new AbstractContentHandler() { + private boolean mFoundHeaderWithFields = false; private Uri uncheckedSignedDataUri; String mFilename; @@ -185,7 +197,7 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> { log.add(LogType.MSG_DATA_DETACHED_RAW, 3); - uncheckedSignedDataUri = TemporaryStorageProvider.createFile(mContext, mFilename, "text/plain"); + uncheckedSignedDataUri = TemporaryFileProvider.createFile(mContext, mFilename, "text/plain"); OutputStream out = mContext.getContentResolver().openOutputStream(uncheckedSignedDataUri, "w"); if (out == null) { @@ -209,11 +221,19 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> { } @Override + public void endHeader() throws MimeException { + if ( ! mFoundHeaderWithFields) { + parser.stop(); + } + } + + @Override public void field(Field field) throws MimeException { field = DefaultFieldParser.getParser().parse(field, DecodeMonitor.SILENT); if (field instanceof ContentDispositionField) { mFilename = ((ContentDispositionField) field).getFilename(); } + mFoundHeaderWithFields = true; } private void bodySignature(BodyDescriptor bd, InputStream is) throws MimeException, IOException { @@ -282,12 +302,24 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> { log.add(LogType.MSG_DATA_MIME_PART, 2); - log.add(LogType.MSG_DATA_MIME_TYPE, 3, bd.getMimeType()); + String mimeType = bd.getMimeType(); + if (mFilename != null) { log.add(LogType.MSG_DATA_MIME_FILENAME, 3, mFilename); + boolean isGenericMimeType = ClipDescription.compareMimeTypes(mimeType, "application/octet-stream") + || ClipDescription.compareMimeTypes(mimeType, "application/x-download"); + if (isGenericMimeType) { + String extension = MimeTypeMap.getFileExtensionFromUrl(mFilename); + String extMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (extMimeType != null) { + mimeType = extMimeType; + log.add(LogType.MSG_DATA_MIME_FROM_EXTENSION, 3); + } + } } + log.add(LogType.MSG_DATA_MIME_TYPE, 3, mimeType); - Uri uri = TemporaryStorageProvider.createFile(mContext, mFilename, bd.getMimeType()); + Uri uri = TemporaryFileProvider.createFile(mContext, mFilename, mimeType); OutputStream out = mContext.getContentResolver().openOutputStream(uri, "w"); if (out == null) { @@ -308,7 +340,7 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> { charset = "utf-8"; } - OpenPgpMetadata metadata = new OpenPgpMetadata(mFilename, bd.getMimeType(), 0L, totalLength, charset); + OpenPgpMetadata metadata = new OpenPgpMetadata(mFilename, mimeType, 0L, totalLength, charset); out.close(); outputUris.add(uri); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java index 8f1abde83..f3ceac681 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java @@ -20,39 +20,43 @@ package org.sufficientlysecure.keychain.operations; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.net.Proxy; -import java.util.ArrayList; -import java.util.List; - import android.content.Context; import android.support.annotation.NonNull; +import com.textuality.keybase.lib.KeybaseQuery; import com.textuality.keybase.lib.Proof; import com.textuality.keybase.lib.prover.Prover; -import de.measite.minidns.Client; -import de.measite.minidns.DNSMessage; -import de.measite.minidns.Question; -import de.measite.minidns.Record; -import de.measite.minidns.record.Data; -import de.measite.minidns.record.TXT; + import org.json.JSONObject; import org.spongycastle.openpgp.PGPUtil; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.KeybaseVerificationParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import org.sufficientlysecure.keychain.util.OkHttpKeybaseClient; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.List; + +import de.measite.minidns.Client; +import de.measite.minidns.DNSMessage; +import de.measite.minidns.Question; +import de.measite.minidns.Record; +import de.measite.minidns.record.Data; +import de.measite.minidns.record.TXT; + public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificationParcel> { public KeybaseVerificationOperation(Context context, ProviderHelper providerHelper, @@ -83,6 +87,9 @@ public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificat log.add(OperationResult.LogType.MSG_KEYBASE_VERIFICATION, 0, requiredFingerprint); try { + KeybaseQuery keybaseQuery = new KeybaseQuery(new OkHttpKeybaseClient()); + keybaseQuery.setProxy(proxy); + String keybaseProof = keybaseInput.mKeybaseProof; Proof proof = new Proof(new JSONObject(keybaseProof)); mProgressable.setProgress(R.string.keybase_message_fetching_data, 0, 100); @@ -95,7 +102,7 @@ public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificat return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); } - if (!prover.fetchProofData(proxy)) { + if (!prover.fetchProofData(keybaseQuery)) { log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_FETCH_PROOF, 1); return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log); } @@ -144,7 +151,6 @@ public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificat PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable); PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(messageBytes) - .setSignedLiteralData(true) .setRequiredSignerFingerprint(requiredFingerprint); DecryptVerifyResult decryptVerifyResult = op.execute(input, new CryptoInputParcel()); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java index 975cf541a..3e787560a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java @@ -19,12 +19,13 @@ package org.sufficientlysecure.keychain.operations; + import android.content.Context; import android.net.Uri; import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.operations.results.InputPendingResult; +import org.sufficientlysecure.keychain.operations.results.EditKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.RevokeResult; import org.sufficientlysecure.keychain.pgp.Progressable; @@ -79,9 +80,8 @@ public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> { saveKeyringParcel.mRevokeSubKeys.add(masterKeyId); - InputPendingResult revokeAndUploadResult = new EditKeyOperation(mContext, - mProviderHelper, mProgressable, mCancelled) - .execute(saveKeyringParcel, cryptoInputParcel); + EditKeyResult revokeAndUploadResult = new EditKeyOperation(mContext, + mProviderHelper, mProgressable, mCancelled).execute(saveKeyringParcel, cryptoInputParcel); if (revokeAndUploadResult.isPending()) { return revokeAndUploadResult; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java index 843a55389..2ca74063c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/SignEncryptOperation.java @@ -17,6 +17,16 @@ package org.sufficientlysecure.keychain.operations; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + import android.content.Context; import android.net.Uri; import android.support.annotation.NonNull; @@ -40,21 +50,13 @@ import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ProgressScaler; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -/** This is a high-level operation, which encapsulates one or more sign/encrypt +/** + * This is a high-level operation, which encapsulates one or more sign/encrypt * operations, using URIs or byte arrays as input and output. * * This operation is fail-fast: If any sign/encrypt sub-operation fails or returns * a pending result, it will terminate. - * */ public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> { @@ -63,6 +65,7 @@ public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> { super(context, providerHelper, progressable, cancelled); } + @NonNull public SignEncryptResult execute(SignEncryptParcel input, CryptoInputParcel cryptoInput) { @@ -115,7 +118,7 @@ public class SignEncryptOperation extends BaseOperation<SignEncryptParcel> { log.add(LogType.MSG_SE_INPUT_URI, 1); Uri uri = inputUris.removeFirst(); try { - InputStream is = mContext.getContentResolver().openInputStream(uri); + InputStream is = FileHelper.openInputStreamSafe(mContext.getContentResolver(), uri); long fileSize = FileHelper.getFileSize(mContext, uri, 0); String filename = FileHelper.getFilename(mContext, uri); inputData = new InputData(is, fileSize, filename); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java new file mode 100644 index 000000000..e5f11eaa6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/UploadOperation.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2010-2014 Thialfihar <thi@thialfihar.org> + * Copyright (C) 2015 Vincent Breitmoser <valodim@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.operations; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.Proxy; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; +import org.sufficientlysecure.keychain.keyimport.Keyserver.AddKeyException; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.operations.results.UploadResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; +import org.sufficientlysecure.keychain.pgp.Progressable; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ParcelableProxy; +import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.Preferences.ProxyPrefs; +import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; + + +/** + * An operation class which implements the upload of a single key to a key server. + */ +public class UploadOperation extends BaseOperation<UploadKeyringParcel> { + + public UploadOperation(Context context, ProviderHelper providerHelper, + Progressable progressable, AtomicBoolean cancelled) { + super(context, providerHelper, progressable, cancelled); + } + + @NonNull + public UploadResult execute(UploadKeyringParcel uploadInput, CryptoInputParcel cryptoInput) { + OperationLog log = new OperationLog(); + + log.add(LogType.MSG_UPLOAD, 0); + updateProgress(R.string.progress_uploading, 0, 1); + + Proxy proxy; + { + boolean proxyIsTor = false; + + // Proxy priorities: + // 1. explicit proxy + // 2. orbot proxy state + // 3. proxy from preferences + ParcelableProxy parcelableProxy = cryptoInput.getParcelableProxy(); + if (parcelableProxy != null) { + proxy = parcelableProxy.getProxy(); + } else { + if ( ! OrbotHelper.isOrbotInRequiredState(mContext)) { + return new UploadResult(log, RequiredInputParcel.createOrbotRequiredOperation(), cryptoInput); + } + ProxyPrefs proxyPrefs = Preferences.getPreferences(mContext).getProxyPrefs(); + if (proxyPrefs.torEnabled) { + proxyIsTor = true; + } + proxy = proxyPrefs.getProxy(); + } + + if (proxyIsTor) { + log.add(LogType.MSG_UPLOAD_PROXY_TOR, 1); + } else if (proxy == Proxy.NO_PROXY) { + log.add(LogType.MSG_UPLOAD_PROXY_DIRECT, 1); + } else { + log.add(LogType.MSG_UPLOAD_PROXY, 1, proxy.toString()); + } + + } + + HkpKeyserver hkpKeyserver; + { + hkpKeyserver = new HkpKeyserver(uploadInput.mKeyserver, proxy); + log.add(LogType.MSG_UPLOAD_SERVER, 1, hkpKeyserver.toString()); + } + + CanonicalizedPublicKeyRing keyring = getPublicKeyringFromInput(log, uploadInput); + if (keyring == null) { + return new UploadResult(UploadResult.RESULT_ERROR, log); + } + + return uploadKeyRingToServer(log, hkpKeyserver, keyring); + } + + @Nullable + private CanonicalizedPublicKeyRing getPublicKeyringFromInput(OperationLog log, UploadKeyringParcel uploadInput) { + + boolean hasMasterKeyId = uploadInput.mMasterKeyId != null; + boolean hasKeyringBytes = uploadInput.mUncachedKeyringBytes != null; + if (hasMasterKeyId == hasKeyringBytes) { + throw new IllegalArgumentException("either keyid xor bytes must be non-null for this method call!"); + } + + try { + + if (hasMasterKeyId) { + log.add(LogType.MSG_UPLOAD_KEY, 0, KeyFormattingUtils.convertKeyIdToHex(uploadInput.mMasterKeyId)); + return mProviderHelper.getCanonicalizedPublicKeyRing(uploadInput.mMasterKeyId); + } + + CanonicalizedKeyRing canonicalizedRing = + UncachedKeyRing.decodeFromData(uploadInput.mUncachedKeyringBytes) + .canonicalize(new OperationLog(), 0, true); + if ( ! CanonicalizedPublicKeyRing.class.isInstance(canonicalizedRing)) { + throw new IllegalArgumentException("keyring bytes must contain public key ring!"); + } + log.add(LogType.MSG_UPLOAD_KEY, 0, KeyFormattingUtils.convertKeyIdToHex(canonicalizedRing.getMasterKeyId())); + return (CanonicalizedPublicKeyRing) canonicalizedRing; + + } catch (ProviderHelper.NotFoundException e) { + log.add(LogType.MSG_UPLOAD_ERROR_NOT_FOUND, 1); + return null; + } catch (IOException | PgpGeneralException e) { + log.add(LogType.MSG_UPLOAD_ERROR_IO, 1); + Log.e(Constants.TAG, "error uploading key", e); + return null; + } + + } + + @NonNull + private UploadResult uploadKeyRingToServer( + OperationLog log, HkpKeyserver server, CanonicalizedPublicKeyRing keyring) { + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ArmoredOutputStream aos = null; + + try { + aos = new ArmoredOutputStream(bos); + keyring.encode(aos); + aos.close(); + + String armoredKey = bos.toString("UTF-8"); + server.add(armoredKey); + + updateProgress(R.string.progress_uploading, 1, 1); + + log.add(LogType.MSG_UPLOAD_SUCCESS, 1); + return new UploadResult(UploadResult.RESULT_OK, log); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException", e); + + log.add(LogType.MSG_UPLOAD_ERROR_IO, 1); + return new UploadResult(UploadResult.RESULT_ERROR, log); + } catch (AddKeyException e) { + Log.e(Constants.TAG, "AddKeyException", e); + + log.add(LogType.MSG_UPLOAD_ERROR_UPLOAD, 1); + return new UploadResult(UploadResult.RESULT_ERROR, log); + } finally { + try { + if (aos != null) { + aos.close(); + } + bos.close(); + } catch (IOException e) { + // this is just a finally thing, no matter if it doesn't work out. + } + } + } + +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/BenchmarkResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/BenchmarkResult.java new file mode 100644 index 000000000..473ae9886 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/BenchmarkResult.java @@ -0,0 +1,50 @@ +/* + * 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.operations.results; + +import android.os.Parcel; + + +public class BenchmarkResult extends OperationResult { + + public BenchmarkResult(int result, OperationLog log) { + super(result, log); + } + + /** Construct from a parcel - trivial because we have no extra data. */ + public BenchmarkResult(Parcel source) { + super(source); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + } + + public static Creator<BenchmarkResult> CREATOR = new Creator<BenchmarkResult>() { + public BenchmarkResult createFromParcel(final Parcel source) { + return new BenchmarkResult(source); + } + + public BenchmarkResult[] newArray(final int size) { + return new BenchmarkResult[size]; + } + }; + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java index 95cf179af..f19ba5250 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/DecryptVerifyResult.java @@ -39,6 +39,8 @@ public class DecryptVerifyResult extends InputPendingResult { byte[] mOutputBytes; + public long mOperationTime; + public DecryptVerifyResult(int result, OperationLog log) { super(result, log); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/EditKeyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/EditKeyResult.java index 6098d59d5..fa383a7b5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/EditKeyResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/EditKeyResult.java @@ -38,6 +38,11 @@ public class EditKeyResult extends InputPendingResult { mMasterKeyId = null; } + public EditKeyResult(OperationLog log, InputPendingResult result) { + super(log, result); + mMasterKeyId = null; + } + public EditKeyResult(Parcel source) { super(source); mMasterKeyId = source.readInt() != 0 ? source.readLong() : null; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java index e21ef949f..135f5af3d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/ExportResult.java @@ -24,39 +24,18 @@ import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; public class ExportResult extends InputPendingResult { - final int mOkPublic, mOkSecret; - public ExportResult(int result, OperationLog log) { - this(result, log, 0, 0); - } - - public ExportResult(int result, OperationLog log, int okPublic, int okSecret) { super(result, log); - mOkPublic = okPublic; - mOkSecret = okSecret; - } - - - public ExportResult(OperationLog log, RequiredInputParcel requiredInputParcel, - CryptoInputParcel cryptoInputParcel) { - super(log, requiredInputParcel, cryptoInputParcel); - // we won't use these values - mOkPublic = -1; - mOkSecret = -1; } /** Construct from a parcel - trivial because we have no extra data. */ public ExportResult(Parcel source) { super(source); - mOkPublic = source.readInt(); - mOkSecret = source.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); - dest.writeInt(mOkPublic); - dest.writeInt(mOkSecret); } public static Creator<ExportResult> CREATOR = new Creator<ExportResult>() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java index 0a8c1f653..ed6674ef7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/InputPendingResult.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.operations.results; import android.os.Parcel; +import android.support.annotation.NonNull; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; @@ -32,13 +33,13 @@ public class InputPendingResult extends OperationResult { // in case operation needs to add to/changes the cryptoInputParcel sent to it public final CryptoInputParcel mCryptoInputParcel; - public InputPendingResult(int result, OperationLog log) { + public InputPendingResult(int result, @NonNull OperationLog log) { super(result, log); mRequiredInput = null; mCryptoInputParcel = null; } - public InputPendingResult(OperationLog log, InputPendingResult result) { + public InputPendingResult(@NonNull OperationLog log, @NonNull InputPendingResult result) { super(RESULT_PENDING, log); if (!result.isPending()) { throw new AssertionError("sub result must be pending!"); @@ -47,7 +48,7 @@ public class InputPendingResult extends OperationResult { mCryptoInputParcel = result.mCryptoInputParcel; } - public InputPendingResult(OperationLog log, RequiredInputParcel requiredInput, + public InputPendingResult(@NonNull OperationLog log, RequiredInputParcel requiredInput, CryptoInputParcel cryptoInputParcel) { super(RESULT_PENDING, log); mRequiredInput = requiredInput; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java index a03658808..9877f2318 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java @@ -474,6 +474,7 @@ public abstract class OperationResult implements Parcelable { MSG_KC_UID_BAD (LogLevel.WARN, R.string.msg_kc_uid_bad), MSG_KC_UID_CERT_DUP (LogLevel.DEBUG, R.string.msg_kc_uid_cert_dup), MSG_KC_UID_DUP (LogLevel.DEBUG, R.string.msg_kc_uid_dup), + MSG_KC_UID_TOO_MANY (LogLevel.DEBUG, R.string.msg_kc_uid_too_many), MSG_KC_UID_FOREIGN (LogLevel.DEBUG, R.string.msg_kc_uid_foreign), MSG_KC_UID_NO_CERT (LogLevel.DEBUG, R.string.msg_kc_uid_no_cert), MSG_KC_UID_REVOKE_DUP (LogLevel.DEBUG, R.string.msg_kc_uid_revoke_dup), @@ -513,7 +514,7 @@ public abstract class OperationResult implements Parcelable { MSG_CR_ERROR_NO_USER_ID (LogLevel.ERROR, R.string.msg_cr_error_no_user_id), MSG_CR_ERROR_NO_CERTIFY (LogLevel.ERROR, R.string.msg_cr_error_no_certify), MSG_CR_ERROR_NULL_EXPIRY(LogLevel.ERROR, R.string.msg_cr_error_null_expiry), - MSG_CR_ERROR_KEYSIZE_512 (LogLevel.ERROR, R.string.msg_cr_error_keysize_512), + MSG_CR_ERROR_KEYSIZE_2048(LogLevel.ERROR, R.string.msg_cr_error_keysize_2048), MSG_CR_ERROR_NO_KEYSIZE (LogLevel.ERROR, R.string.msg_cr_error_no_keysize), MSG_CR_ERROR_NO_CURVE (LogLevel.ERROR, R.string.msg_cr_error_no_curve), MSG_CR_ERROR_UNKNOWN_ALGO (LogLevel.ERROR, R.string.msg_cr_error_unknown_algo), @@ -634,10 +635,13 @@ public abstract class OperationResult implements Parcelable { MSG_EK_ERROR_NOT_FOUND (LogLevel.ERROR, R.string.msg_ek_error_not_found), // decryptverify + MSG_DC_ASKIP_BAD_FLAGS (LogLevel.DEBUG, R.string.msg_dc_askip_bad_flags), + MSG_DC_ASKIP_UNAVAILABLE (LogLevel.DEBUG, R.string.msg_dc_askip_unavailable), MSG_DC_ASKIP_NO_KEY (LogLevel.DEBUG, R.string.msg_dc_askip_no_key), MSG_DC_ASKIP_NOT_ALLOWED (LogLevel.DEBUG, R.string.msg_dc_askip_not_allowed), MSG_DC_ASYM (LogLevel.DEBUG, R.string.msg_dc_asym), MSG_DC_CHARSET (LogLevel.DEBUG, R.string.msg_dc_charset), + MSG_DC_BACKUP_VERSION (LogLevel.DEBUG, R.string.msg_dc_backup_version), MSG_DC_CLEAR_DATA (LogLevel.DEBUG, R.string.msg_dc_clear_data), MSG_DC_CLEAR_DECOMPRESS (LogLevel.DEBUG, R.string.msg_dc_clear_decompress), MSG_DC_CLEAR_META_FILE (LogLevel.DEBUG, R.string.msg_dc_clear_meta_file), @@ -660,6 +664,7 @@ public abstract class OperationResult implements Parcelable { MSG_DC_ERROR_INPUT (LogLevel.ERROR, R.string.msg_dc_error_input), MSG_DC_ERROR_NO_DATA (LogLevel.ERROR, R.string.msg_dc_error_no_data), MSG_DC_ERROR_NO_KEY (LogLevel.ERROR, R.string.msg_dc_error_no_key), + MSG_DC_ERROR_NO_SIGNATURE (LogLevel.ERROR, R.string.msg_dc_error_no_signature), MSG_DC_ERROR_PGP_EXCEPTION (LogLevel.ERROR, R.string.msg_dc_error_pgp_exception), MSG_DC_INTEGRITY_CHECK_OK (LogLevel.INFO, R.string.msg_dc_integrity_check_ok), MSG_DC_OK_META_ONLY (LogLevel.OK, R.string.msg_dc_ok_meta_only), @@ -686,6 +691,7 @@ public abstract class OperationResult implements Parcelable { MSG_VL_ERROR_MISSING_SIGLIST (LogLevel.ERROR, R.string.msg_vl_error_no_siglist), MSG_VL_ERROR_MISSING_LITERAL (LogLevel.ERROR, R.string.msg_vl_error_missing_literal), MSG_VL_ERROR_MISSING_KEY (LogLevel.ERROR, R.string.msg_vl_error_wrong_key), + MSG_VL_ERROR_NO_SIGNATURE (LogLevel.ERROR, R.string.msg_vl_error_no_signature), MSG_VL_CLEAR_SIGNATURE_CHECK (LogLevel.DEBUG, R.string.msg_vl_clear_signature_check), MSG_VL_ERROR_INTEGRITY_CHECK (LogLevel.ERROR, R.string.msg_vl_error_integrity_check), MSG_VL_OK (LogLevel.OK, R.string.msg_vl_ok), @@ -702,7 +708,6 @@ public abstract class OperationResult implements Parcelable { // pgpsignencrypt MSG_PSE_ASYMMETRIC (LogLevel.INFO, R.string.msg_pse_asymmetric), - MSG_PSE_CLEARSIGN_ONLY (LogLevel.DEBUG, R.string.msg_pse_clearsign_only), MSG_PSE_COMPRESSING (LogLevel.DEBUG, R.string.msg_pse_compressing), MSG_PSE_ENCRYPTING (LogLevel.DEBUG, R.string.msg_pse_encrypting), MSG_PSE_ERROR_BAD_PASSPHRASE (LogLevel.ERROR, R.string.msg_pse_error_bad_passphrase), @@ -762,23 +767,25 @@ public abstract class OperationResult implements Parcelable { MSG_IMPORT_PARTIAL (LogLevel.ERROR, R.string.msg_import_partial), MSG_IMPORT_SUCCESS (LogLevel.OK, R.string.msg_import_success), - MSG_EXPORT (LogLevel.START, R.plurals.msg_export), - MSG_EXPORT_FILE_NAME (LogLevel.INFO, R.string.msg_export_file_name), - MSG_EXPORT_UPLOAD_PUBLIC (LogLevel.START, R.string.msg_export_upload_public), - MSG_EXPORT_PUBLIC (LogLevel.DEBUG, R.string.msg_export_public), - MSG_EXPORT_SECRET (LogLevel.DEBUG, R.string.msg_export_secret), - MSG_EXPORT_ALL (LogLevel.START, R.string.msg_export_all), - MSG_EXPORT_ERROR_NO_FILE (LogLevel.ERROR, R.string.msg_export_error_no_file), - MSG_EXPORT_ERROR_FOPEN (LogLevel.ERROR, R.string.msg_export_error_fopen), - MSG_EXPORT_ERROR_NO_URI (LogLevel.ERROR, R.string.msg_export_error_no_uri), - MSG_EXPORT_ERROR_URI_OPEN (LogLevel.ERROR, R.string.msg_export_error_uri_open), - MSG_EXPORT_ERROR_STORAGE (LogLevel.ERROR, R.string.msg_export_error_storage), - MSG_EXPORT_ERROR_DB (LogLevel.ERROR, R.string.msg_export_error_db), - MSG_EXPORT_ERROR_IO (LogLevel.ERROR, R.string.msg_export_error_io), - MSG_EXPORT_ERROR_KEY (LogLevel.ERROR, R.string.msg_export_error_key), - MSG_EXPORT_ERROR_UPLOAD (LogLevel.ERROR, R.string.msg_export_error_upload), - MSG_EXPORT_SUCCESS (LogLevel.OK, R.string.msg_export_success), - MSG_EXPORT_UPLOAD_SUCCESS (LogLevel.OK, R.string.msg_export_upload_success), + MSG_BACKUP(LogLevel.START, R.plurals.msg_backup), + MSG_BACKUP_PUBLIC(LogLevel.DEBUG, R.string.msg_backup_public), + MSG_BACKUP_SECRET(LogLevel.DEBUG, R.string.msg_backup_secret), + MSG_BACKUP_ALL(LogLevel.START, R.string.msg_backup_all), + MSG_BACKUP_ERROR_URI_OPEN(LogLevel.ERROR, R.string.msg_backup_error_uri_open), + MSG_BACKUP_ERROR_DB(LogLevel.ERROR, R.string.msg_backup_error_db), + MSG_BACKUP_ERROR_IO(LogLevel.ERROR, R.string.msg_backup_error_io), + MSG_BACKUP_SUCCESS(LogLevel.OK, R.string.msg_backup_success), + + MSG_UPLOAD(LogLevel.START, R.string.msg_upload), + MSG_UPLOAD_KEY(LogLevel.INFO, R.string.msg_upload_key), + MSG_UPLOAD_PROXY_DIRECT(LogLevel.DEBUG, R.string.msg_upload_proxy_direct), + MSG_UPLOAD_PROXY_TOR(LogLevel.DEBUG, R.string.msg_upload_proxy_tor), + MSG_UPLOAD_PROXY(LogLevel.DEBUG, R.string.msg_upload_proxy), + MSG_UPLOAD_SERVER(LogLevel.DEBUG, R.string.msg_upload_server), + MSG_UPLOAD_SUCCESS(LogLevel.OK, R.string.msg_upload_success), + MSG_UPLOAD_ERROR_NOT_FOUND(LogLevel.ERROR, R.string.msg_upload_error_not_found), + MSG_UPLOAD_ERROR_IO(LogLevel.ERROR, R.string.msg_upload_error_key), + MSG_UPLOAD_ERROR_UPLOAD(LogLevel.ERROR, R.string.msg_upload_error_upload), MSG_CRT_UPLOAD_SUCCESS (LogLevel.OK, R.string.msg_crt_upload_success), @@ -829,7 +836,6 @@ public abstract class OperationResult implements Parcelable { MSG_DATA (LogLevel.START, R.string.msg_data), MSG_DATA_OPENPGP (LogLevel.DEBUG, R.string.msg_data_openpgp), MSG_DATA_ERROR_IO (LogLevel.ERROR, R.string.msg_data_error_io), - MSG_DATA_ERROR_OPENPGP (LogLevel.ERROR, R.string.msg_data_error_openpgp), MSG_DATA_DETACHED (LogLevel.INFO, R.string.msg_data_detached), MSG_DATA_DETACHED_CLEAR (LogLevel.WARN, R.string.msg_data_detached_clear), MSG_DATA_DETACHED_SIG (LogLevel.DEBUG, R.string.msg_data_detached_sig), @@ -838,6 +844,7 @@ public abstract class OperationResult implements Parcelable { MSG_DATA_DETACHED_TRAILING (LogLevel.WARN, R.string.msg_data_detached_trailing), MSG_DATA_DETACHED_UNSUPPORTED (LogLevel.WARN, R.string.msg_data_detached_unsupported), MSG_DATA_MIME_BAD(LogLevel.INFO, R.string.msg_data_mime_bad), + MSG_DATA_MIME_FROM_EXTENSION (LogLevel.DEBUG, R.string.msg_data_mime_from_extension), MSG_DATA_MIME_FILENAME (LogLevel.DEBUG, R.string.msg_data_mime_filename), MSG_DATA_MIME_LENGTH (LogLevel.DEBUG, R.string.msg_data_mime_length), MSG_DATA_MIME (LogLevel.DEBUG, R.string.msg_data_mime), @@ -868,6 +875,16 @@ public abstract class OperationResult implements Parcelable { MSG_LV_FETCH_ERROR_IO (LogLevel.ERROR, R.string.msg_lv_fetch_error_io), MSG_LV_FETCH_ERROR_FORMAT(LogLevel.ERROR, R.string.msg_lv_fetch_error_format), MSG_LV_FETCH_ERROR_NOTHING (LogLevel.ERROR, R.string.msg_lv_fetch_error_nothing), + + MSG_BENCH (LogLevel.START, R.string.msg_bench), + MSG_BENCH_ENC_TIME (LogLevel.DEBUG, R.string.msg_bench_enc_time), + MSG_BENCH_ENC_TIME_AVG (LogLevel.INFO, R.string.msg_bench_enc_time_avg), + MSG_BENCH_DEC_TIME (LogLevel.DEBUG, R.string.msg_bench_dec_time), + MSG_BENCH_DEC_TIME_AVG (LogLevel.INFO, R.string.msg_bench_enc_time_avg), + MSG_BENCH_S2K_FOR_IT (LogLevel.DEBUG, R.string.msg_bench_s2k_for_it), + MSG_BENCH_S2K_100MS_ITS (LogLevel.INFO, R.string.msg_bench_s2k_100ms_its), + MSG_BENCH_SUCCESS (LogLevel.OK, R.string.msg_bench_success), + ; public final int mMsgId; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpSignEncryptResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpSignEncryptResult.java index 2b33b8ace..12b091e32 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpSignEncryptResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/PgpSignEncryptResult.java @@ -26,6 +26,7 @@ import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; public class PgpSignEncryptResult extends InputPendingResult { byte[] mDetachedSignature; + public long mOperationTime; public void setDetachedSignature(byte[] detachedSignature) { mDetachedSignature = detachedSignature; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/SignEncryptResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/SignEncryptResult.java index 0e0c5d598..60f47be3c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/SignEncryptResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/SignEncryptResult.java @@ -56,6 +56,10 @@ public class SignEncryptResult extends InputPendingResult { return mResultBytes; } + public ArrayList<PgpSignEncryptResult> getResults() { + return mResults; + } + public int describeContents() { return 0; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java new file mode 100644 index 000000000..ea2b373a9 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/UploadResult.java @@ -0,0 +1,74 @@ +/* + * 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.operations.results; + +import android.os.Parcel; +import android.support.annotation.NonNull; + +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; + + +public class UploadResult extends InputPendingResult { + + final int mOkPublic, mOkSecret; + + public UploadResult(int result, OperationLog log) { + this(result, log, 0, 0); + } + + public UploadResult(int result, OperationLog log, int okPublic, int okSecret) { + super(result, log); + mOkPublic = okPublic; + mOkSecret = okSecret; + } + + + public UploadResult(@NonNull OperationLog log, RequiredInputParcel requiredInputParcel, + CryptoInputParcel cryptoInputParcel) { + super(log, requiredInputParcel, cryptoInputParcel); + // we won't use these values + mOkPublic = -1; + mOkSecret = -1; + } + + /** Construct from a parcel - trivial because we have no extra data. */ + public UploadResult(Parcel source) { + super(source); + mOkPublic = source.readInt(); + mOkSecret = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mOkPublic); + dest.writeInt(mOkSecret); + } + + public static Creator<UploadResult> CREATOR = new Creator<UploadResult>() { + public UploadResult createFromParcel(final Parcel source) { + return new UploadResult(source); + } + + public UploadResult[] newArray(final int size) { + return new UploadResult[size]; + } + }; + +} 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 18a27dd96..6f1e78ce6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java @@ -154,8 +154,13 @@ public abstract class CanonicalizedKeyRing extends KeyRing { return getRing().getEncoded(); } - public boolean containsSubkey(String expectedFingerprint) { + /// Returns true iff the keyring contains a primary key or mutually bound subkey with the expected fingerprint + public boolean containsBoundSubkey(String expectedFingerprint) { for (CanonicalizedPublicKey key : publicKeyIterator()) { + boolean isMasterOrMutuallyBound = key.isMasterKey() || key.canSign(); + if (!isMasterOrMutuallyBound) { + continue; + } if (KeyFormattingUtils.convertFingerprintToHex( key.getFingerprint()).equalsIgnoreCase(expectedFingerprint)) { return true; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java index 412468a48..476b4e59c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedPublicKey.java @@ -44,13 +44,17 @@ import java.util.Iterator; public class CanonicalizedPublicKey extends UncachedPublicKey { // this is the parent key ring - final KeyRing mRing; + final CanonicalizedKeyRing mRing; - CanonicalizedPublicKey(KeyRing ring, PGPPublicKey key) { + CanonicalizedPublicKey(CanonicalizedKeyRing ring, PGPPublicKey key) { super(key); mRing = ring; } + public CanonicalizedKeyRing getKeyRing() { + return mRing; + } + public IterableIterator<String> getUserIds() { return new IterableIterator<String>(mPublicKey.getUserIDs()); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java index 9d059b58f..2dd1e2dde 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/OpenPgpSignatureResultBuilder.java @@ -91,11 +91,12 @@ public class OpenPgpSignatureResultBuilder { return mInsecure; } - public void initValid(CanonicalizedPublicKeyRing signingRing, - CanonicalizedPublicKey signingKey) { + public void initValid(CanonicalizedPublicKey signingKey) { setSignatureAvailable(true); setKnownKey(true); + CanonicalizedKeyRing signingRing = signingKey.getKeyRing(); + // from RING setKeyId(signingRing.getMasterKeyId()); try { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java index 3eef7759c..bc9b54cd6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java @@ -18,7 +18,7 @@ package org.sufficientlysecure.keychain.pgp; -import java.io.InputStream; + import java.util.HashSet; import android.net.Uri; @@ -36,7 +36,6 @@ public class PgpDecryptVerifyInputParcel implements Parcelable { private boolean mDecryptMetadataOnly; private byte[] mDetachedSignature; private String mRequiredSignerFingerprint; - private boolean mSignedLiteralData; public PgpDecryptVerifyInputParcel() { } @@ -61,7 +60,6 @@ public class PgpDecryptVerifyInputParcel implements Parcelable { mDecryptMetadataOnly = source.readInt() != 0; mDetachedSignature = source.createByteArray(); mRequiredSignerFingerprint = source.readString(); - mSignedLiteralData = source.readInt() != 0; } @Override @@ -80,7 +78,6 @@ public class PgpDecryptVerifyInputParcel implements Parcelable { dest.writeInt(mDecryptMetadataOnly ? 1 : 0); dest.writeByteArray(mDetachedSignature); dest.writeString(mRequiredSignerFingerprint); - dest.writeInt(mSignedLiteralData ? 1 : 0); } byte[] getInputBytes() { @@ -150,15 +147,6 @@ public class PgpDecryptVerifyInputParcel implements Parcelable { return this; } - boolean isSignedLiteralData() { - return mSignedLiteralData; - } - - public PgpDecryptVerifyInputParcel setSignedLiteralData(boolean signedLiteralData) { - mSignedLiteralData = signedLiteralData; - return this; - } - public static final Creator<PgpDecryptVerifyInputParcel> CREATOR = new Creator<PgpDecryptVerifyInputParcel>() { public PgpDecryptVerifyInputParcel createFromParcel(final Parcel source) { return new PgpDecryptVerifyInputParcel(source); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java index 007f686e8..ea7465209 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java @@ -18,13 +18,24 @@ package org.sufficientlysecure.keychain.pgp; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SignatureException; +import java.util.Date; +import java.util.Iterator; + import android.content.Context; import android.support.annotation.NonNull; +import android.text.TextUtils; import android.webkit.MimeTypeMap; import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpMetadata; -import org.openintents.openpgp.OpenPgpSignatureResult; import org.spongycastle.bcpg.ArmoredInputStream; import org.spongycastle.openpgp.PGPCompressedData; import org.spongycastle.openpgp.PGPDataValidationException; @@ -33,18 +44,14 @@ import org.spongycastle.openpgp.PGPEncryptedDataList; import org.spongycastle.openpgp.PGPException; import org.spongycastle.openpgp.PGPKeyValidationException; import org.spongycastle.openpgp.PGPLiteralData; -import org.spongycastle.openpgp.PGPOnePassSignature; -import org.spongycastle.openpgp.PGPOnePassSignatureList; import org.spongycastle.openpgp.PGPPBEEncryptedData; import org.spongycastle.openpgp.PGPPublicKeyEncryptedData; -import org.spongycastle.openpgp.PGPSignature; import org.spongycastle.openpgp.PGPSignatureList; import org.spongycastle.openpgp.PGPUtil; -import org.spongycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.spongycastle.openpgp.jcajce.JcaSkipMarkerPGPObjectFactory; import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory; import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider; import org.spongycastle.openpgp.operator.jcajce.CachingDataDecryptorFactory; -import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; import org.spongycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; import org.spongycastle.util.encoders.DecoderException; @@ -52,13 +59,13 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants.key; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.BaseOperation; +import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; -import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; @@ -68,17 +75,6 @@ import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.ProgressScaler; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.SignatureException; -import java.util.Date; -import java.util.Iterator; - public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInputParcel> { public PgpDecryptVerifyOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { @@ -91,6 +87,8 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp InputData inputData; OutputStream outputStream; + long startTime = System.currentTimeMillis(); + if (input.getInputBytes() != null) { byte[] inputBytes = input.getInputBytes(); inputData = new InputData(new ByteArrayInputStream(inputBytes), inputBytes.length); @@ -126,6 +124,8 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp result.setOutputBytes(outputData); } + result.mOperationTime = System.currentTimeMillis() - startTime; + Log.d(Constants.TAG, "total time taken: " + String.format("%.2f", result.mOperationTime / 1000.0) + "s"); return result; } @@ -153,9 +153,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp // it is ascii armored Log.d(Constants.TAG, "ASCII Armor Header Line: " + aIn.getArmorHeaderLine()); - if (input.isSignedLiteralData()) { - return verifySignedLiteralData(input, aIn, outputStream, 0); - } else if (aIn.isClearText()) { + if (aIn.isClearText()) { // a cleartext signature, verify it with the other method return verifyCleartextSignature(aIn, outputStream, 0); } else { @@ -186,133 +184,6 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp } } - /**Verify signed plaintext data (PGP/INLINE). */ - @NonNull - private DecryptVerifyResult verifySignedLiteralData( - PgpDecryptVerifyInputParcel input, InputStream in, OutputStream out, int indent) - throws IOException, PGPException { - OperationLog log = new OperationLog(); - log.add(LogType.MSG_VL, indent); - - // thinking that the proof-fetching operation is going to take most of the time - updateProgress(R.string.progress_reading_data, 75, 100); - - JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in); - Object o = pgpF.nextObject(); - if (o instanceof PGPCompressedData) { - log.add(LogType.MSG_DC_CLEAR_DECOMPRESS, indent + 1); - - pgpF = new JcaPGPObjectFactory(((PGPCompressedData) o).getDataStream()); - o = pgpF.nextObject(); - updateProgress(R.string.progress_decompressing_data, 80, 100); - } - - // all we want to see is a OnePassSignatureList followed by LiteralData - if (!(o instanceof PGPOnePassSignatureList)) { - log.add(LogType.MSG_VL_ERROR_MISSING_SIGLIST, indent); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); - } - PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) o; - - // go through all signatures (should be just one), make sure we have - // the key and it matches the one we’re looking for - CanonicalizedPublicKeyRing signingRing = null; - CanonicalizedPublicKey signingKey = null; - int signatureIndex = -1; - for (int i = 0; i < sigList.size(); ++i) { - try { - long sigKeyId = sigList.get(i).getKeyID(); - signingRing = mProviderHelper.getCanonicalizedPublicKeyRing( - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId) - ); - signingKey = signingRing.getPublicKey(sigKeyId); - signatureIndex = i; - } catch (ProviderHelper.NotFoundException e) { - Log.d(Constants.TAG, "key not found, trying next signature..."); - } - } - - // there has to be a key, and it has to be the right one - if (signingKey == null) { - log.add(LogType.MSG_VL_ERROR_MISSING_KEY, indent); - Log.d(Constants.TAG, "Failed to find key in signed-literal message"); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); - } - - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(signingRing.getFingerprint()); - if (!(input.getRequiredSignerFingerprint().equals(fingerprint))) { - log.add(LogType.MSG_VL_ERROR_MISSING_KEY, indent); - Log.d(Constants.TAG, "Fingerprint mismatch; wanted " + input.getRequiredSignerFingerprint() + - " got " + fingerprint + "!"); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); - } - - OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); - - PGPOnePassSignature signature = sigList.get(signatureIndex); - signatureResultBuilder.initValid(signingRing, signingKey); - - JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = - new JcaPGPContentVerifierBuilderProvider() - .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); - signature.init(contentVerifierBuilderProvider, signingKey.getPublicKey()); - - o = pgpF.nextObject(); - - if (!(o instanceof PGPLiteralData)) { - log.add(LogType.MSG_VL_ERROR_MISSING_LITERAL, indent); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); - } - - PGPLiteralData literalData = (PGPLiteralData) o; - - log.add(LogType.MSG_DC_CLEAR_DATA, indent + 1); - updateProgress(R.string.progress_decrypting, 85, 100); - - InputStream dataIn = literalData.getInputStream(); - - int length; - byte[] buffer = new byte[1 << 16]; - while ((length = dataIn.read(buffer)) > 0) { - out.write(buffer, 0, length); - signature.update(buffer, 0, length); - } - - updateProgress(R.string.progress_verifying_signature, 95, 100); - log.add(LogType.MSG_VL_CLEAR_SIGNATURE_CHECK, indent + 1); - - PGPSignatureList signatureList = (PGPSignatureList) pgpF.nextObject(); - PGPSignature messageSignature = signatureList.get(signatureIndex); - - // Verify signature and check binding signatures - boolean validSignature = signature.verify(messageSignature); - if (validSignature) { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); - } else { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); - } - signatureResultBuilder.setValidSignature(validSignature); - - OpenPgpSignatureResult signatureResult = signatureResultBuilder.build(); - - if (signatureResult.getResult() != OpenPgpSignatureResult.RESULT_VALID_CONFIRMED - && signatureResult.getResult() != OpenPgpSignatureResult.RESULT_VALID_UNCONFIRMED) { - log.add(LogType.MSG_VL_ERROR_INTEGRITY_CHECK, indent); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); - } - - updateProgress(R.string.progress_done, 100, 100); - - log.add(LogType.MSG_VL_OK, indent); - - // Return a positive result, with metadata and verification info - DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); - result.setSignatureResult(signatureResult); - result.setDecryptionResult( - new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED)); - return result; - } - private static class EncryptStreamResult { // this is non-null iff an error occured, return directly @@ -335,6 +206,57 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp } + private static class ArmorHeaders { + String charset = null; + Integer backupVersion = null; + } + + private ArmorHeaders parseArmorHeaders(InputStream in, OperationLog log, int indent) { + ArmorHeaders armorHeaders = new ArmorHeaders(); + + // If the input stream is armored, and there is a charset specified, take a note for later + // https://tools.ietf.org/html/rfc4880#page56 + if (in instanceof ArmoredInputStream) { + ArmoredInputStream aIn = (ArmoredInputStream) in; + if (aIn.getArmorHeaders() != null) { + for (String header : aIn.getArmorHeaders()) { + String[] pieces = header.split(":", 2); + if (pieces.length != 2 + || TextUtils.isEmpty(pieces[0]) + || TextUtils.isEmpty(pieces[1])) { + continue; + } + + switch (pieces[0].toLowerCase()) { + case "charset": { + armorHeaders.charset = pieces[1].trim(); + break; + } + case "backupversion": { + try { + armorHeaders.backupVersion = Integer.valueOf(pieces[1].trim()); + } catch (NumberFormatException e) { + continue; + } + break; + } + default: { + // continue; + } + } + } + if (armorHeaders.charset != null) { + log.add(LogType.MSG_DC_CHARSET, indent, armorHeaders.charset); + } + if (armorHeaders.backupVersion != null) { + log.add(LogType.MSG_DC_BACKUP_VERSION, indent, Integer.toString(armorHeaders.backupVersion)); + } + } + } + + return armorHeaders; + } + /** Decrypt and/or verify binary or ascii armored pgp data. */ @NonNull private DecryptVerifyResult decryptVerify( @@ -349,38 +271,27 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp int currentProgress = 0; updateProgress(R.string.progress_reading_data, currentProgress, 100); - // If the input stream is armored, and there is a charset specified, take a note for later - // https://tools.ietf.org/html/rfc4880#page56 - String charset = null; - if (in instanceof ArmoredInputStream) { - ArmoredInputStream aIn = (ArmoredInputStream) in; - if (aIn.getArmorHeaders() != null) { - for (String header : aIn.getArmorHeaders()) { - String[] pieces = header.split(":", 2); - if (pieces.length == 2 && "charset".equalsIgnoreCase(pieces[0])) { - charset = pieces[1].trim(); - break; - } - } - if (charset != null) { - log.add(LogType.MSG_DC_CHARSET, indent, charset); - } - } + // parse ASCII Armor headers + ArmorHeaders armorHeaders = parseArmorHeaders(in, log, indent); + String charset = armorHeaders.charset; + boolean useBackupCode = false; + if (armorHeaders.backupVersion != null && armorHeaders.backupVersion == 1) { + useBackupCode = true; } - OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder(); - JcaPGPObjectFactory plainFact; + JcaSkipMarkerPGPObjectFactory plainFact; Object dataChunk; EncryptStreamResult esResult = null; { // resolve encrypted (symmetric and asymmetric) packets - JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in); + JcaSkipMarkerPGPObjectFactory pgpF = new JcaSkipMarkerPGPObjectFactory(in); Object obj = pgpF.nextObject(); if (obj instanceof PGPEncryptedDataList) { esResult = handleEncryptedPacket( - input, cryptoInput, (PGPEncryptedDataList) obj, log, indent, currentProgress); + input, cryptoInput, (PGPEncryptedDataList) obj, log, indent, + currentProgress, useBackupCode); // if there is an error, nothing left to do here if (esResult.errorResult != null) { @@ -401,7 +312,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp decryptionResultBuilder.setInsecure(true); } - plainFact = new JcaPGPObjectFactory(esResult.cleartextStream); + plainFact = new JcaSkipMarkerPGPObjectFactory(esResult.cleartextStream); dataChunk = plainFact.nextObject(); } else { @@ -415,10 +326,6 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp log.add(LogType.MSG_DC_PREP_STREAMS, indent); - int signatureIndex = -1; - CanonicalizedPublicKeyRing signingRing = null; - CanonicalizedPublicKey signingKey = null; - log.add(LogType.MSG_DC_CLEAR, indent); indent += 1; @@ -430,63 +337,13 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp PGPCompressedData compressedData = (PGPCompressedData) dataChunk; - JcaPGPObjectFactory fact = new JcaPGPObjectFactory(compressedData.getDataStream()); + JcaSkipMarkerPGPObjectFactory fact = new JcaSkipMarkerPGPObjectFactory(compressedData.getDataStream()); dataChunk = fact.nextObject(); plainFact = fact; } - // resolve leading signature data - PGPOnePassSignature signature = null; - if (dataChunk instanceof PGPOnePassSignatureList) { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE, indent + 1); - currentProgress += 2; - updateProgress(R.string.progress_processing_signature, currentProgress, 100); - - PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk; - - // NOTE: following code is similar to processSignature, but for PGPOnePassSignature - - // go through all signatures - // and find out for which signature we have a key in our database - for (int i = 0; i < sigList.size(); ++i) { - try { - long sigKeyId = sigList.get(i).getKeyID(); - signingRing = mProviderHelper.getCanonicalizedPublicKeyRing( - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId) - ); - signingKey = signingRing.getPublicKey(sigKeyId); - signatureIndex = i; - } catch (ProviderHelper.NotFoundException e) { - Log.d(Constants.TAG, "key not found, trying next signature..."); - } - } - - if (signingKey != null) { - // key found in our database! - signature = sigList.get(signatureIndex); - - signatureResultBuilder.initValid(signingRing, signingKey); - - JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = - new JcaPGPContentVerifierBuilderProvider() - .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); - signature.init(contentVerifierBuilderProvider, signingKey.getPublicKey()); - } else { - // no key in our database -> return "unknown pub key" status including the first key id - if (!sigList.isEmpty()) { - signatureResultBuilder.setSignatureAvailable(true); - signatureResultBuilder.setKnownKey(false); - signatureResultBuilder.setKeyId(sigList.get(0).getKeyID()); - } - } - - // check for insecure signing key - // TODO: checks on signingRing ? - if (signingKey != null && ! PgpSecurityConstants.isSecureKey(signingKey)) { - log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1); - signatureResultBuilder.setInsecure(true); - } - + PgpSignatureChecker signatureChecker = new PgpSignatureChecker(mProviderHelper); + if (signatureChecker.initializeOnePassSignature(dataChunk, log, indent +1)) { dataChunk = plainFact.nextObject(); } @@ -512,8 +369,9 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp PGPLiteralData literalData = (PGPLiteralData) dataChunk; String originalFilename = literalData.getFileName(); + // reject filenames with slashes completely (path traversal issue) if (originalFilename.contains("/")) { - originalFilename = originalFilename.substring(originalFilename.lastIndexOf('/')); + originalFilename = ""; } String mimeType = null; if (literalData.getFormat() == PGPLiteralData.TEXT @@ -534,14 +392,14 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp if (!"".equals(originalFilename)) { log.add(LogType.MSG_DC_CLEAR_META_FILE, indent + 1, originalFilename); } - log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1, - mimeType); log.add(LogType.MSG_DC_CLEAR_META_TIME, indent + 1, new Date(literalData.getModificationTime().getTime()).toString()); // return here if we want to decrypt the metadata only if (input.isDecryptMetadataOnly()) { + log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1, mimeType); + // this operation skips the entire stream to find the data length! Long originalSize = literalData.findDataLength(); @@ -566,23 +424,18 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp return result; } - int endProgress; - if (signature != null) { - endProgress = 90; - } else if (esResult != null && esResult.encryptedData.isIntegrityProtected()) { - endProgress = 95; - } else { - endProgress = 100; - } ProgressScaler progressScaler = - new ProgressScaler(mProgressable, currentProgress, endProgress, 100); + new ProgressScaler(mProgressable, currentProgress, 95, 100); InputStream dataIn = literalData.getInputStream(); + long opTime, startTime = System.currentTimeMillis(); + long alreadyWritten = 0; long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition(); int length; - byte[] buffer = new byte[1 << 16]; + byte[] buffer = new byte[8192]; + byte[] firstBytes = new byte[48]; while ((length = dataIn.read(buffer)) > 0) { // Log.d(Constants.TAG, "read bytes: " + length); if (out != null) { @@ -590,11 +443,15 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp } // update signature buffer if signature is also present - if (signature != null) { - signature.update(buffer, 0, length); + signatureChecker.updateSignatureData(buffer, 0, length); + + // note down first couple of bytes for "magic bytes" file type detection + if (alreadyWritten == 0) { + System.arraycopy(buffer, 0, firstBytes, 0, length > firstBytes.length ? firstBytes.length : length); } alreadyWritten += length; + // noinspection ConstantConditions, TODO progress if (wholeSize > 0) { long progress = 100 * alreadyWritten / wholeSize; // stop at 100% for wrong file sizes... @@ -603,36 +460,36 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp } progressScaler.setProgress((int) progress, 100); } - // TODO: slow annealing to fake a progress? } - metadata = new OpenPgpMetadata( - originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten, charset); + if (signatureChecker.isInitialized()) { - if (signature != null) { - updateProgress(R.string.progress_verifying_signature, 90, 100); - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); + Object o = plainFact.nextObject(); + boolean signatureCheckOk = signatureChecker.verifySignatureOnePass(o, log, indent + 1); - PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject(); - PGPSignature messageSignature = signatureList.get(signatureIndex); - - // Verify signature - boolean validSignature = signature.verify(messageSignature); - if (validSignature) { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); - } else { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); + if (!signatureCheckOk) { + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } - // check for insecure hash algorithms - if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { - log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); - signatureResultBuilder.setInsecure(true); - } + } + + opTime = System.currentTimeMillis()-startTime; + Log.d(Constants.TAG, "decrypt time taken: " + String.format("%.2f", opTime / 1000.0) + "s"); - signatureResultBuilder.setValidSignature(validSignature); + // special treatment to detect pgp mime types + if (matchesPrefix(firstBytes, "-----BEGIN PGP PUBLIC KEY BLOCK-----") + || matchesPrefix(firstBytes, "-----BEGIN PGP PRIVATE KEY BLOCK-----")) { + mimeType = Constants.MIME_TYPE_KEYS; + } else if (matchesPrefix(firstBytes, "-----BEGIN PGP MESSAGE-----")) { + // this is NOT application/pgp-encrypted, see RFC 3156! + mimeType = Constants.MIME_TYPE_ENCRYPTED_ALTERNATE; } + log.add(LogType.MSG_DC_CLEAR_META_MIME, indent + 1, mimeType); + + metadata = new OpenPgpMetadata( + originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten, charset); + indent -= 1; if (esResult != null) { @@ -645,7 +502,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } - } else if (signature == null) { + } else if ( ! signatureChecker.isInitialized() ) { // If no signature is present, we *require* an MDC! // Handle missing integrity protection like failed integrity protection! // The MDC packet can be stripped by an attacker! @@ -662,23 +519,17 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); result.setCachedCryptoInputParcel(cryptoInput); - result.setSignatureResult(signatureResultBuilder.build()); + result.setSignatureResult(signatureChecker.getSignatureResult()); result.setDecryptionResult(decryptionResultBuilder.build()); result.setDecryptionMetadata(metadata); + result.mOperationTime = opTime; return result; } private EncryptStreamResult handleEncryptedPacket(PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput, - PGPEncryptedDataList enc, OperationLog log, int indent, int currentProgress) throws PGPException { - - // TODO is this necessary? - /* - else if (obj instanceof PGPEncryptedDataList) { - enc = (PGPEncryptedDataList) pgpF.nextObject(); - } - */ + PGPEncryptedDataList enc, OperationLog log, int indent, int currentProgress, boolean useBackupCode) throws PGPException { EncryptStreamResult result = new EncryptStreamResult(); @@ -720,11 +571,6 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1); continue; } - if (secretKeyRing == null) { - // continue with the next packet in the while loop - log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1); - continue; - } // allow only specific keys for decryption? if (input.getAllowedKeyIds() != null) { @@ -744,9 +590,16 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp // get subkey which has been used for this encryption packet secretEncryptionKey = secretKeyRing.getSecretKey(subKeyId); - if (secretEncryptionKey == null) { - // should actually never happen, so no need to be more specific. - log.add(LogType.MSG_DC_ASKIP_NO_KEY, indent + 1); + + if (!secretEncryptionKey.canEncrypt()) { + secretEncryptionKey = null; + log.add(LogType.MSG_DC_ASKIP_BAD_FLAGS, indent + 1); + continue; + } + + if (!secretEncryptionKey.getSecretKeyType().isUsable()) { + secretEncryptionKey = null; + log.add(LogType.MSG_DC_ASKIP_UNAVAILABLE, indent + 1); continue; } @@ -820,8 +673,11 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp if (passphrase == null) { log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1); + RequiredInputParcel requiredInputParcel = useBackupCode ? + RequiredInputParcel.createRequiredBackupCode() : + RequiredInputParcel.createRequiredSymmetricPassphrase(); return result.with(new DecryptVerifyResult(log, - RequiredInputParcel.createRequiredSymmetricPassphrase(), + requiredInputParcel, cryptoInput)); } @@ -864,8 +720,10 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp result.cleartextStream = encryptedDataSymmetric.getDataStream(decryptorFactory); } catch (PGPDataValidationException e) { log.add(LogType.MSG_DC_ERROR_SYM_PASSPHRASE, indent + 1); - return result.with(new DecryptVerifyResult(log, - RequiredInputParcel.createRequiredSymmetricPassphrase(), cryptoInput)); + RequiredInputParcel requiredInputParcel = useBackupCode ? + RequiredInputParcel.createRequiredBackupCode() : + RequiredInputParcel.createRequiredSymmetricPassphrase(); + return result.with(new DecryptVerifyResult(log, requiredInputParcel, cryptoInput)); } result.encryptedData = encryptedDataSymmetric; @@ -950,83 +808,53 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp OperationLog log = new OperationLog(); - OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] clearText; + { // read cleartext + ByteArrayOutputStream out = new ByteArrayOutputStream(); - updateProgress(R.string.progress_reading_data, 0, 100); + updateProgress(R.string.progress_reading_data, 0, 100); - ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); - int lookAhead = readInputLine(lineOut, aIn); - byte[] lineSep = getLineSeparator(); + ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); + int lookAhead = readInputLine(lineOut, aIn); + byte[] lineSep = getLineSeparator(); - byte[] line = lineOut.toByteArray(); - out.write(line, 0, getLengthWithoutSeparator(line)); - out.write(lineSep); - - while (lookAhead != -1 && aIn.isClearText()) { - lookAhead = readInputLine(lineOut, lookAhead, aIn); - line = lineOut.toByteArray(); + byte[] line = lineOut.toByteArray(); out.write(line, 0, getLengthWithoutSeparator(line)); out.write(lineSep); - } - out.close(); + while (lookAhead != -1 && aIn.isClearText()) { + lookAhead = readInputLine(lineOut, lookAhead, aIn); + line = lineOut.toByteArray(); + out.write(line, 0, getLengthWithoutSeparator(line)); + out.write(lineSep); + } + + out.close(); + clearText = out.toByteArray(); + } - byte[] clearText = out.toByteArray(); if (outputStream != null) { outputStream.write(clearText); outputStream.close(); } updateProgress(R.string.progress_processing_signature, 60, 100); - JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(aIn); + JcaSkipMarkerPGPObjectFactory pgpFact = new JcaSkipMarkerPGPObjectFactory(aIn); + + PgpSignatureChecker signatureChecker = new PgpSignatureChecker(mProviderHelper); - PGPSignatureList sigList = (PGPSignatureList) pgpFact.nextObject(); - if (sigList == null) { + Object o = pgpFact.nextObject(); + if (!signatureChecker.initializeSignature(o, log, indent+1)) { log.add(LogType.MSG_DC_ERROR_INVALID_DATA, 0); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } - PGPSignature signature = processPGPSignatureList(sigList, signatureResultBuilder, log, indent); - - if (signature != null) { + if (signatureChecker.isInitialized()) { try { updateProgress(R.string.progress_verifying_signature, 90, 100); - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); - - InputStream sigIn = new BufferedInputStream(new ByteArrayInputStream(clearText)); - - lookAhead = readInputLine(lineOut, sigIn); - - processLine(signature, lineOut.toByteArray()); - if (lookAhead != -1) { - do { - lookAhead = readInputLine(lineOut, lookAhead, sigIn); - - signature.update((byte) '\r'); - signature.update((byte) '\n'); - - processLine(signature, lineOut.toByteArray()); - } while (lookAhead != -1); - } - - // Verify signature and check binding signatures - boolean validSignature = signature.verify(); - if (validSignature) { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); - } else { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); - } - - // check for insecure hash algorithms - if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { - log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); - signatureResultBuilder.setInsecure(true); - } - - signatureResultBuilder.setValidSignature(validSignature); + signatureChecker.updateSignatureWithCleartext(clearText); + signatureChecker.verifySignature(log, indent); } catch (SignatureException e) { Log.d(Constants.TAG, "SignatureException", e); @@ -1045,7 +873,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp clearText.length); DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); - result.setSignatureResult(signatureResultBuilder.build()); + result.setSignatureResult(signatureChecker.getSignatureResult()); result.setDecryptionResult( new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED)); result.setDecryptionMetadata(metadata); @@ -1059,30 +887,28 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp OperationLog log = new OperationLog(); - OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); - updateProgress(R.string.progress_processing_signature, 0, 100); InputStream detachedSigIn = new ByteArrayInputStream(input.getDetachedSignature()); detachedSigIn = PGPUtil.getDecoderStream(detachedSigIn); - JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(detachedSigIn); + JcaSkipMarkerPGPObjectFactory pgpFact = new JcaSkipMarkerPGPObjectFactory(detachedSigIn); - PGPSignatureList sigList; Object o = pgpFact.nextObject(); if (o instanceof PGPCompressedData) { PGPCompressedData c1 = (PGPCompressedData) o; - pgpFact = new JcaPGPObjectFactory(c1.getDataStream()); - sigList = (PGPSignatureList) pgpFact.nextObject(); - } else if (o instanceof PGPSignatureList) { - sigList = (PGPSignatureList) o; - } else { + pgpFact = new JcaSkipMarkerPGPObjectFactory(c1.getDataStream()); + o = pgpFact.nextObject(); + } + + PgpSignatureChecker signatureChecker = new PgpSignatureChecker(mProviderHelper); + + if ( ! signatureChecker.initializeSignature(o, log, indent+1)) { log.add(LogType.MSG_DC_ERROR_INVALID_DATA, 0); return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); } - PGPSignature signature = processPGPSignatureList(sigList, signatureResultBuilder, log, indent); + if (signatureChecker.isInitialized()) { - if (signature != null) { updateProgress(R.string.progress_reading_data, 60, 100); ProgressScaler progressScaler = new ProgressScaler(mProgressable, 60, 90, 100); @@ -1097,7 +923,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp } // update signature buffer if signature is also present - signature.update(buffer, 0, length); + signatureChecker.updateSignatureData(buffer, 0, length); alreadyWritten += length; if (wholeSize > 0) { @@ -1108,105 +934,28 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp } progressScaler.setProgress((int) progress, 100); } - // TODO: slow annealing to fake a progress? } updateProgress(R.string.progress_verifying_signature, 90, 100); log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); - // Verify signature and check binding signatures - boolean validSignature = signature.verify(); - if (validSignature) { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); - } else { - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); - } - - // check for insecure hash algorithms - if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { - log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); - signatureResultBuilder.setInsecure(true); - } + signatureChecker.verifySignature(log, indent); - signatureResultBuilder.setValidSignature(validSignature); } updateProgress(R.string.progress_done, 100, 100); log.add(LogType.MSG_DC_OK, indent); + // TODO return metadata object? + DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); - result.setSignatureResult(signatureResultBuilder.build()); + result.setSignatureResult(signatureChecker.getSignatureResult()); result.setDecryptionResult( new OpenPgpDecryptionResult(OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED)); return result; } - private PGPSignature processPGPSignatureList( - PGPSignatureList sigList, OpenPgpSignatureResultBuilder signatureResultBuilder, - OperationLog log, int indent) - throws PGPException { - CanonicalizedPublicKeyRing signingRing = null; - CanonicalizedPublicKey signingKey = null; - int signatureIndex = -1; - - // go through all signatures - // and find out for which signature we have a key in our database - for (int i = 0; i < sigList.size(); ++i) { - try { - long sigKeyId = sigList.get(i).getKeyID(); - signingRing = mProviderHelper.getCanonicalizedPublicKeyRing( - KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId) - ); - signingKey = signingRing.getPublicKey(sigKeyId); - signatureIndex = i; - } catch (ProviderHelper.NotFoundException e) { - Log.d(Constants.TAG, "key not found, trying next signature..."); - } - } - - PGPSignature signature = null; - - if (signingKey != null) { - // key found in our database! - signature = sigList.get(signatureIndex); - - signatureResultBuilder.initValid(signingRing, signingKey); - - JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = - new JcaPGPContentVerifierBuilderProvider() - .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); - signature.init(contentVerifierBuilderProvider, signingKey.getPublicKey()); - } else { - // no key in our database -> return "unknown pub key" status including the first key id - if (!sigList.isEmpty()) { - signatureResultBuilder.setSignatureAvailable(true); - signatureResultBuilder.setKnownKey(false); - signatureResultBuilder.setKeyId(sigList.get(0).getKeyID()); - } - } - - // check for insecure signing key - // TODO: checks on signingRing ? - if (signingKey != null && ! PgpSecurityConstants.isSecureKey(signingKey)) { - log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1); - signatureResultBuilder.setInsecure(true); - } - - return signature; - } - - /** - * Mostly taken from ClearSignedFileProcessor in Bouncy Castle - */ - private static void processLine(PGPSignature sig, byte[] line) - throws SignatureException { - int length = getLengthWithoutWhiteSpace(line); - if (length > 0) { - sig.update(line, 0, length); - } - } - private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn) throws IOException { bOut.reset(); @@ -1272,22 +1021,21 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp return b == '\r' || b == '\n'; } - private static int getLengthWithoutWhiteSpace(byte[] line) { - int end = line.length - 1; - - while (end >= 0 && isWhiteSpace(line[end])) { - end--; - } - - return end + 1; - } - - private static boolean isWhiteSpace(byte b) { - return b == '\r' || b == '\n' || b == '\t' || b == ' '; - } - private static byte[] getLineSeparator() { String nl = System.getProperty("line.separator"); return nl.getBytes(); } + + /// Convenience method - Trivially checks if a byte array matches the bytes of a plain text string + // Assumes data.length >= needle.length() + static boolean matchesPrefix(byte[] data, String needle) { + byte[] needleBytes = needle.getBytes(); + for (int i = 0; i < needle.length(); i++) { + if (data[i] != needleBytes[i]) { + return false; + } + } + return true; + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java index e8d1d3111..016651c3b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java @@ -18,23 +18,15 @@ package org.sufficientlysecure.keychain.pgp; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import android.support.annotation.NonNull; import android.text.TextUtils; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.util.Preferences; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.security.SecureRandom; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class PgpHelper { @@ -51,35 +43,6 @@ public class PgpHelper { Pattern.DOTALL); /** - * Deletes file securely by overwriting it with random data before deleting it. - * <p/> - * TODO: Does this really help on flash storage? - * - * @throws IOException - */ - public static void deleteFileSecurely(Context context, Progressable progressable, File file) - throws IOException { - long length = file.length(); - SecureRandom random = new SecureRandom(); - RandomAccessFile raf = new RandomAccessFile(file, "rws"); - raf.seek(0); - raf.getFilePointer(); - byte[] data = new byte[1 << 16]; - int pos = 0; - String msg = context.getString(R.string.progress_deleting_securely, file.getName()); - while (pos < length) { - if (progressable != null) { - progressable.setProgress(msg, (int) (100 * pos / length), 100); - } - random.nextBytes(data); - raf.write(data); - pos += data.length; - } - raf.close(); - file.delete(); - } - - /** * Fixing broken PGP MESSAGE Strings coming from GMail/AOSP Mail */ public static String fixPgpMessage(String message) { @@ -117,7 +80,7 @@ public class PgpHelper { } } - public static String getPgpContent(@NonNull CharSequence input) { + public static String getPgpMessageContent(@NonNull CharSequence input) { Log.dEscaped(Constants.TAG, "input: " + input); Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input); @@ -141,4 +104,18 @@ public class PgpHelper { } } + public static String getPgpKeyContent(@NonNull CharSequence input) { + Log.dEscaped(Constants.TAG, "input: " + input); + + Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(input); + if (matcher.matches()) { + String text = matcher.group(1); + text = fixPgpMessage(text); + + Log.dEscaped(Constants.TAG, "input fixed: " + text); + return text; + } + return null; + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java index 6f156c201..59b840054 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java @@ -171,8 +171,8 @@ public class PgpKeyOperation { log.add(LogType.MSG_CR_ERROR_NO_KEYSIZE, indent); return null; } - if (add.mKeySize < 512) { - log.add(LogType.MSG_CR_ERROR_KEYSIZE_512, indent); + if (add.mKeySize < 2048) { + log.add(LogType.MSG_CR_ERROR_KEYSIZE_2048, indent); return null; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java index cbd8ce47a..7ad7b4d0f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java @@ -79,8 +79,8 @@ public class PgpSecurityConstants { */ private static HashSet<Integer> sHashAlgorithmsWhitelist = new HashSet<>(Arrays.asList( // MD5: broken - // SHA1: broken - // RIPEMD160: same security properties as SHA1 + HashAlgorithmTags.SHA1, // TODO: disable when SHA256 is widely deployed + HashAlgorithmTags.RIPEMD160, // same security properties as SHA1, TODO: disable when SHA256 is widely deployed // DOUBLE_SHA: not used widely // MD2: not used widely // TIGER_192: not used widely diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java index 36d1a07cb..c2c6234eb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptInputParcel.java @@ -44,6 +44,7 @@ public class PgpSignEncryptInputParcel implements Parcelable { protected boolean mDetachedSignature = false; protected boolean mHiddenRecipients = false; protected boolean mIntegrityProtected = true; + protected boolean mAddBackupHeader = false; public PgpSignEncryptInputParcel() { @@ -70,6 +71,7 @@ public class PgpSignEncryptInputParcel implements Parcelable { mDetachedSignature = source.readInt() == 1; mHiddenRecipients = source.readInt() == 1; mIntegrityProtected = source.readInt() == 1; + mAddBackupHeader = source.readInt() == 1; } @Override @@ -100,6 +102,7 @@ public class PgpSignEncryptInputParcel implements Parcelable { dest.writeInt(mDetachedSignature ? 1 : 0); dest.writeInt(mHiddenRecipients ? 1 : 0); dest.writeInt(mIntegrityProtected ? 1 : 0); + dest.writeInt(mAddBackupHeader ? 1 : 0); } public String getCharset() { @@ -244,6 +247,15 @@ public class PgpSignEncryptInputParcel implements Parcelable { return this; } + public PgpSignEncryptInputParcel setAddBackupHeader(boolean addBackupHeader) { + this.mAddBackupHeader = addBackupHeader; + return this; + } + + public boolean isAddBackupHeader() { + return mAddBackupHeader; + } + public boolean isHiddenRecipients() { return mHiddenRecipients; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java index 29b2ef727..45641b33a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java @@ -53,6 +53,7 @@ import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.ProgressScaler; +import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@ -148,6 +149,10 @@ public class PgpSignEncryptOperation extends BaseOperation { if (input.getCharset() != null) { armorOut.setHeader("Charset", input.getCharset()); } + // add proprietary header to indicate that this is a key backup + if (input.isAddBackupHeader()) { + armorOut.setHeader("BackupVersion", "1"); + } out = armorOut; } else { out = outputStream; @@ -316,6 +321,8 @@ public class PgpSignEncryptOperation extends BaseOperation { ArmoredOutputStream detachedArmorOut = null; BCPGOutputStream detachedBcpgOut = null; + long opTime, startTime = System.currentTimeMillis(); + try { if (enableEncryption) { @@ -361,7 +368,7 @@ public class PgpSignEncryptOperation extends BaseOperation { long alreadyWritten = 0; int length; byte[] buffer = new byte[1 << 16]; - InputStream in = inputData.getInputStream(); + InputStream in = new BufferedInputStream(inputData.getInputStream()); while ((length = in.read(buffer)) > 0) { pOut.write(buffer, 0, length); @@ -389,7 +396,7 @@ public class PgpSignEncryptOperation extends BaseOperation { // write -----BEGIN PGP SIGNED MESSAGE----- armorOut.beginClearText(input.getSignatureHashAlgorithm()); - InputStream in = inputData.getInputStream(); + InputStream in = new BufferedInputStream(inputData.getInputStream()); final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); // update signature buffer with first line @@ -421,7 +428,7 @@ public class PgpSignEncryptOperation extends BaseOperation { updateProgress(R.string.progress_signing, 8, 100); log.add(LogType.MSG_PSE_SIGNING_DETACHED, indent); - InputStream in = inputData.getInputStream(); + InputStream in = new BufferedInputStream(inputData.getInputStream()); // handle output stream separately for detached signatures detachedByteOut = new ByteArrayOutputStream(); @@ -458,7 +465,7 @@ public class PgpSignEncryptOperation extends BaseOperation { updateProgress(R.string.progress_signing, 8, 100); log.add(LogType.MSG_PSE_SIGNING, indent); - InputStream in = inputData.getInputStream(); + InputStream in = new BufferedInputStream(inputData.getInputStream()); if (enableCompression) { compressGen = new PGPCompressedDataGenerator(input.getCompressionAlgorithm()); @@ -491,9 +498,7 @@ public class PgpSignEncryptOperation extends BaseOperation { literalGen.close(); } else { - pOut = null; - // TODO: Is this log right? - log.add(LogType.MSG_PSE_CLEARSIGN_ONLY, indent); + throw new AssertionError("cannot clearsign in non-ascii armored text, this is a bug!"); } if (enableSignature) { @@ -513,6 +518,10 @@ public class PgpSignEncryptOperation extends BaseOperation { } } + opTime = System.currentTimeMillis() -startTime; + Log.d(Constants.TAG, "sign/encrypt time taken: " + String.format("%.2f", + opTime / 1000.0) + "s"); + // closing outputs // NOTE: closing needs to be done in the correct order! if (encryptionOut != null) { @@ -556,6 +565,7 @@ public class PgpSignEncryptOperation extends BaseOperation { log.add(LogType.MSG_PSE_OK, indent); PgpSignEncryptResult result = new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_OK, log); + result.mOperationTime = opTime; if (detachedByteOut != null) { try { detachedByteOut.flush(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java new file mode 100644 index 000000000..ed5566bc1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignatureChecker.java @@ -0,0 +1,344 @@ +package org.sufficientlysecure.keychain.pgp; + + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.SignatureException; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPOnePassSignature; +import org.spongycastle.openpgp.PGPOnePassSignatureList; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureList; +import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; + + +/** This class is used to track the state of a single signature verification. + * + * + */ +class PgpSignatureChecker { + + OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); + + private CanonicalizedPublicKey signingKey; + + private int signatureIndex; + PGPOnePassSignature onePassSignature; + PGPSignature signature; + + ProviderHelper mProviderHelper; + + PgpSignatureChecker(ProviderHelper providerHelper) { + mProviderHelper = providerHelper; + } + + boolean initializeSignature(Object dataChunk, OperationLog log, int indent) throws PGPException { + + if (!(dataChunk instanceof PGPSignatureList)) { + return false; + } + + PGPSignatureList sigList = (PGPSignatureList) dataChunk; + findAvailableSignature(sigList); + + if (signingKey != null) { + + // key found in our database! + signatureResultBuilder.initValid(signingKey); + + JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = + new JcaPGPContentVerifierBuilderProvider() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + signature.init(contentVerifierBuilderProvider, signingKey.getPublicKey()); + checkKeySecurity(log, indent); + + + } else if (!sigList.isEmpty()) { + + signatureResultBuilder.setSignatureAvailable(true); + signatureResultBuilder.setKnownKey(false); + signatureResultBuilder.setKeyId(sigList.get(0).getKeyID()); + + } + + return true; + + } + + boolean initializeOnePassSignature(Object dataChunk, OperationLog log, int indent) throws PGPException { + + if (!(dataChunk instanceof PGPOnePassSignatureList)) { + return false; + } + + log.add(LogType.MSG_DC_CLEAR_SIGNATURE, indent + 1); + + PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk; + findAvailableSignature(sigList); + + if (signingKey != null) { + + // key found in our database! + signatureResultBuilder.initValid(signingKey); + + JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = + new JcaPGPContentVerifierBuilderProvider() + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME); + onePassSignature.init(contentVerifierBuilderProvider, signingKey.getPublicKey()); + + checkKeySecurity(log, indent); + + } else if (!sigList.isEmpty()) { + + signatureResultBuilder.setSignatureAvailable(true); + signatureResultBuilder.setKnownKey(false); + signatureResultBuilder.setKeyId(sigList.get(0).getKeyID()); + + } + + return true; + + } + + private void checkKeySecurity(OperationLog log, int indent) { + // TODO: checks on signingRing ? + if (!PgpSecurityConstants.isSecureKey(signingKey)) { + log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1); + signatureResultBuilder.setInsecure(true); + } + } + + public boolean isInitialized() { + return signingKey != null; + } + + private void findAvailableSignature(PGPOnePassSignatureList sigList) { + // go through all signatures (should be just one), make sure we have + // the key and it matches the one we’re looking for + for (int i = 0; i < sigList.size(); ++i) { + try { + long sigKeyId = sigList.get(i).getKeyID(); + CanonicalizedPublicKeyRing signingRing = mProviderHelper.getCanonicalizedPublicKeyRing( + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId) + ); + CanonicalizedPublicKey keyCandidate = signingRing.getPublicKey(sigKeyId); + if ( ! keyCandidate.canSign()) { + continue; + } + signatureIndex = i; + signingKey = keyCandidate; + onePassSignature = sigList.get(i); + return; + } catch (ProviderHelper.NotFoundException e) { + Log.d(Constants.TAG, "key not found, trying next signature..."); + } + } + } + + public void findAvailableSignature(PGPSignatureList sigList) { + // go through all signatures (should be just one), make sure we have + // the key and it matches the one we’re looking for + for (int i = 0; i < sigList.size(); ++i) { + try { + long sigKeyId = sigList.get(i).getKeyID(); + CanonicalizedPublicKeyRing signingRing = mProviderHelper.getCanonicalizedPublicKeyRing( + KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId) + ); + CanonicalizedPublicKey keyCandidate = signingRing.getPublicKey(sigKeyId); + if ( ! keyCandidate.canSign()) { + continue; + } + signatureIndex = i; + signingKey = keyCandidate; + signature = sigList.get(i); + return; + } catch (ProviderHelper.NotFoundException e) { + Log.d(Constants.TAG, "key not found, trying next signature..."); + } + } + } + + public void updateSignatureWithCleartext(byte[] clearText) throws IOException, SignatureException { + + InputStream sigIn = new BufferedInputStream(new ByteArrayInputStream(clearText)); + + ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); + + int lookAhead = readInputLine(outputBuffer, sigIn); + + processLine(signature, outputBuffer.toByteArray()); + + while (lookAhead != -1) { + lookAhead = readInputLine(outputBuffer, lookAhead, sigIn); + + signature.update((byte) '\r'); + signature.update((byte) '\n'); + + processLine(signature, outputBuffer.toByteArray()); + } + + } + + public void updateSignatureData(byte[] buf, int off, int len) { + if (signature != null) { + signature.update(buf, off, len); + } else if (onePassSignature != null) { + onePassSignature.update(buf, off, len); + } + } + + void verifySignature(OperationLog log, int indent) throws PGPException { + + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); + + // Verify signature + boolean validSignature = signature.verify(); + if (validSignature) { + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); + } else { + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); + } + + // check for insecure hash algorithms + if (!PgpSecurityConstants.isSecureHashAlgorithm(signature.getHashAlgorithm())) { + log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); + signatureResultBuilder.setInsecure(true); + } + + signatureResultBuilder.setValidSignature(validSignature); + + } + + boolean verifySignatureOnePass(Object o, OperationLog log, int indent) throws PGPException { + + if (!(o instanceof PGPSignatureList)) { + log.add(LogType.MSG_DC_ERROR_NO_SIGNATURE, indent); + return false; + } + PGPSignatureList signatureList = (PGPSignatureList) o; + if (signatureList.size() <= signatureIndex) { + log.add(LogType.MSG_DC_ERROR_NO_SIGNATURE, indent); + return false; + } + + // PGPOnePassSignature and PGPSignature packets are "bracketed", + // so we need to take the last-minus-index'th element here + PGPSignature messageSignature = signatureList.get(signatureList.size() - 1 - signatureIndex); + + // Verify signature + boolean validSignature = onePassSignature.verify(messageSignature); + if (validSignature) { + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_OK, indent + 1); + } else { + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_BAD, indent + 1); + } + + // check for insecure hash algorithms + if (!PgpSecurityConstants.isSecureHashAlgorithm(onePassSignature.getHashAlgorithm())) { + log.add(LogType.MSG_DC_INSECURE_HASH_ALGO, indent + 1); + signatureResultBuilder.setInsecure(true); + } + + signatureResultBuilder.setValidSignature(validSignature); + + return true; + + } + + public byte[] getSigningFingerprint() { + return signingKey.getFingerprint(); + } + + public OpenPgpSignatureResult getSignatureResult() { + return signatureResultBuilder.build(); + } + + /** + * Mostly taken from ClearSignedFileProcessor in Bouncy Castle + */ + + private static void processLine(PGPSignature sig, byte[] line) + throws SignatureException { + int length = getLengthWithoutWhiteSpace(line); + if (length > 0) { + sig.update(line, 0, length); + } + } + + private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn) + throws IOException { + bOut.reset(); + + int lookAhead = -1; + int ch; + + while ((ch = fIn.read()) >= 0) { + bOut.write(ch); + if (ch == '\r' || ch == '\n') { + lookAhead = readPastEOL(bOut, ch, fIn); + break; + } + } + + return lookAhead; + } + + private static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn) + throws IOException { + bOut.reset(); + + int ch = lookAhead; + + do { + bOut.write(ch); + if (ch == '\r' || ch == '\n') { + lookAhead = readPastEOL(bOut, ch, fIn); + break; + } + } while ((ch = fIn.read()) >= 0); + + if (ch < 0) { + lookAhead = -1; + } + + return lookAhead; + } + + private static int readPastEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn) + throws IOException { + int lookAhead = fIn.read(); + + if (lastCh == '\r' && lookAhead == '\n') { + bOut.write(lookAhead); + lookAhead = fIn.read(); + } + + return lookAhead; + } + + private static int getLengthWithoutWhiteSpace(byte[] line) { + int end = line.length - 1; + + while (end >= 0 && isWhiteSpace(line[end])) { + end--; + } + + return end + 1; + } + + private static boolean isWhiteSpace(byte b) { + return b == '\r' || b == '\n' || b == '\t' || b == ' '; + } + +} 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 ca98882d8..c967a5abc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java @@ -18,6 +18,23 @@ package org.sufficientlysecure.keychain.pgp; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Comparator; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeSet; + import org.spongycastle.bcpg.ArmoredOutputStream; import org.spongycastle.bcpg.PublicKeyAlgorithmTags; import org.spongycastle.bcpg.SignatureSubpacketTags; @@ -43,23 +60,6 @@ import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Utf8Util; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Comparator; -import java.util.Date; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.TimeZone; -import java.util.TreeSet; - /** Wrapper around PGPKeyRing class, to be constructed from bytes. * * This class and its relatives UncachedPublicKey and UncachedSecretKey are @@ -78,12 +78,13 @@ import java.util.TreeSet; * @see org.sufficientlysecure.keychain.pgp.UncachedSecretKey * */ -@SuppressWarnings("unchecked") -public class UncachedKeyRing implements Serializable { +public class UncachedKeyRing { final PGPKeyRing mRing; final boolean mIsSecret; + private static final int CANONICALIZE_MAX_USER_IDS = 100; + UncachedKeyRing(PGPKeyRing ring) { mRing = ring; mIsSecret = ring instanceof PGPSecretKeyRing; @@ -457,11 +458,15 @@ public class UncachedKeyRing implements Serializable { // check for duplicate user ids if (processedUserIds.contains(userId)) { - log.add(LogType.MSG_KC_UID_DUP, - indent, userId); + log.add(LogType.MSG_KC_UID_DUP, indent, userId); // strip out the first found user id with this name modified = PGPPublicKey.removeCertification(modified, rawUserId); } + if (processedUserIds.size() > CANONICALIZE_MAX_USER_IDS) { + log.add(LogType.MSG_KC_UID_TOO_MANY, indent, userId); + // strip out the user id + modified = PGPPublicKey.removeCertification(modified, rawUserId); + } processedUserIds.add(userId); PGPSignature selfCert = null; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java index d7fb738fc..752c13007 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -54,7 +54,7 @@ import java.io.IOException; */ public class KeychainDatabase extends SQLiteOpenHelper { private static final String DATABASE_NAME = "openkeychain.db"; - private static final int DATABASE_VERSION = 12; + private static final int DATABASE_VERSION = 14; static Boolean apgHack = false; private Context mContext; @@ -79,7 +79,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { private static final String CREATE_KEYRINGS_SECRET = "CREATE TABLE IF NOT EXISTS keyrings_secret (" + KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY," - + KeyRingsColumns.KEY_RING_DATA + " BLOB," + + KeyRingsColumns.KEY_RING_DATA + " BLOB, " + "FOREIGN KEY(" + KeyRingsColumns.MASTER_KEY_ID + ") " + "REFERENCES keyrings_public(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" + ")"; @@ -220,6 +220,13 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL(CREATE_API_APPS); db.execSQL(CREATE_API_APPS_ACCOUNTS); db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); + + db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");"); + db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", " + + UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");"); + db.execSQL("CREATE INDEX verified_certs ON certs (" + + CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");"); + } @Override @@ -291,11 +298,14 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3"); case 12: db.execSQL(CREATE_UPDATE_KEYS); - if (oldVersion == 10) { - // no consolidate if we are updating from 10, we're just here for - // the api_accounts fix and the new update keys table - return; - } + case 13: + // do nothing here, just consolidate + case 14: + db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");"); + db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", " + + UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");"); + db.execSQL("CREATE INDEX verified_certs ON certs (" + + CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");"); } @@ -306,6 +316,17 @@ public class KeychainDatabase extends SQLiteOpenHelper { mContext.getApplicationContext().startActivity(consolidateIntent); } + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Downgrade is ok for the debug version, makes it easier to work with branches + if (Constants.DEBUG) { + return; + } + // NOTE: downgrading the database is explicitly not allowed to prevent + // someone from exploiting old bugs to export the database + throw new RuntimeException("Downgrading the database is not allowed!"); + } + /** This method tries to import data from a provided database. * * The sole assumptions made on this db are that there is a key_rings table diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index d722fa9e7..104343074 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -303,14 +303,14 @@ public class KeychainProvider extends ContentProvider { projectionMap.put(KeyRings.FINGERPRINT, Tables.KEYS + "." + Keys.FINGERPRINT); projectionMap.put(KeyRings.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID); projectionMap.put(KeyRings.HAS_DUPLICATE_USER_ID, - "(SELECT COUNT (*) FROM " + Tables.USER_PACKETS + " AS dups" + "(EXISTS (SELECT * FROM " + Tables.USER_PACKETS + " AS dups" + " WHERE dups." + UserPackets.MASTER_KEY_ID + " != " + Tables.KEYS + "." + Keys.MASTER_KEY_ID + " AND dups." + UserPackets.RANK + " = 0" + " AND dups." + UserPackets.USER_ID + " = "+ Tables.USER_PACKETS + "." + UserPackets.USER_ID - + ") AS " + KeyRings.HAS_DUPLICATE_USER_ID); - projectionMap.put(KeyRings.VERIFIED, KeyRings.VERIFIED); + + ")) AS " + KeyRings.HAS_DUPLICATE_USER_ID); + projectionMap.put(KeyRings.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED); projectionMap.put(KeyRings.PUBKEY_DATA, Tables.KEY_RINGS_PUBLIC + "." + KeyRingData.KEY_RING_DATA + " AS " + KeyRings.PUBKEY_DATA); @@ -319,10 +319,8 @@ public class KeychainProvider extends ContentProvider { + " AS " + KeyRings.PRIVKEY_DATA); projectionMap.put(KeyRings.HAS_SECRET, Tables.KEYS + "." + KeyRings.HAS_SECRET); projectionMap.put(KeyRings.HAS_ANY_SECRET, - "(EXISTS (SELECT * FROM " + Tables.KEY_RINGS_SECRET - + " WHERE " + Tables.KEY_RINGS_SECRET + "." + KeyRingData.MASTER_KEY_ID - + " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID - + ")) AS " + KeyRings.HAS_ANY_SECRET); + "(" + Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + " IS NOT NULL)" + + " AS " + KeyRings.HAS_ANY_SECRET); projectionMap.put(KeyRings.HAS_ENCRYPT, "kE." + Keys.KEY_ID + " AS " + KeyRings.HAS_ENCRYPT); projectionMap.put(KeyRings.HAS_SIGN, @@ -363,7 +361,7 @@ public class KeychainProvider extends ContentProvider { + " = " + Tables.KEY_RINGS_PUBLIC + "." + KeyRingData.MASTER_KEY_ID + ")" : "") - + (plist.contains(KeyRings.PRIVKEY_DATA) ? + + (plist.contains(KeyRings.PRIVKEY_DATA) || plist.contains(KeyRings.HAS_ANY_SECRET) ? " LEFT JOIN " + Tables.KEY_RINGS_SECRET + " ON (" + Tables.KEYS + "." + Keys.MASTER_KEY_ID + " = " @@ -712,20 +710,45 @@ public class KeychainProvider extends ContentProvider { } SQLiteDatabase db = getDb().getReadableDatabase(); + Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, having, 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)); + if (Constants.DEBUG && Constants.DEBUG_LOG_DB_QUERIES) { - Log.d(Constants.TAG, - "Query: " - + qb.buildQuery(projection, selection, selectionArgs, null, null, - orderBy, null)); Log.d(Constants.TAG, "Cursor: " + DatabaseUtils.dumpCursorToString(cursor)); } + if (Constants.DEBUG && Constants.DEBUG_EXPLAIN_QUERIES) { + String rawQuery = qb.buildQuery(projection, selection, groupBy, having, orderBy, null); + Cursor explainCursor = db.rawQuery("EXPLAIN QUERY PLAN " + rawQuery, selectionArgs); + + // this is a debugging feature, we can be a little careless + explainCursor.moveToFirst(); + + StringBuilder line = new StringBuilder(); + for (int i = 0; i < explainCursor.getColumnCount(); i++) { + line.append(explainCursor.getColumnName(i)).append(", "); + } + Log.d(Constants.TAG, line.toString()); + + while (!explainCursor.isAfterLast()) { + line = new StringBuilder(); + for (int i = 0; i < explainCursor.getColumnCount(); i++) { + line.append(explainCursor.getString(i)).append(", "); + } + Log.d(Constants.TAG, line.toString()); + explainCursor.moveToNext(); + } + + explainCursor.close(); + } + return cursor; } 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 a6823d3ac..375775ff1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -62,7 +62,6 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; import org.sufficientlysecure.keychain.remote.AccountSettings; import org.sufficientlysecure.keychain.remote.AppSettings; @@ -968,7 +967,7 @@ public class ProviderHelper { // If we have an expected fingerprint, make sure it matches if (expectedFingerprint != null) { - if (!canPublicRing.containsSubkey(expectedFingerprint)) { + if (!canPublicRing.containsBoundSubkey(expectedFingerprint)) { log(LogType.MSG_IP_FINGERPRINT_ERROR); return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); } else { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java index 67f2c36bc..68963d595 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryStorageProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/TemporaryFileProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2014-2015 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 @@ -29,7 +29,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.os.ParcelFileDescriptor; -import android.provider.OpenableColumns; +import android.provider.MediaStore; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.util.DatabaseUtil; @@ -39,18 +39,20 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * TemporaryStorageProvider stores decrypted files inside the app's cache directory previously to * sharing them with other applications. - * + * <p/> * Security: * - It is writable by OpenKeychain only (see Manifest), but exported for reading files * - It uses UUIDs as identifiers which makes predicting files from outside impossible * - Querying a number of files is not allowed, only querying single files * -> You can only open a file if you know the Uri containing the precise UUID, this Uri is only * revealed when the user shares a decrypted file with another app. - * + * <p/> * Why is support lib's FileProvider not used? * Because granting Uri permissions temporarily does not work correctly. See * - https://code.google.com/p/android/issues/detail?id=76683 @@ -59,30 +61,36 @@ import java.util.UUID; * - http://stackoverflow.com/q/18249007 * - Comments at http://www.blogc.at/2014/03/23/share-private-files-with-other-apps-fileprovider/ */ -public class TemporaryStorageProvider extends ContentProvider { +public class TemporaryFileProvider extends ContentProvider { private static final String DB_NAME = "tempstorage.db"; private static final String TABLE_FILES = "files"; - private static final String COLUMN_ID = "id"; - private static final String COLUMN_NAME = "name"; - private static final String COLUMN_TIME = "time"; - private static final String COLUMN_TYPE = "mimetype"; - public static final String AUTHORITY = Constants.TEMPSTORAGE_AUTHORITY; + public static final String AUTHORITY = Constants.TEMP_FILE_PROVIDER_AUTHORITY; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); private static final int DB_VERSION = 3; - private static File cacheDir; + interface TemporaryFileColumns { + String COLUMN_UUID = "id"; + String COLUMN_NAME = "name"; + String COLUMN_TIME = "time"; + String COLUMN_TYPE = "mimetype"; + } + + private static final String TEMP_FILES_DIR = "temp"; + private static File tempFilesDir; + + private static Pattern UUID_PATTERN = Pattern.compile("[a-fA-F0-9-]+"); public static Uri createFile(Context context, String targetName, String mimeType) { ContentValues contentValues = new ContentValues(); - contentValues.put(COLUMN_NAME, targetName); - contentValues.put(COLUMN_TYPE, mimeType); + contentValues.put(TemporaryFileColumns.COLUMN_NAME, targetName); + contentValues.put(TemporaryFileColumns.COLUMN_TYPE, mimeType); return context.getContentResolver().insert(CONTENT_URI, contentValues); } public static Uri createFile(Context context, String targetName) { ContentValues contentValues = new ContentValues(); - contentValues.put(COLUMN_NAME, targetName); + contentValues.put(TemporaryFileColumns.COLUMN_NAME, targetName); return context.getContentResolver().insert(CONTENT_URI, contentValues); } @@ -93,13 +101,16 @@ public class TemporaryStorageProvider extends ContentProvider { public static int setMimeType(Context context, Uri uri, String mimetype) { ContentValues values = new ContentValues(); - values.put(COLUMN_TYPE, mimetype); + values.put(TemporaryFileColumns.COLUMN_TYPE, mimetype); return context.getContentResolver().update(uri, values, null, null); } public static int cleanUp(Context context) { - return context.getContentResolver().delete(CONTENT_URI, COLUMN_TIME + "< ?", - new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)}); + return context.getContentResolver().delete( + CONTENT_URI, + TemporaryFileColumns.COLUMN_TIME + "< ?", + new String[]{Long.toString(System.currentTimeMillis() - Constants.TEMPFILE_TTL)} + ); } private class TemporaryStorageDatabase extends SQLiteOpenHelper { @@ -111,10 +122,10 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" + - COLUMN_ID + " TEXT PRIMARY KEY, " + - COLUMN_NAME + " TEXT, " + - COLUMN_TYPE + " TEXT, " + - COLUMN_TIME + " INTEGER" + + TemporaryFileColumns.COLUMN_UUID + " TEXT PRIMARY KEY, " + + TemporaryFileColumns.COLUMN_NAME + " TEXT, " + + TemporaryFileColumns.COLUMN_TYPE + " TEXT, " + + TemporaryFileColumns.COLUMN_TIME + " INTEGER" + ");"); } @@ -126,12 +137,12 @@ public class TemporaryStorageProvider extends ContentProvider { case 1: db.execSQL("DROP TABLE IF EXISTS files"); db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" + - COLUMN_ID + " TEXT PRIMARY KEY, " + - COLUMN_NAME + " TEXT, " + - COLUMN_TIME + " INTEGER" + + TemporaryFileColumns.COLUMN_UUID + " TEXT PRIMARY KEY, " + + TemporaryFileColumns.COLUMN_NAME + " TEXT, " + + TemporaryFileColumns.COLUMN_TIME + " INTEGER" + ");"); case 2: - db.execSQL("ALTER TABLE files ADD COLUMN " + COLUMN_TYPE + " TEXT"); + db.execSQL("ALTER TABLE files ADD COLUMN " + TemporaryFileColumns.COLUMN_TYPE + " TEXT"); } } } @@ -147,14 +158,19 @@ public class TemporaryStorageProvider extends ContentProvider { } private File getFile(String id) { - return new File(cacheDir, "temp/" + id); + Matcher m = UUID_PATTERN.matcher(id); + if (!m.matches()) { + throw new SecurityException("Can only open temporary files with UUIDs!"); + } + + return new File(tempFilesDir, id); } @Override public boolean onCreate() { db = new TemporaryStorageDatabase(getContext()); - cacheDir = getContext().getCacheDir(); - return new File(cacheDir, "temp").mkdirs(); + tempFilesDir = new File(getContext().getCacheDir(), TEMP_FILES_DIR); + return tempFilesDir.mkdirs(); } @Override @@ -163,27 +179,24 @@ public class TemporaryStorageProvider extends ContentProvider { throw new SecurityException("Listing temporary files is not allowed, only querying single files."); } - Log.d(Constants.TAG, "being asked for file " + uri); - File file; try { file = getFile(uri); - if (file.exists()) { - Log.e(Constants.TAG, "already exists"); - } } catch (FileNotFoundException e) { Log.e(Constants.TAG, "file not found!"); return null; } - Cursor fileName = db.getReadableDatabase().query(TABLE_FILES, new String[]{COLUMN_NAME}, COLUMN_ID + "=?", + Cursor fileName = db.getReadableDatabase().query(TABLE_FILES, + new String[]{TemporaryFileColumns.COLUMN_NAME}, + TemporaryFileColumns.COLUMN_UUID + "=?", new String[]{uri.getLastPathSegment()}, null, null, null); if (fileName != null) { if (fileName.moveToNext()) { MatrixCursor cursor = new MatrixCursor(new String[]{ - OpenableColumns.DISPLAY_NAME, - OpenableColumns.SIZE, - "_data" + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.SIZE, + MediaStore.MediaColumns.DATA, }); cursor.newRow() .add(fileName.getString(0)) @@ -200,7 +213,8 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public String getType(Uri uri) { Cursor cursor = db.getReadableDatabase().query(TABLE_FILES, - new String[]{COLUMN_TYPE}, COLUMN_ID + "=?", + new String[]{TemporaryFileColumns.COLUMN_TYPE}, + TemporaryFileColumns.COLUMN_UUID + "=?", new String[]{uri.getLastPathSegment()}, null, null, null); if (cursor != null) { try { @@ -227,11 +241,11 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public Uri insert(Uri uri, ContentValues values) { - if (!values.containsKey(COLUMN_TIME)) { - values.put(COLUMN_TIME, System.currentTimeMillis()); + if (!values.containsKey(TemporaryFileColumns.COLUMN_TIME)) { + values.put(TemporaryFileColumns.COLUMN_TIME, System.currentTimeMillis()); } String uuid = UUID.randomUUID().toString(); - values.put(COLUMN_ID, uuid); + values.put(TemporaryFileColumns.COLUMN_UUID, uuid); int insert = (int) db.getWritableDatabase().insert(TABLE_FILES, null, values); if (insert == -1) { Log.e(Constants.TAG, "Insert failed!"); @@ -252,10 +266,10 @@ public class TemporaryStorageProvider extends ContentProvider { return 0; } - selection = DatabaseUtil.concatenateWhere(selection, COLUMN_ID + "=?"); + selection = DatabaseUtil.concatenateWhere(selection, TemporaryFileColumns.COLUMN_UUID + "=?"); selectionArgs = DatabaseUtil.appendSelectionArgs(selectionArgs, new String[]{uri.getLastPathSegment()}); - Cursor files = db.getReadableDatabase().query(TABLE_FILES, new String[]{COLUMN_ID}, selection, + Cursor files = db.getReadableDatabase().query(TABLE_FILES, new String[]{TemporaryFileColumns.COLUMN_UUID}, selection, selectionArgs, null, null, null); if (files != null) { while (files.moveToNext()) { @@ -269,19 +283,18 @@ public class TemporaryStorageProvider extends ContentProvider { @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - if (values.size() != 1 || !values.containsKey(COLUMN_TYPE)) { + if (values.size() != 1 || !values.containsKey(TemporaryFileColumns.COLUMN_TYPE)) { throw new UnsupportedOperationException("Update supported only for type field!"); } if (selection != null || selectionArgs != null) { throw new UnsupportedOperationException("Update supported only for plain uri!"); } return db.getWritableDatabase().update(TABLE_FILES, values, - COLUMN_ID + " = ?", new String[]{uri.getLastPathSegment()}); + TemporaryFileColumns.COLUMN_UUID + " = ?", new String[]{uri.getLastPathSegment()}); } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { - Log.d(Constants.TAG, "openFile"); return openFileHelper(uri, mode); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java index e7709e58e..b810f5a6a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java @@ -25,6 +25,7 @@ import android.net.Uri; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.Parcelable; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; @@ -34,13 +35,15 @@ import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.OpenPgpMetadata; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.util.OpenPgpApi; +import org.spongycastle.bcpg.ArmoredOutputStream; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel; import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; -import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; -import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; +import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; +import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; @@ -238,10 +241,8 @@ public class OpenPgpService extends RemoteService { PendingIntent.FLAG_CANCEL_CURRENT); } - private Intent signImpl(Intent data, ParcelFileDescriptor input, - ParcelFileDescriptor output, boolean cleartextSign) { - InputStream is = null; - OutputStream os = null; + private Intent signImpl(Intent data, InputStream inputStream, + OutputStream outputStream, boolean cleartextSign) { try { boolean asciiArmor = cleartextSign || data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); @@ -277,14 +278,13 @@ public class OpenPgpService extends RemoteService { } // Get Input- and OutputStream from ParcelFileDescriptor - is = new ParcelFileDescriptor.AutoCloseInputStream(input); - if (cleartextSign) { + if (!cleartextSign) { // output stream only needed for cleartext signatures, // detached signatures are returned as extra - os = new ParcelFileDescriptor.AutoCloseOutputStream(output); + outputStream = null; } - long inputLength = is.available(); - InputData inputData = new InputData(is, inputLength); + long inputLength = inputStream.available(); + InputData inputData = new InputData(inputStream, inputLength); CryptoInputParcel inputParcel = CryptoInputParcelCacheService.getCryptoInputParcel(this, data); if (inputParcel == null) { @@ -298,7 +298,7 @@ public class OpenPgpService extends RemoteService { // execute PGP operation! PgpSignEncryptOperation pse = new PgpSignEncryptOperation(this, new ProviderHelper(getContext()), null); - PgpSignEncryptResult pgpResult = pse.execute(pseInput, inputParcel, inputData, os); + PgpSignEncryptResult pgpResult = pse.execute(pseInput, inputParcel, inputData, outputStream); if (pgpResult.isPending()) { @@ -330,28 +330,11 @@ public class OpenPgpService extends RemoteService { new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); return result; - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException when closing InputStream", e); - } - } - if (os != null) { - try { - os.close(); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException when closing OutputStream", e); - } - } } } - private Intent encryptAndSignImpl(Intent data, ParcelFileDescriptor input, - ParcelFileDescriptor output, boolean sign) { - InputStream is = null; - OutputStream os = null; + private Intent encryptAndSignImpl(Intent data, InputStream inputStream, + OutputStream outputStream, boolean sign) { try { boolean asciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); String originalFilename = data.getStringExtra(OpenPgpApi.EXTRA_ORIGINAL_FILENAME); @@ -383,13 +366,9 @@ public class OpenPgpService extends RemoteService { } } - // build InputData and write into OutputStream - // Get Input- and OutputStream from ParcelFileDescriptor - is = new ParcelFileDescriptor.AutoCloseInputStream(input); - os = new ParcelFileDescriptor.AutoCloseOutputStream(output); - - long inputLength = is.available(); - InputData inputData = new InputData(is, inputLength, originalFilename); + // TODO this is not correct! + long inputLength = inputStream.available(); + InputData inputData = new InputData(inputStream, inputLength, originalFilename); PgpSignEncryptInputParcel pseInput = new PgpSignEncryptInputParcel(); pseInput.setEnableAsciiArmorOutput(asciiArmor) @@ -455,7 +434,7 @@ public class OpenPgpService extends RemoteService { PgpSignEncryptOperation op = new PgpSignEncryptOperation(this, new ProviderHelper(getContext()), null); // execute PGP operation! - PgpSignEncryptResult pgpResult = op.execute(pseInput, inputParcel, inputData, os); + PgpSignEncryptResult pgpResult = op.execute(pseInput, inputParcel, inputData, outputStream); if (pgpResult.isPending()) { RequiredInputParcel requiredInput = pgpResult.getRequiredInputParcel(); @@ -482,37 +461,15 @@ public class OpenPgpService extends RemoteService { new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); return result; - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException when closing InputStream", e); - } - } - if (os != null) { - try { - os.close(); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException when closing OutputStream", e); - } - } } } - private Intent decryptAndVerifyImpl(Intent data, ParcelFileDescriptor inputDescriptor, - ParcelFileDescriptor output, boolean decryptMetadataOnly) { - InputStream inputStream = null; - OutputStream outputStream = null; + private Intent decryptAndVerifyImpl(Intent data, InputStream inputStream, + OutputStream outputStream, boolean decryptMetadataOnly) { try { - // Get Input- and OutputStream from ParcelFileDescriptor - inputStream = new ParcelFileDescriptor.AutoCloseInputStream(inputDescriptor); - // output is optional, e.g., for verifying detached signatures - if (decryptMetadataOnly || output == null) { + if (decryptMetadataOnly) { outputStream = null; - } else { - outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(output); } String currentPkg = getCurrentCallingPackage(); @@ -538,6 +495,7 @@ public class OpenPgpService extends RemoteService { PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(this, mProviderHelper, null); + // TODO this is not correct! long inputLength = inputStream.available(); InputData inputData = new InputData(inputStream, inputLength); @@ -604,6 +562,7 @@ public class OpenPgpService extends RemoteService { // case RESULT_NOT_ENCRYPTED, but a signature, fallback to deprecated signatureOnly variable if (decryptionResult.getResult() == OpenPgpDecryptionResult.RESULT_NOT_ENCRYPTED && signatureResult.getResult() != OpenPgpSignatureResult.RESULT_NO_SIGNATURE) { + // noinspection deprecation, TODO signatureResult.setSignatureOnly(true); } @@ -665,35 +624,40 @@ public class OpenPgpService extends RemoteService { result.putExtra(OpenPgpApi.RESULT_ERROR, new OpenPgpError(OpenPgpError.GENERIC_ERROR, e.getMessage())); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); return result; - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException when closing InputStream", e); - } - } - if (outputStream != null) { - try { - outputStream.close(); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException when closing OutputStream", e); - } - } } } - private Intent getKeyImpl(Intent data) { + private Intent getKeyImpl(Intent data, OutputStream outputStream) { try { long masterKeyId = data.getLongExtra(OpenPgpApi.EXTRA_KEY_ID, 0); try { // try to find key, throws NotFoundException if not in db! - mProviderHelper.getCanonicalizedPublicKeyRing(masterKeyId); + CanonicalizedPublicKeyRing keyRing = + mProviderHelper.getCanonicalizedPublicKeyRing(masterKeyId); Intent result = new Intent(); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + + boolean requestedKeyData = outputStream != null; + if (requestedKeyData) { + boolean requestAsciiArmor = data.getBooleanExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, false); + + try { + if (requestAsciiArmor) { + outputStream = new ArmoredOutputStream(outputStream); + } + keyRing.encode(outputStream); + } finally { + try { + outputStream.close(); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException when closing OutputStream", e); + } + } + } + // also return PendingIntent that opens the key view activity result.putExtra(OpenPgpApi.RESULT_INTENT, getShowKeyPendingIntent(masterKeyId)); @@ -821,7 +785,7 @@ public class OpenPgpService extends RemoteService { OpenPgpError error = new OpenPgpError (OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n" + "used API version: " + data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) + "\n" - + "supported API versions: " + supportedVersions.toString()); + + "supported API versions: " + supportedVersions); result.putExtra(OpenPgpApi.RESULT_ERROR, error); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); return result; @@ -850,68 +814,88 @@ public class OpenPgpService extends RemoteService { return mBinder; } + @Nullable + protected Intent executeInternal( + @NonNull Intent data, + @Nullable ParcelFileDescriptor input, + @Nullable ParcelFileDescriptor output) { - protected Intent executeInternal(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) { - try { - Intent errorResult = checkRequirements(data); - if (errorResult != null) { - return errorResult; - } + OutputStream outputStream = + (output != null) ? new ParcelFileDescriptor.AutoCloseOutputStream(output) : null; + InputStream inputStream = + (input != null) ? new ParcelFileDescriptor.AutoCloseInputStream(input) : null; - String action = data.getAction(); - switch (action) { - case OpenPgpApi.ACTION_CLEARTEXT_SIGN: { - return signImpl(data, input, output, true); - } - case OpenPgpApi.ACTION_SIGN: { - // DEPRECATED: same as ACTION_CLEARTEXT_SIGN - Log.w(Constants.TAG, "You are using a deprecated API call, please use ACTION_CLEARTEXT_SIGN instead of ACTION_SIGN!"); - return signImpl(data, input, output, true); - } - case OpenPgpApi.ACTION_DETACHED_SIGN: { - return signImpl(data, input, output, false); - } - case OpenPgpApi.ACTION_ENCRYPT: { - return encryptAndSignImpl(data, input, output, false); - } - case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: { - return encryptAndSignImpl(data, input, output, true); - } - case OpenPgpApi.ACTION_DECRYPT_VERIFY: { - return decryptAndVerifyImpl(data, input, output, false); - } - case OpenPgpApi.ACTION_DECRYPT_METADATA: { - return decryptAndVerifyImpl(data, input, output, true); - } - case OpenPgpApi.ACTION_GET_SIGN_KEY_ID: { - return getSignKeyIdImpl(data); - } - case OpenPgpApi.ACTION_GET_KEY_IDS: { - return getKeyIdsImpl(data); - } - case OpenPgpApi.ACTION_GET_KEY: { - return getKeyImpl(data); - } - default: { - return null; - } - } + try { + return executeInternalWithStreams(data, inputStream, outputStream); } finally { // always close input and output file descriptors even in error cases - if (input != null) { + if (inputStream != null) { try { - input.close(); + inputStream.close(); } catch (IOException e) { Log.e(Constants.TAG, "IOException when closing input ParcelFileDescriptor", e); } } - if (output != null) { + if (outputStream != null) { try { - output.close(); + outputStream.close(); } catch (IOException e) { Log.e(Constants.TAG, "IOException when closing output ParcelFileDescriptor", e); } } } } + + @Nullable + protected Intent executeInternalWithStreams( + @NonNull Intent data, + @Nullable InputStream inputStream, + @Nullable OutputStream outputStream) { + + Intent errorResult = checkRequirements(data); + if (errorResult != null) { + return errorResult; + } + + String action = data.getAction(); + switch (action) { + case OpenPgpApi.ACTION_CLEARTEXT_SIGN: { + return signImpl(data, inputStream, outputStream, true); + } + case OpenPgpApi.ACTION_SIGN: { + // DEPRECATED: same as ACTION_CLEARTEXT_SIGN + Log.w(Constants.TAG, "You are using a deprecated API call, please use ACTION_CLEARTEXT_SIGN instead of ACTION_SIGN!"); + return signImpl(data, inputStream, outputStream, true); + } + case OpenPgpApi.ACTION_DETACHED_SIGN: { + return signImpl(data, inputStream, outputStream, false); + } + case OpenPgpApi.ACTION_ENCRYPT: { + return encryptAndSignImpl(data, inputStream, outputStream, false); + } + case OpenPgpApi.ACTION_SIGN_AND_ENCRYPT: { + return encryptAndSignImpl(data, inputStream, outputStream, true); + } + case OpenPgpApi.ACTION_DECRYPT_VERIFY: { + return decryptAndVerifyImpl(data, inputStream, outputStream, false); + } + case OpenPgpApi.ACTION_DECRYPT_METADATA: { + return decryptAndVerifyImpl(data, inputStream, outputStream, true); + } + case OpenPgpApi.ACTION_GET_SIGN_KEY_ID: { + return getSignKeyIdImpl(data); + } + case OpenPgpApi.ACTION_GET_KEY_IDS: { + return getKeyIdsImpl(data); + } + case OpenPgpApi.ACTION_GET_KEY: { + return getKeyImpl(data, outputStream); + } + default: { + return null; + } + } + + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/BackupKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/BackupKeyringParcel.java new file mode 100644 index 000000000..3d9626934 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/BackupKeyringParcel.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@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.service; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import org.sufficientlysecure.keychain.util.Passphrase; + + +public class BackupKeyringParcel implements Parcelable { + public Uri mCanonicalizedPublicKeyringUri; + public Passphrase mSymmetricPassphrase; + + public boolean mExportSecret; + public long mMasterKeyIds[]; + public Uri mOutputUri; + + public BackupKeyringParcel(Passphrase symmetricPassphrase, + long[] masterKeyIds, boolean exportSecret, Uri outputUri) { + mSymmetricPassphrase = symmetricPassphrase; + mMasterKeyIds = masterKeyIds; + mExportSecret = exportSecret; + mOutputUri = outputUri; + } + + protected BackupKeyringParcel(Parcel in) { + mCanonicalizedPublicKeyringUri = (Uri) in.readValue(Uri.class.getClassLoader()); + mExportSecret = in.readByte() != 0x00; + mOutputUri = (Uri) in.readValue(Uri.class.getClassLoader()); + mMasterKeyIds = in.createLongArray(); + mSymmetricPassphrase = in.readParcelable(getClass().getClassLoader()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeValue(mCanonicalizedPublicKeyringUri); + dest.writeByte((byte) (mExportSecret ? 0x01 : 0x00)); + dest.writeValue(mOutputUri); + dest.writeLongArray(mMasterKeyIds); + dest.writeParcelable(mSymmetricPassphrase, 0); + } + + public static final Parcelable.Creator<BackupKeyringParcel> CREATOR = new Parcelable.Creator<BackupKeyringParcel>() { + @Override + public BackupKeyringParcel createFromParcel(Parcel in) { + return new BackupKeyringParcel(in); + } + + @Override + public BackupKeyringParcel[] newArray(int size) { + return new BackupKeyringParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/BenchmarkInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/BenchmarkInputParcel.java new file mode 100644 index 000000000..cfbdfefff --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/BenchmarkInputParcel.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@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.service; + +import android.os.Parcel; +import android.os.Parcelable; + + +public class BenchmarkInputParcel implements Parcelable { + + public BenchmarkInputParcel() { + } + + protected BenchmarkInputParcel(Parcel in) { + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + } + + public static final Creator<BenchmarkInputParcel> CREATOR = new Creator<BenchmarkInputParcel>() { + @Override + public BenchmarkInputParcel createFromParcel(Parcel in) { + return new BenchmarkInputParcel(in); + } + + @Override + public BenchmarkInputParcel[] newArray(int size) { + return new BenchmarkInputParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java deleted file mode 100644 index 24c002bbd..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ExportKeyringParcel.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> - * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> - * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@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.service; - -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; - -import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; - -public class ExportKeyringParcel implements Parcelable { - public String mKeyserver; - public Uri mCanonicalizedPublicKeyringUri; - public UncachedKeyRing mUncachedKeyRing; - - public boolean mExportSecret; - public long mMasterKeyIds[]; - public String mOutputFile; - public Uri mOutputUri; - public ExportType mExportType; - - public enum ExportType { - UPLOAD_KEYSERVER, - EXPORT_FILE, - EXPORT_URI - } - - public ExportKeyringParcel(String keyserver, Uri keyringUri) { - mExportType = ExportType.UPLOAD_KEYSERVER; - mKeyserver = keyserver; - mCanonicalizedPublicKeyringUri = keyringUri; - } - - public ExportKeyringParcel(String keyserver, UncachedKeyRing uncachedKeyRing) { - mExportType = ExportType.UPLOAD_KEYSERVER; - mKeyserver = keyserver; - mUncachedKeyRing = uncachedKeyRing; - } - - public ExportKeyringParcel(long[] masterKeyIds, boolean exportSecret, String outputFile) { - mExportType = ExportType.EXPORT_FILE; - mMasterKeyIds = masterKeyIds; - mExportSecret = exportSecret; - mOutputFile = outputFile; - } - - @SuppressWarnings("unused") // TODO: is it used? - public ExportKeyringParcel(long[] masterKeyIds, boolean exportSecret, Uri outputUri) { - mExportType = ExportType.EXPORT_URI; - mMasterKeyIds = masterKeyIds; - mExportSecret = exportSecret; - mOutputUri = outputUri; - } - - protected ExportKeyringParcel(Parcel in) { - mKeyserver = in.readString(); - mCanonicalizedPublicKeyringUri = (Uri) in.readValue(Uri.class.getClassLoader()); - mUncachedKeyRing = (UncachedKeyRing) in.readValue(UncachedKeyRing.class.getClassLoader()); - mExportSecret = in.readByte() != 0x00; - mOutputFile = in.readString(); - mOutputUri = (Uri) in.readValue(Uri.class.getClassLoader()); - mExportType = (ExportType) in.readValue(ExportType.class.getClassLoader()); - mMasterKeyIds = in.createLongArray(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mKeyserver); - dest.writeValue(mCanonicalizedPublicKeyringUri); - dest.writeValue(mUncachedKeyRing); - dest.writeByte((byte) (mExportSecret ? 0x01 : 0x00)); - dest.writeString(mOutputFile); - dest.writeValue(mOutputUri); - dest.writeValue(mExportType); - dest.writeLongArray(mMasterKeyIds); - } - - public static final Parcelable.Creator<ExportKeyringParcel> CREATOR = new Parcelable.Creator<ExportKeyringParcel>() { - @Override - public ExportKeyringParcel createFromParcel(Parcel in) { - return new ExportKeyringParcel(in); - } - - @Override - public ExportKeyringParcel[] newArray(int size) { - return new ExportKeyringParcel[size]; - } - }; -}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java index c7ac92eef..cf51e3b55 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java @@ -29,17 +29,19 @@ import android.os.RemoteException; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.operations.BaseOperation; +import org.sufficientlysecure.keychain.operations.BenchmarkOperation; import org.sufficientlysecure.keychain.operations.CertifyOperation; import org.sufficientlysecure.keychain.operations.ConsolidateOperation; import org.sufficientlysecure.keychain.operations.DeleteOperation; import org.sufficientlysecure.keychain.operations.EditKeyOperation; -import org.sufficientlysecure.keychain.operations.ExportOperation; +import org.sufficientlysecure.keychain.operations.BackupOperation; import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation; import org.sufficientlysecure.keychain.operations.InputDataOperation; import org.sufficientlysecure.keychain.operations.PromoteKeyOperation; import org.sufficientlysecure.keychain.operations.RevokeOperation; import org.sufficientlysecure.keychain.operations.SignEncryptOperation; +import org.sufficientlysecure.keychain.operations.UploadOperation; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; @@ -124,14 +126,18 @@ public class KeychainService extends Service implements Progressable { op = new PromoteKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); } else if (inputParcel instanceof ImportKeyringParcel) { op = new ImportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); - } else if (inputParcel instanceof ExportKeyringParcel) { - op = new ExportOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); + } else if (inputParcel instanceof BackupKeyringParcel) { + op = new BackupOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); + } else if (inputParcel instanceof UploadKeyringParcel) { + op = new UploadOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); } else if (inputParcel instanceof ConsolidateInputParcel) { op = new ConsolidateOperation(outerThis, new ProviderHelper(outerThis), outerThis); } else if (inputParcel instanceof KeybaseVerificationParcel) { op = new KeybaseVerificationOperation(outerThis, new ProviderHelper(outerThis), outerThis); } else if (inputParcel instanceof InputDataParcel) { op = new InputDataOperation(outerThis, new ProviderHelper(outerThis), outerThis); + } else if (inputParcel instanceof BenchmarkInputParcel) { + op = new BenchmarkOperation(outerThis, new ProviderHelper(outerThis), outerThis); } else { throw new AssertionError("Unrecognized input parcel in KeychainService!"); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java index 8aebae7aa..122eb6cf4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java @@ -59,11 +59,12 @@ public class KeyserverSyncAdapterService extends Service { // time since last update after which a key should be updated again, in s public static final long KEY_UPDATE_LIMIT = Constants.DEBUG_KEYSERVER_SYNC ? 1 : TimeUnit.DAYS.toSeconds(7); - // time by which a sync is postponed in case of a + // time by which a sync is postponed in case screen is on public static final long SYNC_POSTPONE_TIME = Constants.DEBUG_KEYSERVER_SYNC ? 30 * 1000 : TimeUnit.MINUTES.toMillis(5); // Time taken by Orbot before a new circuit is created - public static final int ORBOT_CIRCUIT_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(10); + public static final int ORBOT_CIRCUIT_TIMEOUT_SECONDS = + Constants.DEBUG_KEYSERVER_SYNC ? 2 : (int) TimeUnit.MINUTES.toSeconds(10); private static final String ACTION_IGNORE_TOR = "ignore_tor"; @@ -77,10 +78,14 @@ public class KeyserverSyncAdapterService extends Service { @Override public int onStartCommand(final Intent intent, int flags, final int startId) { + if (intent == null || intent.getAction() == null) { + // introduced due to https://github.com/open-keychain/open-keychain/issues/1573 + return START_NOT_STICKY; // we can't act on this Intent and don't want it redelivered + } switch (intent.getAction()) { case ACTION_CANCEL: { mCancelled.set(true); - break; + return START_NOT_STICKY; } // the reason for the separation betweyeen SYNC_NOW and UPDATE_ALL is so that starting // the sync directly from the notification is possible while the screen is on with @@ -92,44 +97,47 @@ public class KeyserverSyncAdapterService extends Service { Constants.PROVIDER_AUTHORITY, new Bundle() ); - break; + return START_NOT_STICKY; } case ACTION_UPDATE_ALL: { // does not check for screen on/off - asyncKeyUpdate(this, new CryptoInputParcel()); - break; + asyncKeyUpdate(this, new CryptoInputParcel(), startId); + // we depend on handleUpdateResult to call stopSelf when it is no longer necessary + // for the intent to be redelivered + return START_REDELIVER_INTENT; } case ACTION_IGNORE_TOR: { NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT); - asyncKeyUpdate(this, new CryptoInputParcel(ParcelableProxy.getForNoProxy())); - break; + asyncKeyUpdate(this, new CryptoInputParcel(ParcelableProxy.getForNoProxy()), + startId); + // we depend on handleUpdateResult to call stopSelf when it is no longer necessary + // for the intent to be redelivered + return START_REDELIVER_INTENT; } case ACTION_START_ORBOT: { - NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + NotificationManager manager = (NotificationManager) + getSystemService(NOTIFICATION_SERVICE); manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT); + Intent startOrbot = new Intent(this, OrbotRequiredDialogActivity.class); startOrbot.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_START_ORBOT, true); + Messenger messenger = new Messenger( new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case OrbotRequiredDialogActivity.MESSAGE_ORBOT_STARTED: { - asyncKeyUpdate(KeyserverSyncAdapterService.this, - new CryptoInputParcel()); - break; - } - case OrbotRequiredDialogActivity.MESSAGE_ORBOT_IGNORE: { - asyncKeyUpdate(KeyserverSyncAdapterService.this, - new CryptoInputParcel( - ParcelableProxy.getForNoProxy())); + startServiceWithUpdateAll(); break; } + case OrbotRequiredDialogActivity.MESSAGE_ORBOT_IGNORE: case OrbotRequiredDialogActivity.MESSAGE_DIALOG_CANCEL: { - // just stop service - stopSelf(); + // not possible since we proceed to Orbot's Activity + // directly, by starting OrbotRequiredDialogActivity with + // EXTRA_START_ORBOT set to true break; } } @@ -138,13 +146,17 @@ public class KeyserverSyncAdapterService extends Service { ); startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_MESSENGER, messenger); startActivity(startOrbot); - break; + // since we return START_NOT_STICKY, we also postpone the sync as a backup in case + // the service is killed before OrbotRequiredDialogActivity can get back to us + postponeSync(); + // if use START_REDELIVER_INTENT, we might annoy the user by repeatedly starting the + // Orbot Activity when our service is killed and restarted + return START_NOT_STICKY; } case ACTION_DISMISS_NOTIFICATION: { NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); manager.cancel(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT); - stopSelf(startId); - break; + return START_NOT_STICKY; } } return START_NOT_STICKY; @@ -167,10 +179,7 @@ public class KeyserverSyncAdapterService extends Service { boolean isScreenOn = pm.isScreenOn(); if (!isScreenOn) { - Intent serviceIntent = new Intent(KeyserverSyncAdapterService.this, - KeyserverSyncAdapterService.class); - serviceIntent.setAction(ACTION_UPDATE_ALL); - startService(serviceIntent); + startServiceWithUpdateAll(); } else { postponeSync(); } @@ -188,16 +197,24 @@ public class KeyserverSyncAdapterService extends Service { return new KeyserverSyncAdapter().getSyncAdapterBinder(); } - private void handleUpdateResult(ImportKeyResult result) { + /** + * Since we're returning START_REDELIVER_INTENT in onStartCommand, we need to remember to call + * stopSelf(int) to prevent the Intent from being redelivered if our work is already done + * + * @param result result of keyserver sync + * @param startId startId provided to the onStartCommand call which resulted in this sync + */ + private void handleUpdateResult(ImportKeyResult result, final int startId) { if (result.isPending()) { + Log.d(Constants.TAG, "Orbot required for sync but not running, attempting to start"); // result is pending due to Orbot not being started // try to start it silently, if disabled show notifications new OrbotHelper.SilentStartManager() { @Override protected void onOrbotStarted() { // retry the update - asyncKeyUpdate(KeyserverSyncAdapterService.this, - new CryptoInputParcel()); + startServiceWithUpdateAll(); + stopSelf(startId); // startServiceWithUpdateAll will deliver a new Intent } @Override @@ -207,16 +224,24 @@ public class KeyserverSyncAdapterService extends Service { (NotificationManager) getSystemService(NOTIFICATION_SERVICE); manager.notify(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT, getOrbotNoification(KeyserverSyncAdapterService.this)); + // further action on user interaction with notification, intent should not be + // redelivered, therefore: + stopSelf(startId); } }.startOrbotAndListen(this, false); + // if we're killed before we get a response from Orbot, we need the intent to be + // redelivered, so no stopSelf(int) here } else if (isUpdateCancelled()) { Log.d(Constants.TAG, "Keyserver sync cancelled, postponing by" + SYNC_POSTPONE_TIME + "ms"); postponeSync(); + // postponeSync creates a new intent, so we don't need this to be redelivered + stopSelf(startId); } else { Log.d(Constants.TAG, "Keyserver sync completed: Updated: " + result.mUpdatedKeys + " Failed: " + result.mBadKeys); - stopSelf(); + // key sync completed successfully, we can stop + stopSelf(startId); } } @@ -234,12 +259,12 @@ public class KeyserverSyncAdapterService extends Service { } private void asyncKeyUpdate(final Context context, - final CryptoInputParcel cryptoInputParcel) { + final CryptoInputParcel cryptoInputParcel, final int startId) { new Thread(new Runnable() { @Override public void run() { ImportKeyResult result = updateKeysFromKeyserver(context, cryptoInputParcel); - handleUpdateResult(result); + handleUpdateResult(result, startId); } }).start(); } @@ -278,7 +303,6 @@ public class KeyserverSyncAdapterService extends Service { ); } - /** * will perform a staggered update of user's keys using delays to ensure new Tor circuits, as * performed by parcimonie. Relevant issue and method at: @@ -290,17 +314,31 @@ public class KeyserverSyncAdapterService extends Service { CryptoInputParcel cryptoInputParcel) { Log.d(Constants.TAG, "Starting staggered update"); // final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7); + // we are limiting our randomness to ORBOT_CIRCUIT_TIMEOUT_SECONDS for now final int WEEK_IN_SECONDS = 0; + ImportOperation.KeyImportAccumulator accumulator = new ImportOperation.KeyImportAccumulator(keyList.size(), null); + + // so that the first key can be updated without waiting. This is so that there isn't a + // large gap between a "Start Orbot" notification and the next key update + boolean first = true; + for (ParcelableKeyRing keyRing : keyList) { int waitTime; int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size())); - if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT) { + if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT_SECONDS) { waitTime = staggeredTime; } else { - waitTime = ORBOT_CIRCUIT_TIMEOUT + new Random().nextInt(ORBOT_CIRCUIT_TIMEOUT); + waitTime = ORBOT_CIRCUIT_TIMEOUT_SECONDS + + new Random().nextInt(1 + ORBOT_CIRCUIT_TIMEOUT_SECONDS); + } + + if (first) { + waitTime = 0; + first = false; } + Log.d(Constants.TAG, "Updating key with fingerprint " + keyRing.mExpectedFingerprint + " with a wait time of " + waitTime + "s"); try { @@ -362,13 +400,15 @@ public class KeyserverSyncAdapterService extends Service { ); ArrayList<Long> ignoreMasterKeyIds = new ArrayList<>(); - while (updatedKeysCursor.moveToNext()) { + while (updatedKeysCursor != null && updatedKeysCursor.moveToNext()) { long masterKeyId = updatedKeysCursor.getLong(INDEX_UPDATED_KEYS_MASTER_KEY_ID); Log.d(Constants.TAG, "Keyserver sync: Ignoring {" + masterKeyId + "} last updated at {" + updatedKeysCursor.getLong(INDEX_LAST_UPDATED) + "}s"); ignoreMasterKeyIds.add(masterKeyId); } - updatedKeysCursor.close(); + if (updatedKeysCursor != null) { + updatedKeysCursor.close(); + } // 2. Make a list of public keys which should be updated final int INDEX_MASTER_KEY_ID = 0; @@ -413,7 +453,7 @@ public class KeyserverSyncAdapterService extends Service { /** * will cancel an update already in progress. We send an Intent to cancel it instead of simply - * modifying a static variable sync the service is running in a process that is different from + * modifying a static variable since the service is running in a process that is different from * the default application process where the UI code runs. * * @param context used to send an Intent to the service requesting cancellation. @@ -491,6 +531,12 @@ public class KeyserverSyncAdapterService extends Service { } } + private void startServiceWithUpdateAll() { + Intent serviceIntent = new Intent(this, KeyserverSyncAdapterService.class); + serviceIntent.setAction(ACTION_UPDATE_ALL); + this.startService(serviceIntent); + } + // from de.azapps.mirakel.helper.Helpers from https://github.com/MirakelX/mirakel-android private Bitmap getBitmap(int resId, Context context) { int mLargeIconWidth = (int) context.getResources().getDimension( diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java index 5d04317b3..df29a388f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java @@ -256,14 +256,6 @@ public class PassphraseCacheService extends Service { SecretKeyType keyType = keyRing.getSecretKeyType(subKeyId); switch (keyType) { - case DIVERT_TO_CARD: - if (Preferences.getPreferences(this).useDefaultYubiKeyPin()) { - Log.d(Constants.TAG, "PassphraseCacheService: Using default YubiKey PIN: 123456"); - return new Passphrase("123456"); // default YubiKey PIN, see http://www.yubico.com/2012/12/yubikey-neo-openpgp/ - } else { - Log.d(Constants.TAG, "PassphraseCacheService: NOT using default YubiKey PIN"); - break; - } case PASSPHRASE_EMPTY: return new Passphrase(""); case UNAVAILABLE: diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java new file mode 100644 index 000000000..0a14f3dc6 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/UploadKeyringParcel.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * Copyright (C) 2015 Adithya Abraham Philip <adithyaphilip@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.service; + + +import android.os.Parcel; +import android.os.Parcelable; + + +public class UploadKeyringParcel implements Parcelable { + public String mKeyserver; + + public final Long mMasterKeyId; + public final byte[] mUncachedKeyringBytes; + + public UploadKeyringParcel(String keyserver, long masterKeyId) { + mKeyserver = keyserver; + mMasterKeyId = masterKeyId; + mUncachedKeyringBytes = null; + } + + public UploadKeyringParcel(String keyserver, byte[] uncachedKeyringBytes) { + mKeyserver = keyserver; + mMasterKeyId = null; + mUncachedKeyringBytes = uncachedKeyringBytes; + } + + protected UploadKeyringParcel(Parcel in) { + mKeyserver = in.readString(); + mMasterKeyId = in.readInt() != 0 ? in.readLong() : null; + mUncachedKeyringBytes = in.createByteArray(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mKeyserver); + if (mMasterKeyId != null) { + dest.writeInt(1); + dest.writeLong(mMasterKeyId); + } else { + dest.writeInt(0); + } + dest.writeByteArray(mUncachedKeyringBytes); + } + + public static final Creator<UploadKeyringParcel> CREATOR = new Creator<UploadKeyringParcel>() { + @Override + public UploadKeyringParcel createFromParcel(Parcel in) { + return new UploadKeyringParcel(in); + } + + @Override + public UploadKeyringParcel[] newArray(int size) { + return new UploadKeyringParcel[size]; + } + }; +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java index 0d8569fe6..849418905 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/CryptoInputParcel.java @@ -23,6 +23,7 @@ import android.os.Parcelable; import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Passphrase; +import java.net.Proxy; import java.nio.ByteBuffer; import java.util.Date; import java.util.HashMap; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java index e4dac3227..1f99836ea 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java @@ -14,8 +14,8 @@ import java.util.Date; public class RequiredInputParcel implements Parcelable { public enum RequiredInputType { - PASSPHRASE, PASSPHRASE_SYMMETRIC, NFC_SIGN, NFC_DECRYPT, NFC_MOVE_KEY_TO_CARD, ENABLE_ORBOT, - UPLOAD_FAIL_RETRY + PASSPHRASE, PASSPHRASE_SYMMETRIC, BACKUP_CODE, NFC_SIGN, NFC_DECRYPT, + NFC_MOVE_KEY_TO_CARD, NFC_RESET_CARD, ENABLE_ORBOT, UPLOAD_FAIL_RETRY, } public Date mSignatureTime; @@ -100,6 +100,11 @@ public class RequiredInputParcel implements Parcelable { new byte[][] { encryptedSessionKey }, null, null, masterKeyId, subKeyId); } + public static RequiredInputParcel createNfcReset() { + return new RequiredInputParcel(RequiredInputType.NFC_RESET_CARD, + null, null, null, null, null); + } + public static RequiredInputParcel createRequiredSignPassphrase( long masterKeyId, long subKeyId, Date signatureTime) { return new RequiredInputParcel(RequiredInputType.PASSPHRASE, @@ -117,6 +122,11 @@ public class RequiredInputParcel implements Parcelable { null, null, null, null, null); } + public static RequiredInputParcel createRequiredBackupCode() { + return new RequiredInputParcel(RequiredInputType.BACKUP_CODE, + null, null, null, null, null); + } + public static RequiredInputParcel createRequiredPassphrase( RequiredInputParcel req) { return new RequiredInputParcel(RequiredInputType.PASSPHRASE, diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java new file mode 100644 index 000000000..ff120c9b5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupActivity.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui; + + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.view.MenuItem; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; + + +public class BackupActivity extends BaseActivity { + + public static final String EXTRA_MASTER_KEY_IDS = "master_key_ids"; + public static final String EXTRA_SECRET = "export_secret"; + + @Override + protected void initLayout() { + setContentView(R.layout.backup_activity); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // noinspection ConstantConditions, we know this activity has an action bar + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + if (savedInstanceState == null) { + Intent intent = getIntent(); + boolean exportSecret = intent.getBooleanExtra(EXTRA_SECRET, false); + long[] masterKeyIds = intent.getLongArrayExtra(EXTRA_MASTER_KEY_IDS); + + Fragment frag = BackupCodeFragment.newInstance(masterKeyIds, exportSecret); + + FragmentManager fragMan = getSupportFragmentManager(); + fragMan.beginTransaction() + .setCustomAnimations(0, 0) + .replace(R.id.content_frame, frag) + .commit(); + } + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + FragmentManager fragMan = getSupportFragmentManager(); + // pop from back stack, or if nothing was on there finish activity + if ( ! fragMan.popBackStackImmediate()) { + finish(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java new file mode 100644 index 000000000..8afdb5f94 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupCodeFragment.java @@ -0,0 +1,543 @@ +/* + * Copyright (C) 2015 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.ui; + +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentManager.OnBackStackChangedListener; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.widget.EditText; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; +import org.sufficientlysecure.keychain.service.BackupKeyringParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator; +import org.sufficientlysecure.keychain.util.FileHelper; +import org.sufficientlysecure.keychain.util.Passphrase; + +public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringParcel, ExportResult> + implements OnBackStackChangedListener { + + public static final String ARG_BACKUP_CODE = "backup_code"; + public static final String BACK_STACK_INPUT = "state_display"; + public static final String ARG_EXPORT_SECRET = "export_secret"; + public static final String ARG_MASTER_KEY_IDS = "master_key_ids"; + + public static final String ARG_CURRENT_STATE = "current_state"; + + public static final int REQUEST_SAVE = 1; + public static final String ARG_BACK_STACK = "back_stack"; + + // argument variables + private boolean mExportSecret; + private long[] mMasterKeyIds; + String mBackupCode; + + private EditText[] mCodeEditText; + private ToolableViewAnimator mStatusAnimator, mTitleAnimator, mCodeFieldsAnimator; + + private Integer mBackStackLevel; + + private Uri mCachedBackupUri; + private boolean mShareNotSave; + + public static BackupCodeFragment newInstance(long[] masterKeyIds, boolean exportSecret) { + BackupCodeFragment frag = new BackupCodeFragment(); + + Bundle args = new Bundle(); + args.putString(ARG_BACKUP_CODE, generateRandomCode()); + args.putLongArray(ARG_MASTER_KEY_IDS, masterKeyIds); + args.putBoolean(ARG_EXPORT_SECRET, exportSecret); + frag.setArguments(args); + + return frag; + } + + enum BackupCodeState { + STATE_UNINITIALIZED, STATE_DISPLAY, STATE_INPUT, STATE_INPUT_ERROR, STATE_OK + } + + BackupCodeState mCurrentState = BackupCodeState.STATE_UNINITIALIZED; + + void switchState(BackupCodeState state, boolean animate) { + + switch (state) { + case STATE_UNINITIALIZED: + throw new AssertionError("can't switch to uninitialized state, this is a bug!"); + + case STATE_DISPLAY: + mTitleAnimator.setDisplayedChild(0, animate); + mStatusAnimator.setDisplayedChild(0, animate); + mCodeFieldsAnimator.setDisplayedChild(0, animate); + + break; + + case STATE_INPUT: + mTitleAnimator.setDisplayedChild(1, animate); + mStatusAnimator.setDisplayedChild(1, animate); + mCodeFieldsAnimator.setDisplayedChild(1, animate); + + for (EditText editText : mCodeEditText) { + editText.setText(""); + } + + pushBackStackEntry(); + + break; + + case STATE_INPUT_ERROR: { + mTitleAnimator.setDisplayedChild(1, false); + mStatusAnimator.setDisplayedChild(2, animate); + mCodeFieldsAnimator.setDisplayedChild(1, false); + + hideKeyboard(); + + if (animate) { + @ColorInt int black = mCodeEditText[0].getCurrentTextColor(); + @ColorInt int red = getResources().getColor(R.color.android_red_dark); + animateFlashText(mCodeEditText, black, red, false); + } + + break; + } + + case STATE_OK: { + mTitleAnimator.setDisplayedChild(2, animate); + mStatusAnimator.setDisplayedChild(3, animate); + mCodeFieldsAnimator.setDisplayedChild(1, false); + + hideKeyboard(); + + for (EditText editText : mCodeEditText) { + editText.setEnabled(false); + } + + @ColorInt int green = getResources().getColor(R.color.android_green_dark); + if (animate) { + @ColorInt int black = mCodeEditText[0].getCurrentTextColor(); + animateFlashText(mCodeEditText, black, green, true); + } else { + for (TextView textView : mCodeEditText) { + textView.setTextColor(green); + } + } + + popBackStackNoAction(); + + break; + } + + } + + mCurrentState = state; + + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.backup_code_fragment, container, false); + + Bundle args = getArguments(); + mBackupCode = args.getString(ARG_BACKUP_CODE); + mMasterKeyIds = args.getLongArray(ARG_MASTER_KEY_IDS); + mExportSecret = args.getBoolean(ARG_EXPORT_SECRET); + + mCodeEditText = new EditText[4]; + mCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1); + mCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2); + mCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3); + mCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4); + + { + TextView[] codeDisplayText = new TextView[4]; + 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); + + // set backup code in code TextViews + char[] backupCode = mBackupCode.toCharArray(); + for (int i = 0; i < codeDisplayText.length; i++) { + codeDisplayText[i].setText(backupCode, i * 7, 6); + } + + // 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); + + mStatusAnimator = (ToolableViewAnimator) view.findViewById(R.id.status_animator); + mTitleAnimator = (ToolableViewAnimator) view.findViewById(R.id.title_animator); + mCodeFieldsAnimator = (ToolableViewAnimator) view.findViewById(R.id.code_animator); + + View backupInput = view.findViewById(R.id.button_backup_input); + backupInput.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + switchState(BackupCodeState.STATE_INPUT, true); + } + }); + + view.findViewById(R.id.button_backup_save).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mShareNotSave = false; + startBackup(); + } + }); + + view.findViewById(R.id.button_backup_share).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mShareNotSave = true; + startBackup(); + } + }); + + view.findViewById(R.id.button_backup_back).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + FragmentManager fragMan = getFragmentManager(); + if (fragMan != null) { + fragMan.popBackStack(); + } + } + }); + + return view; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (savedInstanceState != null) { + int savedBackStack = savedInstanceState.getInt(ARG_BACK_STACK); + if (savedBackStack >= 0) { + mBackStackLevel = savedBackStack; + // unchecked use, we know that this one is available in onViewCreated + getFragmentManager().addOnBackStackChangedListener(this); + } + BackupCodeState savedState = BackupCodeState.values()[savedInstanceState.getInt(ARG_CURRENT_STATE)]; + switchState(savedState, false); + } else if (mCurrentState == BackupCodeState.STATE_UNINITIALIZED) { + switchState(BackupCodeState.STATE_DISPLAY, true); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(ARG_CURRENT_STATE, mCurrentState.ordinal()); + outState.putInt(ARG_BACK_STACK, mBackStackLevel == null ? -1 : mBackStackLevel); + } + + private void setupEditTextSuccessListener(final EditText[] backupCodes) { + for (EditText backupCode : backupCodes) { + + backupCode.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + if (s.length() > 6) { + throw new AssertionError("max length of each field is 6!"); + } + + boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT + || mCurrentState == BackupCodeState.STATE_INPUT_ERROR; + boolean partIsComplete = s.length() == 6; + if (!inInputState || !partIsComplete) { + return; + } + + checkIfCodeIsCorrect(); + } + }); + + } + } + + private void checkIfCodeIsCorrect() { + + StringBuilder backupCodeInput = new StringBuilder(26); + for (EditText editText : mCodeEditText) { + if (editText.getText().length() < 6) { + return; + } + backupCodeInput.append(editText.getText()); + backupCodeInput.append('-'); + } + backupCodeInput.deleteCharAt(backupCodeInput.length() - 1); + + // if they don't match, do nothing + if (backupCodeInput.toString().equals(mBackupCode)) { + switchState(BackupCodeState.STATE_OK, true); + return; + } + + switchState(BackupCodeState.STATE_INPUT_ERROR, true); + + } + + private static void animateFlashText( + final TextView[] textViews, int color1, int color2, boolean staySecondColor) { + + ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), color1, color2); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + for (TextView textView : textViews) { + textView.setTextColor((Integer) animator.getAnimatedValue()); + } + } + }); + anim.setRepeatMode(ValueAnimator.REVERSE); + anim.setRepeatCount(staySecondColor ? 4 : 5); + anim.setDuration(180); + anim.setInterpolator(new AccelerateInterpolator()); + anim.start(); + + } + + 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) == 6; + + if (inserting && cursorAtEnd) { + backupCodes[next].requestFocus(); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + } + } + + private void pushBackStackEntry() { + if (mBackStackLevel != null) { + return; + } + FragmentManager fragMan = getFragmentManager(); + mBackStackLevel = fragMan.getBackStackEntryCount(); + fragMan.beginTransaction().addToBackStack(BACK_STACK_INPUT).commit(); + fragMan.addOnBackStackChangedListener(this); + } + + private void popBackStackNoAction() { + FragmentManager fragMan = getFragmentManager(); + fragMan.removeOnBackStackChangedListener(this); + fragMan.popBackStackImmediate(BACK_STACK_INPUT, FragmentManager.POP_BACK_STACK_INCLUSIVE); + mBackStackLevel = null; + } + + @Override + public void onBackStackChanged() { + FragmentManager fragMan = getFragmentManager(); + if (mBackStackLevel != null && fragMan.getBackStackEntryCount() == mBackStackLevel) { + fragMan.removeOnBackStackChangedListener(this); + switchState(BackupCodeState.STATE_DISPLAY, true); + mBackStackLevel = null; + } + } + + private void startBackup() { + + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); + String filename = Constants.FILE_ENCRYPTED_BACKUP_PREFIX + date + + (mExportSecret ? Constants.FILE_EXTENSION_ENCRYPTED_BACKUP_SECRET + : Constants.FILE_EXTENSION_ENCRYPTED_BACKUP_PUBLIC); + + if (mCachedBackupUri == null) { + mCachedBackupUri = TemporaryFileProvider.createFile(activity, filename, + Constants.MIME_TYPE_ENCRYPTED_ALTERNATE); + cryptoOperation(); + return; + } + + if (mShareNotSave) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(Constants.MIME_TYPE_ENCRYPTED_ALTERNATE); + intent.putExtra(Intent.EXTRA_STREAM, mCachedBackupUri); + startActivity(intent); + } else { + saveFile(filename, false); + } + + } + + private void saveFile(final String filename, boolean overwrite) { + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + + // for kitkat and above, we have the document api + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + FileHelper.saveDocument(this, filename, Constants.MIME_TYPE_ENCRYPTED_ALTERNATE, REQUEST_SAVE); + return; + } + + File file = new File(Constants.Path.APP_DIR, filename); + + if (!overwrite && file.exists()) { + Notify.create(activity, R.string.snack_backup_exists, Style.WARN, new ActionListener() { + @Override + public void onAction() { + saveFile(filename, true); + } + }, R.string.snack_btn_overwrite).show(); + return; + } + + try { + FileHelper.copyUriData(activity, mCachedBackupUri, Uri.fromFile(file)); + Notify.create(activity, R.string.snack_backup_saved_dir, Style.OK).show(); + } catch (IOException e) { + Notify.create(activity, R.string.snack_backup_error_saving, Style.ERROR).show(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode != REQUEST_SAVE) { + super.onActivityResult(requestCode, resultCode, data); + return; + } + + if (resultCode != FragmentActivity.RESULT_OK) { + return; + } + + FragmentActivity activity = getActivity(); + if (activity == null) { + return; + } + try { + Uri outputUri = data.getData(); + FileHelper.copyUriData(activity, mCachedBackupUri, outputUri); + Notify.create(activity, R.string.snack_backup_saved, Style.OK).show(); + } catch (IOException e) { + Notify.create(activity, R.string.snack_backup_error_saving, Style.ERROR).show(); + } + } + + @Nullable + @Override + public BackupKeyringParcel createOperationInput() { + return new BackupKeyringParcel(new Passphrase(mBackupCode), mMasterKeyIds, mExportSecret, mCachedBackupUri); + } + + @Override + public void onCryptoOperationSuccess(ExportResult result) { + startBackup(); + } + + @Override + public void onCryptoOperationError(ExportResult result) { + result.createNotify(getActivity()).show(); + mCachedBackupUri = null; + } + + @Override + public void onCryptoOperationCancelled() { + mCachedBackupUri = null; + } + + @NonNull + private static String generateRandomCode() { + + Random r = new SecureRandom(); + + // simple generation of a 20 character backup code + StringBuilder code = new StringBuilder(28); + for (int i = 0; i < 24; i++) { + if (i == 6 || i == 12 || i == 18) { + code.append('-'); + } + code.append((char) ('A' + r.nextInt(26))); + } + + return code.toString(); + + } + + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java index a3ea8ad9a..25601d655 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java @@ -17,17 +17,13 @@ package org.sufficientlysecure.keychain.ui; - -import java.io.File; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; -import java.util.Locale; import android.app.Activity; import android.content.ContentResolver; import android.content.Intent; import android.database.Cursor; +import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; @@ -37,41 +33,29 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.util.ExportHelper; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.util.FileHelper; -public class BackupFragment extends Fragment { +public class BackupRestoreFragment extends Fragment { // This ids for multiple key export. private ArrayList<Long> mIdsForRepeatAskPassphrase; // This index for remembering the number of master key. private int mIndex; - static final int REQUEST_REPEAT_PASSPHRASE = 1; - private ExportHelper mExportHelper; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - // we won't get attached to a non-fragment activity, so the cast should be safe - mExportHelper = new ExportHelper((FragmentActivity) activity); - } - - @Override - public void onDetach() { - super.onDetach(); - mExportHelper = null; - } + private static final int REQUEST_REPEAT_PASSPHRASE = 0x00007002; + private static final int REQUEST_CODE_INPUT = 0x00007003; @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.backup_fragment, container, false); + View view = inflater.inflate(R.layout.backup_restore_fragment, container, false); View backupAll = view.findViewById(R.id.backup_all); View backupPublicKeys = view.findViewById(R.id.backup_public_keys); + final View restore = view.findViewById(R.id.restore); backupAll.setOnClickListener(new View.OnClickListener() { @Override @@ -87,6 +71,13 @@ public class BackupFragment extends Fragment { } }); + restore.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + restore(); + } + }); + return view; } @@ -101,12 +92,12 @@ public class BackupFragment extends Fragment { return; } - new AsyncTask<ContentResolver,Void,ArrayList<Long>>() { + new AsyncTask<ContentResolver, Void, ArrayList<Long>>() { @Override protected ArrayList<Long> doInBackground(ContentResolver... resolver) { ArrayList<Long> askPassphraseIds = new ArrayList<>(); Cursor cursor = resolver[0].query( - KeyRings.buildUnifiedKeyRingsUri(), new String[] { + KeyRings.buildUnifiedKeyRingsUri(), new String[]{ KeyRings.MASTER_KEY_ID, KeyRings.HAS_SECRET, }, KeyRings.HAS_SECRET + " != 0", null, null); @@ -156,7 +147,6 @@ public class BackupFragment extends Fragment { } }.execute(activity.getContentResolver()); - } private void startPassphraseActivity() { @@ -173,28 +163,53 @@ public class BackupFragment extends Fragment { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_REPEAT_PASSPHRASE) { - if (resultCode != Activity.RESULT_OK) { - return; + switch (requestCode) { + case REQUEST_REPEAT_PASSPHRASE: { + if (resultCode != Activity.RESULT_OK) { + return; + } + if (mIndex < mIdsForRepeatAskPassphrase.size()) { + startPassphraseActivity(); + return; + } + + startBackup(true); + + break; } - if (mIndex < mIdsForRepeatAskPassphrase.size()) { - startPassphraseActivity(); - return; + + case REQUEST_CODE_INPUT: { + if (resultCode != Activity.RESULT_OK || data == null) { + return; + } + + Uri uri = data.getData(); + if (uri == null) { + Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR).show(); + return; + } + + Intent intent = new Intent(getActivity(), DecryptActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(uri); + startActivity(intent); + break; } - startBackup(true); + default: { + super.onActivityResult(requestCode, resultCode, data); + } } } private void startBackup(boolean exportSecret) { - File filename; - String date = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); - if (exportSecret) { - filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".asc"); - } else { - filename = new File(Constants.Path.APP_DIR, "keys_" + date + ".pub.asc"); - } - mExportHelper.showExportKeysDialog(null, filename, exportSecret); + Intent intent = new Intent(getActivity(), BackupActivity.class); + intent.putExtra(BackupActivity.EXTRA_SECRET, exportSecret); + startActivity(intent); + } + + private void restore() { + FileHelper.openDocument(this, null, "*/*", false, REQUEST_CODE_INPUT); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java index 552fa34c0..85be68505 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java @@ -33,12 +33,14 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.experimental.SentenceConfirm; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.ui.util.ExperimentalWordConfirm; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; +import java.io.IOException; + public class CertifyFingerprintFragment extends LoaderFragment implements LoaderManager.LoaderCallbacks<Cursor> { @@ -46,24 +48,26 @@ public class CertifyFingerprintFragment extends LoaderFragment implements static final int REQUEST_CERTIFY = 1; public static final String ARG_DATA_URI = "uri"; - public static final String ARG_ENABLE_WORD_CONFIRM = "enable_word_confirm"; + public static final String ARG_ENABLE_PHRASES_CONFIRM = "enable_word_confirm"; + private TextView mActionYes; private TextView mFingerprint; private TextView mIntro; + private TextView mHeader; private static final int LOADER_ID_UNIFIED = 0; private Uri mDataUri; - private boolean mEnableWordConfirm; + private boolean mEnablePhrasesConfirm; /** * Creates new instance of this fragment */ - public static CertifyFingerprintFragment newInstance(Uri dataUri, boolean enableWordConfirm) { + public static CertifyFingerprintFragment newInstance(Uri dataUri, boolean enablePhrasesConfirm) { CertifyFingerprintFragment frag = new CertifyFingerprintFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_DATA_URI, dataUri); - args.putBoolean(ARG_ENABLE_WORD_CONFIRM, enableWordConfirm); + args.putBoolean(ARG_ENABLE_PHRASES_CONFIRM, enablePhrasesConfirm); frag.setArguments(args); @@ -75,11 +79,12 @@ public class CertifyFingerprintFragment extends LoaderFragment implements View root = super.onCreateView(inflater, superContainer, savedInstanceState); View view = inflater.inflate(R.layout.certify_fingerprint_fragment, getContainer()); - View actionNo = view.findViewById(R.id.certify_fingerprint_button_no); - View actionYes = view.findViewById(R.id.certify_fingerprint_button_yes); + TextView actionNo = (TextView) view.findViewById(R.id.certify_fingerprint_button_no); + mActionYes = (TextView) view.findViewById(R.id.certify_fingerprint_button_yes); mFingerprint = (TextView) view.findViewById(R.id.certify_fingerprint_fingerprint); mIntro = (TextView) view.findViewById(R.id.certify_fingerprint_intro); + mHeader = (TextView) view.findViewById(R.id.certify_fingerprint_fingerprint_header); actionNo.setOnClickListener(new View.OnClickListener() { @Override @@ -87,7 +92,7 @@ public class CertifyFingerprintFragment extends LoaderFragment implements getActivity().finish(); } }); - actionYes.setOnClickListener(new View.OnClickListener() { + mActionYes.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { certify(mDataUri); @@ -107,10 +112,12 @@ public class CertifyFingerprintFragment extends LoaderFragment implements getActivity().finish(); return; } - mEnableWordConfirm = getArguments().getBoolean(ARG_ENABLE_WORD_CONFIRM); + mEnablePhrasesConfirm = getArguments().getBoolean(ARG_ENABLE_PHRASES_CONFIRM); - if (mEnableWordConfirm) { - mIntro.setText(R.string.certify_fingerprint_text_words); + if (mEnablePhrasesConfirm) { + mIntro.setText(R.string.certify_fingerprint_text_phrases); + mHeader.setText(R.string.section_phrases); + mActionYes.setText(R.string.btn_match_phrases); } loadData(dataUri); @@ -160,7 +167,7 @@ public class CertifyFingerprintFragment extends LoaderFragment implements if (data.moveToFirst()) { byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); - if (mEnableWordConfirm) { + if (mEnablePhrasesConfirm) { displayWordConfirm(fingerprintBlob); } else { displayHexConfirm(fingerprintBlob); @@ -180,9 +187,17 @@ public class CertifyFingerprintFragment extends LoaderFragment implements } private void displayWordConfirm(byte[] fingerprintBlob) { - String fingerprint = ExperimentalWordConfirm.getWords(getActivity(), fingerprintBlob); +// String fingerprint = ExperimentalWordConfirm.getWords(getActivity(), fingerprintBlob); + + String fingerprint; + try { + fingerprint = new SentenceConfirm(getActivity()).fromBytes(fingerprintBlob, 20); + } catch (IOException e) { + fingerprint = "-"; + Log.e(Constants.TAG, "Problem when creating sentence!", e); + } - mFingerprint.setTextSize(24); + mFingerprint.setTextSize(18); mFingerprint.setTypeface(Typeface.DEFAULT, Typeface.BOLD); mFingerprint.setText(fingerprint); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java index 579a001cb..a4163d7f9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java @@ -30,7 +30,6 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; @@ -114,19 +113,15 @@ public class CreateKeyActivity extends BaseNfcActivity { byte[] nfcAid = intent.getByteArrayExtra(EXTRA_NFC_AID); if (containsKeys(nfcFingerprints)) { - Fragment frag = CreateYubiKeyImportFragment.newInstance( + Fragment frag = CreateYubiKeyImportResetFragment.newInstance( nfcFingerprints, nfcAid, nfcUserId); loadFragment(frag, FragAction.START); setTitle(R.string.title_import_keys); } else { -// Fragment frag = CreateYubiKeyBlankFragment.newInstance(); -// loadFragment(frag, FragAction.START); -// setTitle(R.string.title_manage_my_keys); - Notify.create(this, - "YubiKey key creation is currently not supported. Please follow our FAQ.", - Notify.Style.ERROR - ).show(); + Fragment frag = CreateYubiKeyBlankFragment.newInstance(); + loadFragment(frag, FragAction.START); + setTitle(R.string.title_manage_my_keys); } // done @@ -160,7 +155,7 @@ public class CreateKeyActivity extends BaseNfcActivity { } @Override - protected void onNfcPostExecute() throws IOException { + protected void onNfcPostExecute() { if (mCurrentFragment instanceof NfcListenerFragment) { ((NfcListenerFragment) mCurrentFragment).onNfcPostExecute(); return; @@ -181,30 +176,28 @@ public class CreateKeyActivity extends BaseNfcActivity { finish(); } catch (PgpKeyNotFoundException e) { - Fragment frag = CreateYubiKeyImportFragment.newInstance( + Fragment frag = CreateYubiKeyImportResetFragment.newInstance( mScannedFingerprints, mNfcAid, mNfcUserId); loadFragment(frag, FragAction.TO_RIGHT); } } else { -// Fragment frag = CreateYubiKeyBlankFragment.newInstance(); -// loadFragment(frag, FragAction.TO_RIGHT); - Notify.create(this, - "YubiKey key creation is currently not supported. Please follow our FAQ.", - Notify.Style.ERROR - ).show(); + Fragment frag = CreateYubiKeyBlankFragment.newInstance(); + loadFragment(frag, FragAction.TO_RIGHT); } } private boolean containsKeys(byte[] scannedFingerprints) { + if (scannedFingerprints == null) { + return false; + } + // If all fingerprint bytes are 0, the card contains no keys. - boolean cardContainsKeys = false; for (byte b : scannedFingerprints) { if (b != 0) { - cardContainsKeys = true; - break; + return true; } } - return cardContainsKeys; + return false; } @Override @@ -264,7 +257,7 @@ public class CreateKeyActivity extends BaseNfcActivity { interface NfcListenerFragment { void doNfcInBackground() throws IOException; - void onNfcPostExecute() throws IOException; + void onNfcPostExecute(); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java index 739eb3e35..b79e4454d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyFinalFragment.java @@ -37,17 +37,17 @@ import org.spongycastle.bcpg.sig.KeyFlags; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.EditKeyResult; -import org.sufficientlysecure.keychain.operations.results.ExportResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm; import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; @@ -69,7 +69,7 @@ public class CreateKeyFinalFragment extends Fragment { SaveKeyringParcel mSaveKeyringParcel; - private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper; + private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper; private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mCreateOpHelper; private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mMoveToCardOpHelper; @@ -407,20 +407,20 @@ public class CreateKeyFinalFragment extends Fragment { } // set data uri as path to keyring - final Uri blobUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(saveKeyResult.mMasterKeyId); + final long masterKeyId = saveKeyResult.mMasterKeyId; // upload to favorite keyserver final String keyserver = Preferences.getPreferences(activity).getPreferredKeyserver(); - CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> callback - = new CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult>() { + CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult> callback + = new CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult>() { @Override - public ExportKeyringParcel createOperationInput() { - return new ExportKeyringParcel(keyserver, blobUri); + public UploadKeyringParcel createOperationInput() { + return new UploadKeyringParcel(keyserver, masterKeyId); } @Override - public void onCryptoOperationSuccess(ExportResult result) { + public void onCryptoOperationSuccess(UploadResult result) { handleResult(result); } @@ -430,11 +430,11 @@ public class CreateKeyFinalFragment extends Fragment { } @Override - public void onCryptoOperationError(ExportResult result) { + public void onCryptoOperationError(UploadResult result) { handleResult(result); } - public void handleResult(ExportResult result) { + public void handleResult(UploadResult result) { saveKeyResult.getLog().add(result, 0); finishWithResult(saveKeyResult); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyImportFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyImportResetFragment.java index d88e6b9f9..0a2d52617 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyImportFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyImportResetFragment.java @@ -25,11 +25,13 @@ import java.util.ArrayList; import android.app.Activity; import android.content.Intent; import android.os.Bundle; +import android.os.Parcelable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.RadioButton; import android.widget.TextView; import org.spongycastle.util.encoders.Hex; @@ -38,6 +40,8 @@ import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.ui.CreateKeyActivity.NfcListenerFragment; import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; @@ -45,11 +49,13 @@ import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Preferences; -public class CreateYubiKeyImportFragment +public class CreateYubiKeyImportResetFragment extends QueueingCryptoOperationFragment<ImportKeyringParcel, ImportKeyResult> implements NfcListenerFragment { - private static final String ARG_FINGERPRINT = "fingerprint"; + private static final int REQUEST_CODE_RESET = 0x00005001; + + private static final String ARG_FINGERPRINTS = "fingerprint"; public static final String ARG_AID = "aid"; public static final String ARG_USER_ID = "user_ids"; @@ -62,6 +68,10 @@ public class CreateYubiKeyImportFragment private ImportKeysListFragment mListFragment; private TextView vSerNo; private TextView vUserId; + private TextView mNextButton; + private RadioButton mRadioImport; + private RadioButton mRadioReset; + private View mResetWarning; // for CryptoOperationFragment key import private String mKeyserver; @@ -69,10 +79,10 @@ public class CreateYubiKeyImportFragment public static Fragment newInstance(byte[] scannedFingerprints, byte[] nfcAid, String userId) { - CreateYubiKeyImportFragment frag = new CreateYubiKeyImportFragment(); + CreateYubiKeyImportResetFragment frag = new CreateYubiKeyImportResetFragment(); Bundle args = new Bundle(); - args.putByteArray(ARG_FINGERPRINT, scannedFingerprints); + args.putByteArray(ARG_FINGERPRINTS, scannedFingerprints); args.putByteArray(ARG_AID, nfcAid); args.putString(ARG_USER_ID, userId); frag.setArguments(args); @@ -86,7 +96,7 @@ public class CreateYubiKeyImportFragment Bundle args = savedInstanceState != null ? savedInstanceState : getArguments(); - mNfcFingerprints = args.getByteArray(ARG_FINGERPRINT); + mNfcFingerprints = args.getByteArray(ARG_FINGERPRINTS); mNfcAid = args.getByteArray(ARG_AID); mNfcUserId = args.getString(ARG_USER_ID); @@ -98,49 +108,78 @@ public class CreateYubiKeyImportFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.create_yubi_key_import_fragment, container, false); + View view = inflater.inflate(R.layout.create_yubi_key_import_reset_fragment, container, false); vSerNo = (TextView) view.findViewById(R.id.yubikey_serno); vUserId = (TextView) view.findViewById(R.id.yubikey_userid); + mNextButton = (TextView) view.findViewById(R.id.create_key_next_button); + mRadioImport = (RadioButton) view.findViewById(R.id.yubikey_decision_import); + mRadioReset = (RadioButton) view.findViewById(R.id.yubikey_decision_reset); + mResetWarning = view.findViewById(R.id.yubikey_import_reset_warning); - { - View mBackButton = view.findViewById(R.id.create_key_back_button); - mBackButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (getFragmentManager().getBackStackEntryCount() == 0) { - getActivity().setResult(Activity.RESULT_CANCELED); - getActivity().finish(); - } else { - mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); - } + View mBackButton = view.findViewById(R.id.create_key_back_button); + mBackButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getFragmentManager().getBackStackEntryCount() == 0) { + getActivity().setResult(Activity.RESULT_CANCELED); + getActivity().finish(); + } else { + mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); } - }); + } + }); - View mNextButton = view.findViewById(R.id.create_key_next_button); - mNextButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { + mNextButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mRadioReset.isChecked()) { + resetCard(); + } else { importKey(); } - }); - } + } + }); mListFragment = ImportKeysListFragment.newInstance(null, null, "0x" + mNfcFingerprint, true, null); - view.findViewById(R.id.button_search).setOnClickListener(new OnClickListener() { + mRadioImport.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override - public void onClick(View v) { - refreshSearch(); + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + mNextButton.setText(R.string.btn_import); + mNextButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_key_plus_grey600_24dp, 0); + mNextButton.setVisibility(View.VISIBLE); + mResetWarning.setVisibility(View.GONE); + + getFragmentManager().beginTransaction() + .replace(R.id.yubikey_import_fragment, mListFragment, "yubikey_import") + .commit(); + + getFragmentManager().executePendingTransactions(); + refreshSearch(); + } + } + }); + mRadioReset.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + mNextButton.setText(R.string.btn_reset); + mNextButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_close_grey_24dp, 0); + mNextButton.setVisibility(View.VISIBLE); + mResetWarning.setVisibility(View.VISIBLE); + + getFragmentManager().beginTransaction() + .remove(mListFragment) + .commit(); + } } }); setData(); - getFragmentManager().beginTransaction() - .replace(R.id.yubikey_import_fragment, mListFragment, "yubikey_import") - .commit(); return view; } @@ -149,7 +188,7 @@ public class CreateYubiKeyImportFragment public void onSaveInstanceState(Bundle args) { super.onSaveInstanceState(args); - args.putByteArray(ARG_FINGERPRINT, mNfcFingerprints); + args.putByteArray(ARG_FINGERPRINTS, mNfcFingerprints); args.putByteArray(ARG_AID, mNfcAid); args.putString(ARG_USER_ID, mNfcUserId); } @@ -195,6 +234,25 @@ public class CreateYubiKeyImportFragment } + public void resetCard() { + Intent intent = new Intent(getActivity(), NfcOperationActivity.class); + intent.putExtra(NfcOperationActivity.EXTRA_SERVICE_INTENT, (Parcelable[]) null); + RequiredInputParcel resetP = RequiredInputParcel.createNfcReset(); + intent.putExtra(NfcOperationActivity.EXTRA_REQUIRED_INPUT, resetP); + intent.putExtra(NfcOperationActivity.EXTRA_CRYPTO_INPUT, new CryptoInputParcel()); + startActivityForResult(intent, REQUEST_CODE_RESET); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CODE_RESET && resultCode == Activity.RESULT_OK) { + mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); + return; + } + + super.onActivityResult(requestCode, resultCode, data); + } + @Override public void doNfcInBackground() throws IOException { @@ -208,11 +266,10 @@ public class CreateYubiKeyImportFragment } @Override - public void onNfcPostExecute() throws IOException { + public void onNfcPostExecute() { setData(); - refreshSearch(); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinFragment.java index a793b31f2..128383d6d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinFragment.java @@ -18,13 +18,15 @@ package org.sufficientlysecure.keychain.ui; import android.app.Activity; +import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; import android.widget.TextView; import org.sufficientlysecure.keychain.R; @@ -32,16 +34,34 @@ import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; import org.sufficientlysecure.keychain.util.Passphrase; import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashSet; public class CreateYubiKeyPinFragment extends Fragment { // view CreateKeyActivity mCreateKeyActivity; - TextView mPin; + EditText mPin; + EditText mPinRepeat; TextView mAdminPin; View mBackButton; View mNextButton; + private static HashSet<String> sPinBlacklist = new HashSet<>(Arrays.asList( + "000000", + "111111", + "222222", + "333333", + "444444", + "555555", + "666666", + "777777", + "888888", + "999999", + "123456", + "XXXXXX" + )); + /** * Creates new instance of this fragment */ @@ -54,50 +74,71 @@ public class CreateYubiKeyPinFragment extends Fragment { return frag; } + /** + * Checks if text of given EditText is not empty. If it is empty an error is + * set and the EditText gets the focus. + * + * @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; + } + + private static boolean areEditTextsEqual(EditText editText1, EditText editText2) { + Passphrase p1 = new Passphrase(editText1); + Passphrase p2 = new Passphrase(editText2); + return (p1.equals(p2)); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.create_yubi_key_pin_fragment, container, false); - mPin = (TextView) view.findViewById(R.id.create_yubi_key_pin); + mPin = (EditText) view.findViewById(R.id.create_yubi_key_pin); + mPinRepeat = (EditText) view.findViewById(R.id.create_yubi_key_pin_repeat); mAdminPin = (TextView) view.findViewById(R.id.create_yubi_key_admin_pin); mBackButton = view.findViewById(R.id.create_key_back_button); mNextButton = view.findViewById(R.id.create_key_next_button); if (mCreateKeyActivity.mYubiKeyPin == null) { - new AsyncTask<Void, Void, Pair<Passphrase, Passphrase>>() { + new AsyncTask<Void, Void, Passphrase>() { @Override - protected Pair<Passphrase, Passphrase> doInBackground(Void... unused) { + protected Passphrase doInBackground(Void... unused) { SecureRandom secureRandom = new SecureRandom(); - // min = 6, we choose 6 - String pin = "" + secureRandom.nextInt(9) + // min = 8, we choose 8 + String adminPin = "" + secureRandom.nextInt(9) + secureRandom.nextInt(9) + secureRandom.nextInt(9) + secureRandom.nextInt(9) + secureRandom.nextInt(9) - + secureRandom.nextInt(9); - // min = 8, we choose 10, but 6 are equals the PIN - String adminPin = pin + secureRandom.nextInt(9) + secureRandom.nextInt(9) + secureRandom.nextInt(9) + secureRandom.nextInt(9); - return new Pair<>(new Passphrase(pin), new Passphrase(adminPin)); + return new Passphrase(adminPin); } @Override - protected void onPostExecute(Pair<Passphrase, Passphrase> pair) { - mCreateKeyActivity.mYubiKeyPin = pair.first; - mCreateKeyActivity.mYubiKeyAdminPin = pair.second; + protected void onPostExecute(Passphrase adminPin) { + mCreateKeyActivity.mYubiKeyAdminPin = adminPin; - mPin.setText(mCreateKeyActivity.mYubiKeyPin.toStringUnsafe()); mAdminPin.setText(mCreateKeyActivity.mYubiKeyAdminPin.toStringUnsafe()); } }.execute(); } else { - mPin.setText(mCreateKeyActivity.mYubiKeyPin.toStringUnsafe()); mAdminPin.setText(mCreateKeyActivity.mYubiKeyAdminPin.toStringUnsafe()); } + mPin.requestFocus(); mBackButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -111,7 +152,6 @@ public class CreateYubiKeyPinFragment extends Fragment { } }); - return view; } @@ -121,14 +161,53 @@ public class CreateYubiKeyPinFragment extends Fragment { mCreateKeyActivity = (CreateKeyActivity) getActivity(); } + private void back() { + hideKeyboard(); + mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); + } private void nextClicked() { - CreateYubiKeyPinRepeatFragment frag = CreateYubiKeyPinRepeatFragment.newInstance(); - mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); + if (isEditTextNotEmpty(getActivity(), mPin)) { + + if (!areEditTextsEqual(mPin, mPinRepeat)) { + mPinRepeat.setError(getString(R.string.create_key_passphrases_not_equal)); + mPinRepeat.requestFocus(); + return; + } + + if (mPin.getText().toString().length() < 6) { + mPin.setError(getString(R.string.create_key_yubi_key_pin_too_short)); + mPin.requestFocus(); + return; + } + + if (sPinBlacklist.contains(mPin.getText().toString())) { + mPin.setError(getString(R.string.create_key_yubi_key_pin_insecure)); + mPin.requestFocus(); + return; + } + + mCreateKeyActivity.mYubiKeyPin = new Passphrase(mPin.getText().toString()); + + CreateKeyFinalFragment frag = CreateKeyFinalFragment.newInstance(); + hideKeyboard(); + mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); + } } - private void back() { - mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); + private void hideKeyboard() { + if (getActivity() == null) { + return; + } + 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); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinRepeatFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinRepeatFragment.java deleted file mode 100644 index 2e752e609..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateYubiKeyPinRepeatFragment.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2015 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.content.Context; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; - -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction; - -public class CreateYubiKeyPinRepeatFragment extends Fragment { - - // view - CreateKeyActivity mCreateKeyActivity; - EditText mPin; - EditText mAdminPin; - View mBackButton; - View mNextButton; - - /** - * Creates new instance of this fragment - */ - public static CreateYubiKeyPinRepeatFragment newInstance() { - CreateYubiKeyPinRepeatFragment frag = new CreateYubiKeyPinRepeatFragment(); - - Bundle args = new Bundle(); - frag.setArguments(args); - - 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; - } - - private static boolean checkPin(Context context, EditText editText1, String pin) { - boolean output = editText1.getText().toString().equals(pin); - - if (!output) { - editText1.setError(context.getString(R.string.create_key_yubi_key_pin_not_correct)); - editText1.requestFocus(); - } else { - editText1.setError(null); - } - - return output; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.create_yubi_key_pin_repeat_fragment, container, false); - - mPin = (EditText) view.findViewById(R.id.create_yubi_key_pin_repeat); - mAdminPin = (EditText) view.findViewById(R.id.create_yubi_key_admin_pin_repeat); - mBackButton = view.findViewById(R.id.create_key_back_button); - mNextButton = view.findViewById(R.id.create_key_next_button); - - mPin.requestFocus(); - mBackButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - back(); - } - }); - mNextButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - nextClicked(); - } - }); - - return view; - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - mCreateKeyActivity = (CreateKeyActivity) getActivity(); - } - - private void back() { - hideKeyboard(); - mCreateKeyActivity.loadFragment(null, FragAction.TO_LEFT); - } - - private void nextClicked() { - if (isEditTextNotEmpty(getActivity(), mPin) - && checkPin(getActivity(), mPin, mCreateKeyActivity.mYubiKeyPin.toStringUnsafe()) - && isEditTextNotEmpty(getActivity(), mAdminPin) - && checkPin(getActivity(), mAdminPin, mCreateKeyActivity.mYubiKeyAdminPin.toStringUnsafe())) { - - CreateKeyFinalFragment frag = CreateKeyFinalFragment.newInstance(); - hideKeyboard(); - mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT); - } - } - - private void hideKeyboard() { - if (getActivity() == null) { - return; - } - 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); - } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java index 4e9a6f17d..cf7a0b1d7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java @@ -37,7 +37,7 @@ import android.widget.Toast; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; import org.sufficientlysecure.keychain.pgp.PgpHelper; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.ui.base.BaseActivity; @@ -193,14 +193,14 @@ public class DecryptActivity extends BaseActivity { @Nullable public Uri readToTempFile(String text) throws IOException { - Uri tempFile = TemporaryStorageProvider.createFile(this); + Uri tempFile = TemporaryFileProvider.createFile(this); OutputStream outStream = getContentResolver().openOutputStream(tempFile); if (outStream == null) { return null; } // clean up ascii armored message, fixing newlines and stuff - String cleanedText = PgpHelper.getPgpContent(text); + String cleanedText = PgpHelper.getPgpMessageContent(text); if (cleanedText == null) { return null; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java index dcba595e9..8adaa0670 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java @@ -22,13 +22,17 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import android.Manifest; +import android.annotation.TargetApi; import android.app.Activity; import android.content.ClipDescription; import android.content.Context; import android.content.Intent; import android.content.pm.LabeledIntent; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.Point; @@ -36,8 +40,12 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -65,10 +73,14 @@ import org.openintents.openpgp.OpenPgpSignatureResult; import org.sufficientlysecure.keychain.BuildConfig; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.InputDataResult; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.service.InputDataParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment; // this import NEEDS to be above the ViewModel AND SubViewHolder one, or it won't compile! (as of 16.09.15) import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder; @@ -82,8 +94,25 @@ import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableHashMap; +import org.sufficientlysecure.keychain.util.Preferences; +/** Displays a list of decrypted inputs. + * + * This class has a complex control flow to manage its input URIs. Each URI + * which is in mInputUris is also in exactly one of mPendingInputUris, + * mCancelledInputUris, mCurrentInputUri, or a key in mInputDataResults. + * + * Processing of URIs happens using a looping approach: + * - There is always exactly one method running which works on mCurrentInputUri + * - Processing starts in cryptoOperation(), which pops a new mCurrentInputUri + * from the list of mPendingInputUris. + * - Once a mCurrentInputUri is finished processing, it should be set to null and + * control handed back to cryptoOperation() + * - Control flow can move through asynchronous calls, and resume in callbacks + * like onActivityResult() or onPermissionRequestResult(). + * + */ public class DecryptListFragment extends QueueingCryptoOperationFragment<InputDataParcel,InputDataResult> implements OnMenuItemClickListener { @@ -95,7 +124,7 @@ public class DecryptListFragment public static final String ARG_CAN_DELETE = "can_delete"; private static final int REQUEST_CODE_OUTPUT = 0x00007007; - public static final String ARG_CURRENT_URI = "current_uri"; + private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12; private ArrayList<Uri> mInputUris; private HashMap<Uri, InputDataResult> mInputDataResults; @@ -111,7 +140,7 @@ public class DecryptListFragment /** * Creates new instance of this fragment */ - public static DecryptListFragment newInstance(ArrayList<Uri> uris, boolean canDelete) { + public static DecryptListFragment newInstance(@NonNull ArrayList<Uri> uris, boolean canDelete) { DecryptListFragment frag = new DecryptListFragment(); Bundle args = new Bundle(); @@ -168,9 +197,12 @@ public class DecryptListFragment outState.putParcelable(ARG_RESULTS, new ParcelableHashMap<>(results)); outState.putParcelable(ARG_OUTPUT_URIS, new ParcelableHashMap<>(mInputDataResults)); outState.putParcelableArrayList(ARG_CANCELLED_URIS, mCancelledInputUris); - outState.putParcelable(ARG_CURRENT_URI, mCurrentInputUri); outState.putBoolean(ARG_CAN_DELETE, mCanDelete); + // this does not save mCurrentInputUri - if anything is being + // processed at fragment recreation time, the operation in + // progress will be lost! + } @Override @@ -182,20 +214,21 @@ public class DecryptListFragment ArrayList<Uri> inputUris = getArguments().getParcelableArrayList(ARG_INPUT_URIS); ArrayList<Uri> cancelledUris = args.getParcelableArrayList(ARG_CANCELLED_URIS); ParcelableHashMap<Uri,InputDataResult> results = args.getParcelable(ARG_RESULTS); - Uri currentInputUri = args.getParcelable(ARG_CURRENT_URI); mCanDelete = args.getBoolean(ARG_CAN_DELETE, false); - displayInputUris(inputUris, currentInputUri, cancelledUris, + displayInputUris(inputUris, cancelledUris, results != null ? results.getMap() : null ); } - private void displayInputUris(ArrayList<Uri> inputUris, Uri currentInputUri, - ArrayList<Uri> cancelledUris, HashMap<Uri,InputDataResult> results) { + private void displayInputUris( + ArrayList<Uri> inputUris, + ArrayList<Uri> cancelledUris, + HashMap<Uri,InputDataResult> results) { mInputUris = inputUris; - mCurrentInputUri = currentInputUri; + mCurrentInputUri = null; mInputDataResults = results != null ? results : new HashMap<Uri,InputDataResult>(inputUris.size()); mCancelledInputUris = cancelledUris != null ? cancelledUris : new ArrayList<Uri>(); @@ -204,30 +237,23 @@ public class DecryptListFragment for (final Uri uri : inputUris) { mAdapter.add(uri); - if (uri.equals(mCurrentInputUri)) { + boolean uriIsCancelled = mCancelledInputUris.contains(uri); + if (uriIsCancelled) { + mAdapter.setCancelled(uri, true); continue; } - if (mCancelledInputUris.contains(uri)) { - mAdapter.setCancelled(uri, new OnClickListener() { - @Override - public void onClick(View v) { - retryUri(uri); - } - }); + boolean uriHasResult = results != null && results.containsKey(uri); + if (uriHasResult) { + processResult(uri); continue; } - if (results != null && results.containsKey(uri)) { - processResult(uri); - } else { - mPendingInputUris.add(uri); - } + mPendingInputUris.add(uri); } - if (mCurrentInputUri == null) { - cryptoOperation(); - } + // check if there are any pending input uris + cryptoOperation(); } @Override @@ -249,6 +275,7 @@ public class DecryptListFragment } } + @TargetApi(VERSION_CODES.KITKAT) private void saveFileDialog(InputDataResult result, int index) { Activity activity = getActivity(); @@ -257,17 +284,16 @@ public class DecryptListFragment } OpenPgpMetadata metadata = result.mMetadata.get(index); - Uri saveUri = Uri.fromFile(activity.getExternalFilesDir(metadata.getMimeType())); mCurrentSaveFileUri = result.getOutputUris().get(index); String filename = metadata.getFilename(); - if (filename == null) { + if (TextUtils.isEmpty(filename)) { String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(metadata.getMimeType()); filename = "decrypted" + (ext != null ? "."+ext : ""); } - FileHelper.saveDocument(this, filename, saveUri, metadata.getMimeType(), - R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, REQUEST_CODE_OUTPUT); + // requires >=kitkat + FileHelper.saveDocument(this, filename, metadata.getMimeType(), REQUEST_CODE_OUTPUT); } private void saveFile(Uri saveUri) { @@ -320,6 +346,29 @@ public class DecryptListFragment Uri uri = mCurrentInputUri; mCurrentInputUri = null; + Activity activity = getActivity(); + + boolean isSingleInput = mInputDataResults.isEmpty() && mPendingInputUris.isEmpty(); + if (isSingleInput) { + + // there is always at least one mMetadata object, so we know this is >= 1 already + boolean isSingleMetadata = result.mMetadata.size() == 1; + OpenPgpMetadata metadata = result.mMetadata.get(0); + boolean isText = "text/plain".equals(metadata.getMimeType()); + boolean isOverSized = metadata.getOriginalSize() > Constants.TEXT_LENGTH_LIMIT; + + if (isSingleMetadata && isText && !isOverSized) { + Intent displayTextIntent = new Intent(activity, DisplayTextActivity.class) + .setDataAndType(result.mOutputUris.get(0), "text/plain") + .putExtra(DisplayTextActivity.EXTRA_RESULT, result.mDecryptVerifyResult) + .putExtra(DisplayTextActivity.EXTRA_METADATA, metadata); + activity.startActivity(displayTextIntent); + activity.finish(); + return; + } + + } + mInputDataResults.put(uri, result); processResult(uri); @@ -334,12 +383,7 @@ public class DecryptListFragment mCurrentInputUri = null; mCancelledInputUris.add(uri); - mAdapter.setCancelled(uri, new OnClickListener() { - @Override - public void onClick(View v) { - retryUri(uri); - } - }); + mAdapter.setCancelled(uri, true); cryptoOperation(); @@ -375,11 +419,20 @@ public class DecryptListFragment if (ClipDescription.compareMimeTypes(type, "text/plain")) { // noinspection deprecation, this should be called from Context, but not available in minSdk icon = getResources().getDrawable(R.drawable.ic_chat_black_24dp); + } else if (ClipDescription.compareMimeTypes(type, "application/octet-stream")) { + // icons for this are just confusing + // noinspection deprecation, this should be called from Context, but not available in minSdk + icon = getResources().getDrawable(R.drawable.ic_doc_generic_am); + } else if (ClipDescription.compareMimeTypes(type, Constants.MIME_TYPE_KEYS)) { + // noinspection deprecation, this should be called from Context, but not available in minSdk + icon = getResources().getDrawable(R.drawable.ic_key_plus_grey600_24dp); } else if (ClipDescription.compareMimeTypes(type, "image/*")) { - int px = FormattingUtils.dpToPx(context, 48); + int px = FormattingUtils.dpToPx(context, 32); Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px)); icon = new BitmapDrawable(context.getResources(), bitmap); - } else { + } + + if (icon == null) { final Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(outputUri, type); @@ -420,11 +473,12 @@ public class DecryptListFragment // un-cancel this one mCancelledInputUris.remove(uri); + mInputDataResults.remove(uri); mPendingInputUris.add(uri); - mAdapter.setCancelled(uri, null); + mAdapter.resetItemData(uri); + // check if there are any pending input uris cryptoOperation(); - } public void displayBottomSheet(final InputDataResult result, final int index) { @@ -445,6 +499,7 @@ public class DecryptListFragment displayWithViewIntent(result, index, true, true); break; case R.id.decrypt_save: + // only inside the menu xml for Android >= 4.4 saveFileDialog(result, index); break; } @@ -525,6 +580,11 @@ public class DecryptListFragment } else { intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(outputUri, metadata.getMimeType()); + + if (!forceChooser && Constants.MIME_TYPE_KEYS.equals(metadata.getMimeType())) { + // bind Intent to this OpenKeychain, don't allow other apps to intercept here! + intent.setPackage(getActivity().getPackageName()); + } } intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -539,6 +599,11 @@ public class DecryptListFragment @Override public InputDataParcel createOperationInput() { + Activity activity = getActivity(); + if (activity == null) { + return null; + } + if (mCurrentInputUri == null) { if (mPendingInputUris.isEmpty()) { // nothing left to do @@ -548,7 +613,11 @@ public class DecryptListFragment mCurrentInputUri = mPendingInputUris.remove(0); } - Log.d(Constants.TAG, "mInputUri=" + mCurrentInputUri); + Log.d(Constants.TAG, "mCurrentInputUri=" + mCurrentInputUri); + + if ( ! checkAndRequestReadPermission(activity, mCurrentInputUri)) { + return null; + } PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel() .setAllowSymmetricDecryption(true); @@ -556,6 +625,94 @@ public class DecryptListFragment } + /** + * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris. + * + * This method returns true on Android < 6, or if permission is already granted. It + * requests the permission and returns false otherwise, taking over responsibility + * for mCurrentInputUri. + * + * see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html + */ + private boolean checkAndRequestReadPermission(Activity activity, final Uri uri) { + if ( ! "file".equals(uri.getScheme())) { + return true; + } + + if (Build.VERSION.SDK_INT < VERSION_CODES.M) { + 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.JELLY_BEAN) { + return true; + } + + if (ContextCompat.checkSelfPermission(activity, 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) { + + // permission granted -> retry all cancelled file uris + Iterator<Uri> it = mCancelledInputUris.iterator(); + while (it.hasNext()) { + Uri uri = it.next(); + if ( ! "file".equals(uri.getScheme())) { + continue; + } + it.remove(); + mPendingInputUris.add(uri); + mAdapter.setCancelled(uri, false); + } + + } else { + + // permission denied -> cancel current, and all pending file uris + mCancelledInputUris.add(mCurrentInputUri); + mAdapter.setCancelled(mCurrentInputUri, true); + + mCurrentInputUri = null; + Iterator<Uri> it = mPendingInputUris.iterator(); + while (it.hasNext()) { + Uri uri = it.next(); + if ( ! "file".equals(uri.getScheme())) { + continue; + } + it.remove(); + mCancelledInputUris.add(uri); + mAdapter.setCancelled(uri, true); + } + + } + + // hand control flow back + cryptoOperation(); + + } + @Override public boolean onMenuItemClick(MenuItem menuItem) { if (mAdapter.mMenuClickedModel == null || !mAdapter.mMenuClickedModel.hasResult()) { @@ -586,38 +743,83 @@ public class DecryptListFragment return false; } + private void lookupUnknownKey(final Uri inputUri, long unknownKeyId) { + + final ArrayList<ParcelableKeyRing> keyList; + final String keyserver; + + // search config + { + Preferences prefs = Preferences.getPreferences(getActivity()); + Preferences.CloudSearchPrefs cloudPrefs = + new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver()); + keyserver = cloudPrefs.keyserver; + } + + { + ParcelableKeyRing keyEntry = new ParcelableKeyRing(null, + KeyFormattingUtils.convertKeyIdToHex(unknownKeyId), null); + ArrayList<ParcelableKeyRing> selectedEntries = new ArrayList<>(); + selectedEntries.add(keyEntry); + + keyList = selectedEntries; + } + + CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> callback + = new CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult>() { + + @Override + public ImportKeyringParcel createOperationInput() { + return new ImportKeyringParcel(keyList, keyserver); + } + + @Override + public void onCryptoOperationSuccess(ImportKeyResult result) { + retryUri(inputUri); + } + + @Override + public void onCryptoOperationCancelled() { + mAdapter.setProcessingKeyLookup(inputUri, false); + } + + @Override + public void onCryptoOperationError(ImportKeyResult result) { + result.createNotify(getActivity()).show(); + mAdapter.setProcessingKeyLookup(inputUri, false); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + + mAdapter.setProcessingKeyLookup(inputUri, true); + + CryptoOperationHelper importOpHelper = new CryptoOperationHelper<>(2, this, callback, null); + importOpHelper.cryptoOperation(); + + } + + private void deleteFile(Activity activity, Uri uri) { // we can only ever delete a file once, if we got this far either it's gone or it will never work mCanDelete = false; - if ("file".equals(uri.getScheme())) { - File file = new File(uri.getPath()); - if (file.delete()) { + try { + int deleted = FileHelper.deleteFileSecurely(activity, uri); + if (deleted > 0) { Notify.create(activity, R.string.file_delete_ok, Style.OK).show(); } else { Notify.create(activity, R.string.file_delete_none, Style.WARN).show(); } - return; - } - - if ("content".equals(uri.getScheme())) { - try { - int deleted = activity.getContentResolver().delete(uri, null, null); - if (deleted > 0) { - Notify.create(activity, R.string.file_delete_ok, Style.OK).show(); - } else { - Notify.create(activity, R.string.file_delete_none, Style.WARN).show(); - } - } catch (Exception e) { - Log.e(Constants.TAG, "exception deleting file", e); - Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show(); - } - return; + } catch (Exception e) { + Log.e(Constants.TAG, "exception deleting file", e); + Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show(); } - Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show(); - } public class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> { @@ -631,6 +833,7 @@ public class DecryptListFragment int mProgress, mMax; String mProgressMsg; OnClickListener mCancelled; + boolean mProcessingKeyLookup; ViewModel(Uri uri) { mInputUri = uri; @@ -639,7 +842,7 @@ public class DecryptListFragment mCancelled = null; } - void addResult(InputDataResult result) { + void setResult(InputDataResult result) { mResult = result; } @@ -659,6 +862,10 @@ public class DecryptListFragment mMax = max; } + void setProcessingKeyLookup(boolean processingKeyLookup) { + mProcessingKeyLookup = processingKeyLookup; + } + // Depends on inputUri only @Override public boolean equals(Object o) { @@ -669,8 +876,10 @@ public class DecryptListFragment return false; } ViewModel viewModel = (ViewModel) o; - return !(mInputUri != null ? !mInputUri.equals(viewModel.mInputUri) - : viewModel.mInputUri != null); + if (mInputUri == null) { + return viewModel.mInputUri == null; + } + return mInputUri.equals(viewModel.mInputUri); } // Depends on inputUri only @@ -725,17 +934,13 @@ public class DecryptListFragment } private void bindItemCancelled(ViewHolder holder, ViewModel model) { - if (holder.vAnimator.getDisplayedChild() != 3) { - holder.vAnimator.setDisplayedChild(3); - } + holder.vAnimator.setDisplayedChild(3); holder.vCancelledRetry.setOnClickListener(model.mCancelled); } private void bindItemProgress(ViewHolder holder, ViewModel model) { - if (holder.vAnimator.getDisplayedChild() != 0) { - holder.vAnimator.setDisplayedChild(0); - } + holder.vAnimator.setDisplayedChild(0); holder.vProgress.setProgress(model.mProgress); holder.vProgress.setMax(model.mMax); @@ -745,11 +950,10 @@ public class DecryptListFragment } private void bindItemSuccess(ViewHolder holder, final ViewModel model) { - if (holder.vAnimator.getDisplayedChild() != 1) { - holder.vAnimator.setDisplayedChild(1); - } + holder.vAnimator.setDisplayedChild(1); - KeyFormattingUtils.setStatus(getResources(), holder, model.mResult.mDecryptVerifyResult); + KeyFormattingUtils.setStatus(getResources(), holder, + model.mResult.mDecryptVerifyResult, model.mProcessingKeyLookup); int numFiles = model.mResult.getOutputUris().size(); holder.resizeFileList(numFiles, LayoutInflater.from(getActivity())); @@ -762,11 +966,14 @@ public class DecryptListFragment String filename; if (metadata == null) { filename = getString(R.string.filename_unknown); - } else if (TextUtils.isEmpty(metadata.getFilename())) { - filename = getString("text/plain".equals(metadata.getMimeType()) - ? R.string.filename_unknown_text : R.string.filename_unknown); - } else { + } else if ( ! TextUtils.isEmpty(metadata.getFilename())) { filename = metadata.getFilename(); + } else if (ClipDescription.compareMimeTypes(metadata.getMimeType(), Constants.MIME_TYPE_KEYS)) { + filename = getString(R.string.filename_keys); + } else if (ClipDescription.compareMimeTypes(metadata.getMimeType(), "text/plain")) { + filename = getString(R.string.filename_unknown_text); + } else { + filename = getString(R.string.filename_unknown); } fileHolder.vFilename.setText(filename); @@ -824,6 +1031,13 @@ public class DecryptListFragment activity.startActivity(intent); } }); + } else { + holder.vSignatureLayout.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + lookupUnknownKey(model.mInputUri, keyId); + } + }); } } @@ -852,9 +1066,7 @@ public class DecryptListFragment } private void bindItemFailure(ViewHolder holder, final ViewModel model) { - if (holder.vAnimator.getDisplayedChild() != 2) { - holder.vAnimator.setDisplayedChild(2); - } + holder.vAnimator.setDisplayedChild(2); holder.vErrorMsg.setText(model.mResult.getLog().getLast().mType.getMsgId()); @@ -903,21 +1115,44 @@ public class DecryptListFragment notifyItemChanged(pos); } - public void setCancelled(Uri uri, OnClickListener retryListener) { + public void setCancelled(final Uri uri, boolean isCancelled) { ViewModel newModel = new ViewModel(uri); int pos = mDataset.indexOf(newModel); - mDataset.get(pos).setCancelled(retryListener); + if (isCancelled) { + mDataset.get(pos).setCancelled(new OnClickListener() { + @Override + public void onClick(View v) { + retryUri(uri); + } + }); + } else { + mDataset.get(pos).setCancelled(null); + } notifyItemChanged(pos); } - public void addResult(Uri uri, InputDataResult result) { + public void setProcessingKeyLookup(Uri uri, boolean processingKeyLookup) { + ViewModel newModel = new ViewModel(uri); + int pos = mDataset.indexOf(newModel); + mDataset.get(pos).setProcessingKeyLookup(processingKeyLookup); + notifyItemChanged(pos); + } + public void addResult(Uri uri, InputDataResult result) { ViewModel model = new ViewModel(uri); int pos = mDataset.indexOf(model); model = mDataset.get(pos); + model.setResult(result); + notifyItemChanged(pos); + } - model.addResult(result); - + public void resetItemData(Uri uri) { + ViewModel model = new ViewModel(uri); + int pos = mDataset.indexOf(model); + model = mDataset.get(pos); + model.setResult(null); + model.setCancelled(null); + model.setProcessingKeyLookup(false); notifyItemChanged(pos); } @@ -941,7 +1176,7 @@ public class DecryptListFragment public View vSignatureLayout; public TextView vSignatureName; public TextView vSignatureMail; - public TextView vSignatureAction; + public ViewAnimator vSignatureAction; public View vContextMenu; public TextView vErrorMsg; @@ -984,7 +1219,7 @@ public class DecryptListFragment vSignatureLayout = itemView.findViewById(R.id.result_signature_layout); vSignatureName = (TextView) itemView.findViewById(R.id.result_signature_name); vSignatureMail= (TextView) itemView.findViewById(R.id.result_signature_email); - vSignatureAction = (TextView) itemView.findViewById(R.id.result_signature_action); + vSignatureAction = (ViewAnimator) itemView.findViewById(R.id.result_signature_action); vFileList = (LinearLayout) itemView.findViewById(R.id.file_list); for (int i = 0; i < vFileList.getChildCount(); i++) { @@ -1048,7 +1283,7 @@ public class DecryptListFragment } @Override - public TextView getSignatureAction() { + public ViewAnimator getSignatureAction() { return vSignatureAction; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java index 4bcca09f1..2fa1aa1c2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextActivity.java @@ -28,9 +28,12 @@ import android.support.v4.app.Fragment; import android.widget.Toast; import org.openintents.openpgp.OpenPgpMetadata; +import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.ui.base.BaseActivity; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.FileHelper; public class DisplayTextActivity extends BaseActivity { @@ -73,6 +76,11 @@ public class DisplayTextActivity extends BaseActivity { } if (plaintext != null) { + if (plaintext.length() > Constants.TEXT_LENGTH_LIMIT) { + plaintext = plaintext.substring(0, Constants.TEXT_LENGTH_LIMIT); + Notify.create(this, R.string.snack_text_too_long, Style.WARN).show(); + } + loadFragment(plaintext, result); } else { Toast.makeText(this, R.string.error_invalid_data, Toast.LENGTH_LONG).show(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java index dc06e9115..1060714f0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DisplayTextFragment.java @@ -36,7 +36,6 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; -import org.sufficientlysecure.keychain.util.ShareHelper; public class DisplayTextFragment extends DecryptFragment { @@ -60,22 +59,6 @@ public class DisplayTextFragment extends DecryptFragment { return frag; } - /** - * Create Intent Chooser but exclude decrypt activites - */ - private Intent sendWithChooserExcludingDecrypt(String text) { - Intent prototype = createSendIntent(text); - String title = getString(R.string.title_share_message); - - // we don't want to decrypt the decrypted, no inception ;) - String[] blacklist = new String[]{ - Constants.PACKAGE_NAME + ".ui.DecryptActivity", - "org.thialfihar.android.apg.ui.DecryptActivity" - }; - - return new ShareHelper(getActivity()).createChooserExcluding(prototype, title, blacklist); - } - private Intent createSendIntent(String text) { Intent sendIntent = new Intent(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, text); @@ -146,7 +129,8 @@ public class DisplayTextFragment extends DecryptFragment { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.decrypt_share: { - startActivity(sendWithChooserExcludingDecrypt(mText.getText().toString())); + startActivity(Intent.createChooser(createSendIntent(mText.getText().toString()), + getString(R.string.title_share_message))); break; } case R.id.decrypt_copy: { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptFragment.java index 84660ca7a..89ea6165b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptFragment.java @@ -25,7 +25,6 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; @@ -42,7 +41,7 @@ import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker; import org.sufficientlysecure.keychain.util.FileHelper; -public class EncryptDecryptOverviewFragment extends Fragment { +public class EncryptDecryptFragment extends Fragment { View mClipboardIcon; @@ -56,7 +55,7 @@ public class EncryptDecryptOverviewFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.encrypt_decrypt_overview_fragment, container, false); + View view = inflater.inflate(R.layout.encrypt_decrypt_fragment, container, false); View mEncryptFile = view.findViewById(R.id.encrypt_files); View mEncryptText = view.findViewById(R.id.encrypt_text); @@ -83,7 +82,7 @@ public class EncryptDecryptOverviewFragment extends Fragment { mDecryptFile.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - FileHelper.openDocument(EncryptDecryptOverviewFragment.this, null, "*/*", false, REQUEST_CODE_INPUT); + FileHelper.openDocument(EncryptDecryptFragment.this, null, "*/*", false, REQUEST_CODE_INPUT); } }); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java index 8572a5712..ff1b9d478 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java @@ -25,6 +25,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import android.annotation.TargetApi; import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; @@ -34,6 +35,7 @@ import android.graphics.Bitmap; import android.graphics.Point; import android.net.Uri; import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v7.widget.DefaultItemAnimator; @@ -55,7 +57,7 @@ import org.sufficientlysecure.keychain.operations.results.SignEncryptResult; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants; import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.adapter.SpacesItemDecoration; import org.sufficientlysecure.keychain.ui.base.CachingCryptoOperationFragment; @@ -68,7 +70,6 @@ import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; -import org.sufficientlysecure.keychain.util.ShareHelper; public class EncryptFilesFragment extends CachingCryptoOperationFragment<SignEncryptParcel, SignEncryptResult> { @@ -216,6 +217,7 @@ public class EncryptFilesFragment mSelectedFiles.requestFocus(); } + @TargetApi(VERSION_CODES.KITKAT) private void showOutputFileDialog() { if (mFilesAdapter.getModelCount() != 1) { throw new IllegalStateException(); @@ -224,9 +226,7 @@ public class EncryptFilesFragment String targetName = (mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri)) + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); - Uri inputUri = model.inputUri; - FileHelper.saveDocument(this, targetName, inputUri, - R.string.title_encrypt_to_file, R.string.specify_file_to_encrypt_to, REQUEST_CODE_OUTPUT); + FileHelper.saveDocument(this, targetName, REQUEST_CODE_OUTPUT); } public void addFile(Intent data) { @@ -308,6 +308,17 @@ public class EncryptFilesFragment return true; } + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + // Show save only on Android >= 4.4 (Document Provider) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + MenuItem save = menu.findItem(R.id.encrypt_save); + save.setVisible(false); + } + } + public void toggleUseArmor(MenuItem item, final boolean useArmor) { mUseArmor = useArmor; @@ -393,7 +404,7 @@ public class EncryptFilesFragment public void onDeleted() { if (mAfterEncryptAction == AfterEncryptAction.SHARE) { // Share encrypted message/file - startActivity(sendWithChooserExcludingEncrypt()); + startActivity(Intent.createChooser(createSendIntent(), getString(R.string.title_share_file))); } else { Activity activity = getActivity(); if (activity == null) { @@ -413,7 +424,7 @@ public class EncryptFilesFragment case SHARE: // Share encrypted message/file - startActivity(sendWithChooserExcludingEncrypt()); + startActivity(Intent.createChooser(createSendIntent(), getString(R.string.title_share_file))); break; case COPY: @@ -455,7 +466,7 @@ public class EncryptFilesFragment String targetName = (mEncryptFilenames ? String.valueOf(filenameCounter) : FileHelper.getFilename(getActivity(), model.inputUri)) + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); - mOutputUris.add(TemporaryStorageProvider.createFile(getActivity(), targetName)); + mOutputUris.add(TemporaryFileProvider.createFile(getActivity(), targetName)); filenameCounter++; } return false; @@ -478,7 +489,7 @@ public class EncryptFilesFragment String targetName = (mEncryptFilenames ? String.valueOf(1) : FileHelper.getFilename(getActivity(), mFilesAdapter.getModelItem(0).inputUri)) + Constants.FILE_EXTENSION_ASC; - mOutputUris.add(TemporaryStorageProvider.createFile(getActivity(), targetName, "text/plain")); + mOutputUris.add(TemporaryFileProvider.createFile(getActivity(), targetName, "text/plain")); return false; } @@ -587,22 +598,6 @@ public class EncryptFilesFragment return data; } - /** - * Create Intent Chooser but exclude OK's EncryptActivity. - */ - private Intent sendWithChooserExcludingEncrypt() { - Intent prototype = createSendIntent(); - String title = getString(R.string.title_share_file); - - // we don't want to encrypt the encrypted, no inception ;) - String[] blacklist = new String[]{ - Constants.PACKAGE_NAME + ".ui.EncryptFilesActivity", - "org.thialfihar.android.apg.ui.EncryptActivity" - }; - - return new ShareHelper(getActivity()).createChooserExcluding(prototype, title, blacklist); - } - private Intent createSendIntent() { Intent sendIntent; // file @@ -613,7 +608,7 @@ public class EncryptFilesFragment sendIntent = new Intent(Intent.ACTION_SEND_MULTIPLE); sendIntent.putExtra(Intent.EXTRA_STREAM, mOutputUris); } - sendIntent.setType(Constants.ENCRYPTED_FILES_MIME); + sendIntent.setType(Constants.MIME_TYPE_ENCRYPTED_ALTERNATE); EncryptActivity modeInterface = (EncryptActivity) getActivity(); EncryptModeFragment modeFragment = modeInterface.getModeFragment(); 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 a849cdf12..50dbc9a8b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextActivity.java @@ -18,14 +18,23 @@ package org.sufficientlysecure.keychain.ui; + +import java.io.IOException; + import android.app.Activity; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; -import android.view.View; +import android.widget.Toast; +import org.apache.james.mime4j.util.MimeUtil; +import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.Log; public class EncryptTextActivity extends EncryptActivity { @@ -51,23 +60,71 @@ public class EncryptTextActivity extends EncryptActivity { extras = new Bundle(); } + String textData = extras.getString(EXTRA_TEXT); + boolean returnProcessText = false; + // When sending to OpenKeychain Encrypt via share menu if (Intent.ACTION_SEND.equals(action) && type != null) { Log.logDebugBundle(extras, "extras"); // When sending to OpenKeychain Encrypt via share menu - if ("text/plain".equals(type)) { - String sharedText = extras.getString(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); + if ( ! MimeUtil.isSameMimeType("text/plain", type)) { + Toast.makeText(this, R.string.toast_wrong_mimetype, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + String sharedText; + if (extras.containsKey(Intent.EXTRA_TEXT)) { + sharedText = extras.getString(Intent.EXTRA_TEXT); + } else if (extras.containsKey(Intent.EXTRA_STREAM)) { + try { + sharedText = FileHelper.readTextFromUri(this, extras.<Uri>getParcelable(Intent.EXTRA_STREAM), null); + } catch (IOException e) { + Toast.makeText(this, R.string.error_preparing_data, Toast.LENGTH_LONG).show(); + finish(); + return; } + } else { + Toast.makeText(this, R.string.toast_no_text, Toast.LENGTH_LONG).show(); + finish(); + return; + } + if (sharedText != null) { + if (sharedText.length() > Constants.TEXT_LENGTH_LIMIT) { + sharedText = sharedText.substring(0, Constants.TEXT_LENGTH_LIMIT); + Notify.create(this, R.string.snack_shared_text_too_long, Style.WARN).show(); + } + // handle like normal text encryption, override action and extras to later + // executeServiceMethod ACTION_ENCRYPT_TEXT in main actions + textData = sharedText; + } + + } + + // Android 6, PROCESS_TEXT Intent + if (Intent.ACTION_PROCESS_TEXT.equals(action) && type != null) { + + String sharedText = null; + if (extras.containsKey(Intent.EXTRA_PROCESS_TEXT)) { + sharedText = extras.getString(Intent.EXTRA_PROCESS_TEXT); + returnProcessText = true; + } else if (extras.containsKey(Intent.EXTRA_PROCESS_TEXT_READONLY)) { + sharedText = extras.getString(Intent.EXTRA_PROCESS_TEXT_READONLY); + } + + if (sharedText != null) { + if (sharedText.length() > Constants.TEXT_LENGTH_LIMIT) { + sharedText = sharedText.substring(0, Constants.TEXT_LENGTH_LIMIT); + Notify.create(this, R.string.snack_shared_text_too_long, Style.WARN).show(); + } + // handle like normal text encryption, override action and extras to later + // executeServiceMethod ACTION_ENCRYPT_TEXT in main actions + textData = sharedText; } } - String textData = extras.getString(EXTRA_TEXT); if (textData == null) { textData = ""; } @@ -75,7 +132,7 @@ public class EncryptTextActivity extends EncryptActivity { if (savedInstanceState == null) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - EncryptTextFragment encryptFragment = EncryptTextFragment.newInstance(textData); + EncryptTextFragment encryptFragment = EncryptTextFragment.newInstance(textData, returnProcessText); transaction.replace(R.id.encrypt_text_container, encryptFragment); transaction.commit(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java index ab676285e..10d88253d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptTextFragment.java @@ -46,7 +46,6 @@ import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; -import org.sufficientlysecure.keychain.util.ShareHelper; import java.util.Date; import java.util.HashSet; @@ -57,8 +56,10 @@ public class EncryptTextFragment public static final String ARG_TEXT = "text"; public static final String ARG_USE_COMPRESSION = "use_compression"; + public static final String ARG_RETURN_PROCESS_TEXT = "return_process_text"; private boolean mShareAfterEncrypt; + private boolean mReturnProcessTextAfterEncrypt; private boolean mUseCompression; private boolean mHiddenRecipients = false; @@ -67,11 +68,12 @@ public class EncryptTextFragment /** * Creates new instance of this fragment */ - public static EncryptTextFragment newInstance(String text) { + public static EncryptTextFragment newInstance(String text, boolean returnProcessTextAfterEncrypt) { EncryptTextFragment frag = new EncryptTextFragment(); Bundle args = new Bundle(); args.putString(ARG_TEXT, text); + args.putBoolean(ARG_RETURN_PROCESS_TEXT, returnProcessTextAfterEncrypt); frag.setArguments(args); return frag; @@ -129,6 +131,7 @@ public class EncryptTextFragment super.onCreate(savedInstanceState); if (savedInstanceState == null) { mMessage = getArguments().getString(ARG_TEXT); + mReturnProcessTextAfterEncrypt = getArguments().getBoolean(ARG_RETURN_PROCESS_TEXT, false); } Preferences prefs = Preferences.getPreferences(getActivity()); @@ -152,6 +155,12 @@ public class EncryptTextFragment inflater.inflate(R.menu.encrypt_text_fragment, menu); menu.findItem(R.id.check_enable_compression).setChecked(mUseCompression); + + if (mReturnProcessTextAfterEncrypt) { + menu.findItem(R.id.encrypt_paste).setVisible(true); + menu.findItem(R.id.encrypt_copy).setVisible(false); + menu.findItem(R.id.encrypt_share).setVisible(false); + } } @Override @@ -178,6 +187,11 @@ public class EncryptTextFragment cryptoOperation(new CryptoInputParcel(new Date())); break; } + case R.id.encrypt_paste: { + hideKeyboard(); + cryptoOperation(new CryptoInputParcel(new Date())); + break; + } default: { return super.onOptionsItemSelected(item); } @@ -289,26 +303,10 @@ public class EncryptTextFragment result.createNotify(activity).show(); } - /** - * Create Intent Chooser but exclude OK's EncryptActivity. - */ - private Intent sendWithChooserExcludingEncrypt(byte[] resultBytes) { - Intent prototype = createSendIntent(resultBytes); - String title = getString(R.string.title_share_message); - - // we don't want to encrypt the encrypted, no inception ;) - String[] blacklist = new String[]{ - Constants.PACKAGE_NAME + ".ui.EncryptTextActivity", - "org.thialfihar.android.apg.ui.EncryptActivity" - }; - - return new ShareHelper(getActivity()).createChooserExcluding(prototype, title, blacklist); - } - private Intent createSendIntent(byte[] resultBytes) { Intent sendIntent; sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.setType(Constants.ENCRYPTED_TEXT_MIME); + sendIntent.setType(Constants.MIME_TYPE_TEXT); sendIntent.putExtra(Intent.EXTRA_TEXT, new String(resultBytes)); EncryptActivity modeInterface = (EncryptActivity) getActivity(); @@ -343,7 +341,13 @@ public class EncryptTextFragment if (mShareAfterEncrypt) { // Share encrypted message/file - startActivity(sendWithChooserExcludingEncrypt(result.getResultBytes())); + startActivity(Intent.createChooser(createSendIntent(result.getResultBytes()), + getString(R.string.title_share_message))); + } else if (mReturnProcessTextAfterEncrypt) { + Intent resultIntent = new Intent(); + resultIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, new String(result.getResultBytes())); + getActivity().setResult(Activity.RESULT_OK, resultIntent); + getActivity().finish(); } else { // Copy to clipboard copyToClipboard(result); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java index 4ef6c40dc..7f7532ddf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -21,8 +21,8 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.os.Message; import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -33,10 +33,8 @@ import org.sufficientlysecure.keychain.intents.OpenKeychainIntents; import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; -import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; @@ -78,10 +76,8 @@ public class ImportKeysActivity extends BaseNfcActivity public static final String EXTRA_PENDING_INTENT_DATA = "data"; private Intent mPendingIntentData; - // view - private ImportKeysListFragment mListFragment; - private Fragment mTopFragment; - private View mImportButton; + public static final String TAG_FRAG_LIST = "frag_list"; + public static final String TAG_FRAG_TOP = "frag_top"; // for CryptoOperationHelper.Callback private String mKeyserver; @@ -94,15 +90,22 @@ public class ImportKeysActivity extends BaseNfcActivity super.onCreate(savedInstanceState); setFullScreenDialogClose(Activity.RESULT_CANCELED, true); - mImportButton = findViewById(R.id.import_import); - mImportButton.setOnClickListener(new OnClickListener() { + findViewById(R.id.import_import).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - importKeys(); + importSelectedKeys(); } }); - handleActions(savedInstanceState, getIntent()); + // only used for OpenPgpService + if (getIntent().hasExtra(EXTRA_PENDING_INTENT_DATA)) { + mPendingIntentData = getIntent().getParcelableExtra(EXTRA_PENDING_INTENT_DATA); + } + + // if we aren't being restored, initialize fragments + if (savedInstanceState == null) { + handleActions(getIntent()); + } } @Override @@ -110,7 +113,7 @@ public class ImportKeysActivity extends BaseNfcActivity setContentView(R.layout.import_keys_activity); } - protected void handleActions(Bundle savedInstanceState, Intent intent) { + protected void handleActions(Intent intent) { String action = intent.getAction(); Bundle extras = intent.getExtras(); Uri dataUri = intent.getData(); @@ -120,14 +123,8 @@ public class ImportKeysActivity extends BaseNfcActivity extras = new Bundle(); } - if (action == null) { - startCloudFragment(savedInstanceState, null, false, null); - startListFragment(savedInstanceState, null, null, null, null); - return; - } - if (Intent.ACTION_VIEW.equals(action)) { - if (scheme.equals("http") || scheme.equals("https")) { + if ("http".equals(scheme) || "https".equals(scheme)) { action = ACTION_SEARCH_KEYSERVER_FROM_URL; } else { // Android's Action when opening file associated to Keychain (see AndroidManifest.xml) @@ -135,20 +132,24 @@ public class ImportKeysActivity extends BaseNfcActivity action = ACTION_IMPORT_KEY; } } + if (action == null) { + // -> switch to default below + action = ""; + } switch (action) { case ACTION_IMPORT_KEY: { - /* Keychain's own Actions */ - startFileFragment(savedInstanceState); - if (dataUri != null) { // action: directly load data - startListFragment(savedInstanceState, null, dataUri, null, null); + startListFragment(null, dataUri, null, null); } else if (extras.containsKey(EXTRA_KEY_BYTES)) { byte[] importData = extras.getByteArray(EXTRA_KEY_BYTES); // action: directly load data - startListFragment(savedInstanceState, importData, null, null, null); + startListFragment(importData, null, null, null); + } else { + startTopFileFragment(); + startListFragment(null, null, null, null); } break; } @@ -156,10 +157,6 @@ public class ImportKeysActivity extends BaseNfcActivity case ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE: case ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT: { - // only used for OpenPgpService - if (extras.containsKey(EXTRA_PENDING_INTENT_DATA)) { - mPendingIntentData = extras.getParcelable(EXTRA_PENDING_INTENT_DATA); - } if (extras.containsKey(EXTRA_QUERY) || extras.containsKey(EXTRA_KEY_ID)) { /* simple search based on query or key id */ @@ -175,10 +172,10 @@ public class ImportKeysActivity extends BaseNfcActivity if (query != null && query.length() > 0) { // display keyserver fragment with query - startCloudFragment(savedInstanceState, query, false, null); + startTopCloudFragment(query, false, null); // action: search immediately - startListFragment(savedInstanceState, null, null, query, null); + startListFragment(null, null, query, null); } else { Log.e(Constants.TAG, "Query is empty!"); return; @@ -194,10 +191,10 @@ public class ImportKeysActivity extends BaseNfcActivity String query = "0x" + fingerprint; // display keyserver fragment with query - startCloudFragment(savedInstanceState, query, true, null); + startTopCloudFragment(query, true, null); // action: search immediately - startListFragment(savedInstanceState, null, null, query, null); + startListFragment(null, null, query, null); } } else { Log.e(Constants.TAG, @@ -208,14 +205,6 @@ public class ImportKeysActivity extends BaseNfcActivity } break; } - case ACTION_IMPORT_KEY_FROM_FILE: { - // NOTE: this only displays the appropriate fragment, no actions are taken - startFileFragment(savedInstanceState); - - // no immediate actions! - startListFragment(savedInstanceState, null, null, null, null); - break; - } case ACTION_SEARCH_KEYSERVER_FROM_URL: { // need to process URL to get search query and keyserver authority String query = dataUri.getQueryParameter("search"); @@ -223,120 +212,88 @@ public class ImportKeysActivity extends BaseNfcActivity // if query not specified, we still allow users to search the keyserver in the link if (query == null) { Notify.create(this, R.string.import_url_warn_no_search_parameter, Notify.LENGTH_INDEFINITE, - Notify.Style.WARN).show(mTopFragment); + Notify.Style.WARN).show(); // we just set the keyserver - startCloudFragment(savedInstanceState, null, false, keyserver); + startTopCloudFragment(null, false, keyserver); // we don't set the keyserver for ImportKeysListFragment since // it'll be set in the cloudSearchPrefs of ImportKeysCloudFragment // which is used when the user clicks on the search button - startListFragment(savedInstanceState, null, null, null, null); + startListFragment(null, null, null, null); } else { // we allow our users to edit the query if they wish - startCloudFragment(savedInstanceState, query, false, keyserver); + startTopCloudFragment(query, false, keyserver); // search immediately - startListFragment(savedInstanceState, null, null, query, keyserver); + startListFragment(null, null, query, keyserver); } break; } + case ACTION_IMPORT_KEY_FROM_FILE: case ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN: { // NOTE: this only displays the appropriate fragment, no actions are taken - startFileFragment(savedInstanceState); - - // no immediate actions! - startListFragment(savedInstanceState, null, null, null, null); + startTopFileFragment(); + startListFragment(null, null, null, null); break; } default: { - startCloudFragment(savedInstanceState, null, false, null); - startListFragment(savedInstanceState, null, null, null, null); + startTopCloudFragment(null, false, null); + startListFragment(null, null, null, null); break; } } } + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + // the only thing we need to take care of for restoring state is + // that the top layout is shown iff it contains a fragment + Fragment topFragment = getSupportFragmentManager().findFragmentByTag(TAG_FRAG_TOP); + boolean hasTopFragment = topFragment != null; + findViewById(R.id.import_keys_top_layout).setVisibility(hasTopFragment ? View.VISIBLE : View.GONE); + } /** * if the fragment is started with non-null bytes/dataUri/serverQuery, it will immediately * load content * - * @param savedInstanceState * @param bytes bytes containing list of keyrings to import * @param dataUri uri to file to import keyrings from * @param serverQuery query to search for on the keyserver * @param keyserver keyserver authority to search on. If null will use keyserver from * user preferences */ - private void startListFragment(Bundle savedInstanceState, byte[] bytes, Uri dataUri, - String serverQuery, String keyserver) { - // However, if we're being restored from a previous state, - // then we don't need to do anything and should return or else - // we could end up with overlapping fragments. - if (mListFragment != null) { - return; - } - - mListFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false, - keyserver); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + private void startListFragment(byte[] bytes, Uri dataUri, String serverQuery, String keyserver) { + Fragment listFragment = + ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false, keyserver); getSupportFragmentManager().beginTransaction() - .replace(R.id.import_keys_list_container, mListFragment) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); + .replace(R.id.import_keys_list_container, listFragment, TAG_FRAG_LIST) + .commit(); } - private void startFileFragment(Bundle savedInstanceState) { - // However, if we're being restored from a previous state, - // then we don't need to do anything and should return or else - // we could end up with overlapping fragments. - if (mTopFragment != null) { - return; - } - - // Create an instance of the fragment - mTopFragment = ImportKeysFileFragment.newInstance(); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + private void startTopFileFragment() { + findViewById(R.id.import_keys_top_layout).setVisibility(View.VISIBLE); + Fragment importFileFragment = ImportKeysFileFragment.newInstance(); getSupportFragmentManager().beginTransaction() - .replace(R.id.import_keys_top_container, mTopFragment) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); + .replace(R.id.import_keys_top_container, importFileFragment, TAG_FRAG_TOP) + .commit(); } /** * loads the CloudFragment, which consists of the search bar, search button and settings icon * visually. * - * @param savedInstanceState * @param query search query * @param disableQueryEdit if true, user will not be able to edit the search query * @param keyserver keyserver authority to use for search, if null will use keyserver * specified in user preferences */ - - private void startCloudFragment(Bundle savedInstanceState, String query, boolean disableQueryEdit, String - keyserver) { - // However, if we're being restored from a previous state, - // then we don't need to do anything and should return or else - // we could end up with overlapping fragments. - if (mTopFragment != null) { - return; - } - - // Create an instance of the fragment - mTopFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit, keyserver); - - // Add the fragment to the 'fragment_container' FrameLayout - // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + private void startTopCloudFragment(String query, boolean disableQueryEdit, String keyserver) { + findViewById(R.id.import_keys_top_layout).setVisibility(View.VISIBLE); + Fragment importCloudFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit, keyserver); getSupportFragmentManager().beginTransaction() - .replace(R.id.import_keys_top_container, mTopFragment) - .commitAllowingStateLoss(); - // do it immediately! - getSupportFragmentManager().executePendingTransactions(); + .replace(R.id.import_keys_top_container, importCloudFragment, TAG_FRAG_TOP) + .commit(); } private boolean isFingerprintValid(String fingerprint) { @@ -350,63 +307,32 @@ public class ImportKeysActivity extends BaseNfcActivity } public void loadCallback(final ImportKeysListFragment.LoaderState loaderState) { - mListFragment.loadNew(loaderState); + FragmentManager fragMan = getSupportFragmentManager(); + ImportKeysListFragment keyListFragment = (ImportKeysListFragment) fragMan.findFragmentByTag(TAG_FRAG_LIST); + keyListFragment.loadNew(loaderState); } - private void handleMessage(Message message) { - if (message.arg1 == ServiceProgressHandler.MessageStatus.OKAY.ordinal()) { - // get returned data bundle - Bundle returnData = message.getData(); - if (returnData == null) { - return; - } - final ImportKeyResult result = - returnData.getParcelable(OperationResult.EXTRA_RESULT); - if (result == null) { - Log.e(Constants.TAG, "result == null"); - return; - } + private void importSelectedKeys() { - if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(getIntent().getAction()) - || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(getIntent().getAction())) { - Intent intent = new Intent(); - intent.putExtra(ImportKeyResult.EXTRA_RESULT, result); - ImportKeysActivity.this.setResult(RESULT_OK, intent); - ImportKeysActivity.this.finish(); - return; - } - if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(getIntent().getAction())) { - ImportKeysActivity.this.setResult(RESULT_OK, mPendingIntentData); - ImportKeysActivity.this.finish(); - return; - } - - result.createNotify(ImportKeysActivity.this) - .show((ViewGroup) findViewById(R.id.import_snackbar)); - } - } + FragmentManager fragMan = getSupportFragmentManager(); + ImportKeysListFragment keyListFragment = (ImportKeysListFragment) fragMan.findFragmentByTag(TAG_FRAG_LIST); - /** - * Import keys with mImportData - */ - public void importKeys() { - - if (mListFragment.getSelectedEntries().size() == 0) { + if (keyListFragment.getSelectedEntries().size() == 0) { Notify.create(this, R.string.error_nothing_import_selected, Notify.Style.ERROR) .show((ViewGroup) findViewById(R.id.import_snackbar)); return; } - mOperationHelper = new CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult>( + mOperationHelper = new CryptoOperationHelper<>( 1, this, this, R.string.progress_importing ); - ImportKeysListFragment.LoaderState ls = mListFragment.getLoaderState(); + ImportKeysListFragment.LoaderState ls = keyListFragment.getLoaderState(); if (ls instanceof ImportKeysListFragment.BytesLoaderState) { Log.d(Constants.TAG, "importKeys started"); // get DATA from selected key entries - IteratorWithSize<ParcelableKeyRing> selectedEntries = mListFragment.getSelectedData(); + IteratorWithSize<ParcelableKeyRing> selectedEntries = keyListFragment.getSelectedData(); // instead of giving the entries by Intent extra, cache them into a // file to prevent Java Binder problems on heavy imports @@ -435,7 +361,7 @@ public class ImportKeysActivity extends BaseNfcActivity ArrayList<ParcelableKeyRing> keys = new ArrayList<>(); { // change the format into ParcelableKeyRing - ArrayList<ImportKeysListEntry> entries = mListFragment.getSelectedEntries(); + ArrayList<ImportKeysListEntry> entries = keyListFragment.getSelectedEntries(); for (ImportKeysListEntry entry : entries) { keys.add(new ParcelableKeyRing( entry.getFingerprintHex(), entry.getKeyIdHex(), entry.getExtraData()) @@ -451,31 +377,35 @@ public class ImportKeysActivity extends BaseNfcActivity } @Override - protected void onNfcPostExecute() throws IOException { + protected void onNfcPostExecute() { // either way, finish after NFC AsyncTask finish(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (mOperationHelper == null || - !mOperationHelper.handleActivityResult(requestCode, resultCode, data)) { - super.onActivityResult(requestCode, resultCode, data); + if (mOperationHelper != null && + mOperationHelper.handleActivityResult(requestCode, resultCode, data)) { + return; } + super.onActivityResult(requestCode, resultCode, data); } public void handleResult(ImportKeyResult result) { - if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(getIntent().getAction()) - || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(getIntent().getAction())) { + String intentAction = getIntent().getAction(); + + if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_RESULT.equals(intentAction) + || ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN.equals(intentAction)) { Intent intent = new Intent(); intent.putExtra(ImportKeyResult.EXTRA_RESULT, result); - ImportKeysActivity.this.setResult(RESULT_OK, intent); - ImportKeysActivity.this.finish(); + setResult(RESULT_OK, intent); + finish(); return; } - if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(getIntent().getAction())) { - ImportKeysActivity.this.setResult(RESULT_OK, mPendingIntentData); - ImportKeysActivity.this.finish(); + + if (ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN_TO_SERVICE.equals(intentAction)) { + setResult(RESULT_OK, mPendingIntentData); + finish(); return; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java index 746c75600..8de60dfd3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java @@ -29,6 +29,9 @@ import android.view.ViewGroup; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.pgp.PgpHelper; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.FileHelper; public class ImportKeysFileFragment extends Fragment { @@ -78,12 +81,16 @@ public class ImportKeysFileFragment extends Fragment { String sendText = ""; if (clipboardText != null) { sendText = clipboardText.toString(); + sendText = PgpHelper.getPgpKeyContent(sendText); + if (sendText == null) { + Notify.create(mImportActivity, "Bad data!", Style.ERROR).show(); + return; + } mImportActivity.loadCallback(new ImportKeysListFragment.BytesLoaderState(sendText.getBytes(), null)); } } }); - return view; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java index 8502798cd..7aed3176c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java @@ -17,6 +17,11 @@ package org.sufficientlysecure.keychain.ui; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + import android.app.Activity; import android.net.Uri; import android.os.Bundle; @@ -40,21 +45,12 @@ import org.sufficientlysecure.keychain.ui.adapter.AsyncTaskResultWrapper; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListCloudLoader; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListLoader; -import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; -import java.io.ByteArrayInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - public class ImportKeysListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>> { @@ -179,8 +175,8 @@ public class ImportKeysListFragment extends ListFragment implements } static public class BytesLoaderState extends LoaderState { - byte[] mKeyBytes; - Uri mDataUri; + public byte[] mKeyBytes; + public Uri mDataUri; BytesLoaderState(byte[] keyBytes, Uri dataUri) { mKeyBytes = keyBytes; @@ -304,9 +300,7 @@ public class ImportKeysListFragment extends ListFragment implements onCreateLoader(int id, Bundle args) { switch (id) { case LOADER_ID_BYTES: { - BytesLoaderState ls = (BytesLoaderState) mLoaderState; - InputData inputData = getInputData(ls.mKeyBytes, ls.mDataUri); - return new ImportKeysListLoader(mActivity, inputData); + return new ImportKeysListLoader(mActivity, (BytesLoaderState) mLoaderState); } case LOADER_ID_CLOUD: { CloudLoaderState ls = (CloudLoaderState) mLoaderState; @@ -431,24 +425,4 @@ public class ImportKeysListFragment extends ListFragment implements } } - private InputData getInputData(byte[] importBytes, Uri dataUri) { - InputData inputData = null; - if (importBytes != null) { - inputData = new InputData(new ByteArrayInputStream(importBytes), importBytes.length); - } else if (dataUri != null) { - try { - InputStream inputStream = getActivity().getContentResolver().openInputStream(dataUri); - int length = inputStream.available(); - - inputData = new InputData(inputStream, length); - } catch (FileNotFoundException e) { - Log.e(Constants.TAG, "FileNotFoundException!", e); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException!", e); - } - } - - return inputData; - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java index b60f3984c..45ce604c3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysProxyActivity.java @@ -87,12 +87,7 @@ public class ImportKeysProxyActivity extends FragmentActivity processScannedContent(dataUri); } else if (ACTION_SCAN_WITH_RESULT.equals(action) || ACTION_SCAN_IMPORT.equals(action) || ACTION_QR_CODE_API.equals(action)) { - IntentIntegrator integrator = new IntentIntegrator(this); - integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES) - .setPrompt(getString(R.string.import_qr_code_text)) - .setResultDisplayDuration(0); - integrator.setOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - integrator.initiateScan(); + new IntentIntegrator(this).setCaptureActivity(QrCodeCaptureActivity.class).initiateScan(); } else if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) { // Check to see if the Activity started due to an Android Beam if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 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 2b6d786d4..db31bd0a1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -30,6 +30,7 @@ import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; @@ -46,14 +47,17 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AdapterView; +import android.widget.Button; import android.widget.ListView; import android.widget.TextView; +import android.widget.ViewAnimator; import com.getbase.floatingactionbutton.FloatingActionButton; import com.getbase.floatingactionbutton.FloatingActionsMenu; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.results.BenchmarkResult; import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; @@ -61,6 +65,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.BenchmarkInputParcel; import org.sufficientlysecure.keychain.service.ConsolidateInputParcel; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; @@ -98,6 +103,8 @@ public class KeyListFragment extends LoaderFragment // saves the mode object for multiselect, needed for reset at some point private ActionMode mActionMode = null; + private Button vSearchButton; + private ViewAnimator vSearchContainer; private String mQuery; private FloatingActionsMenu mFab; @@ -162,7 +169,9 @@ public class KeyListFragment extends LoaderFragment super.onActivityCreated(savedInstanceState); // show app name instead of "keys" from nav drawer - getActivity().setTitle(R.string.app_name); + final FragmentActivity activity = getActivity(); + + activity.setTitle(R.string.app_name); mStickyList.setOnItemClickListener(this); mStickyList.setAreHeadersSticky(true); @@ -171,7 +180,7 @@ public class KeyListFragment extends LoaderFragment // Adds an empty footer view so that the Floating Action Button won't block content // in last few rows. - View footer = new View(getActivity()); + View footer = new View(activity); int spacing = (int) android.util.TypedValue.applyDimension( android.util.TypedValue.COMPLEX_UNIT_DIP, 72, getResources().getDisplayMetrics() @@ -195,7 +204,7 @@ public class KeyListFragment extends LoaderFragment @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { - android.view.MenuInflater inflater = getActivity().getMenuInflater(); + android.view.MenuInflater inflater = activity.getMenuInflater(); inflater.inflate(R.menu.key_list_multi, menu); mActionMode = mode; return true; @@ -235,7 +244,7 @@ public class KeyListFragment extends LoaderFragment @Override public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { + boolean checked) { if (checked) { mAdapter.setNewSelection(position, true); } else { @@ -255,8 +264,21 @@ public class KeyListFragment extends LoaderFragment // Start out with a progress indicator. setContentShown(false); + // this view is made visible if no data is available + mStickyList.setEmptyView(activity.findViewById(R.id.key_list_empty)); + + // click on search button (in empty view) starts query for search string + vSearchContainer = (ViewAnimator) activity.findViewById(R.id.search_container); + vSearchButton = (Button) activity.findViewById(R.id.search_button); + vSearchButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + startSearchForQuery(); + } + }); + // Create an empty adapter we will use to display the loaded data. - mAdapter = new KeyListAdapter(getActivity(), null, 0); + mAdapter = new KeyListAdapter(activity, null, 0); mStickyList.setAdapter(mAdapter); // Prepare the loader. Either re-connect with an existing one, @@ -264,8 +286,20 @@ public class KeyListFragment extends LoaderFragment getLoaderManager().initLoader(0, null, this); } + private void startSearchForQuery() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + Intent searchIntent = new Intent(activity, ImportKeysActivity.class); + searchIntent.putExtra(ImportKeysActivity.EXTRA_QUERY, mQuery); + searchIntent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_KEYSERVER); + startActivity(searchIntent); + } + static final String ORDER = - KeyRings.HAS_ANY_SECRET + " DESC, UPPER(" + KeyRings.USER_ID + ") ASC"; + KeyRings.HAS_ANY_SECRET + " DESC, " + KeyRings.USER_ID + " COLLATE NOCASE ASC"; @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { @@ -319,9 +353,6 @@ public class KeyListFragment extends LoaderFragment mStickyList.setAdapter(mAdapter); - // this view is made visible if no data is available - mStickyList.setEmptyView(getActivity().findViewById(R.id.key_list_empty)); - // end action mode, if any if (mActionMode != null) { mActionMode.finish(); @@ -387,6 +418,7 @@ public class KeyListFragment extends LoaderFragment if (Constants.DEBUG) { menu.findItem(R.id.menu_key_list_debug_cons).setVisible(true); + menu.findItem(R.id.menu_key_list_debug_bench).setVisible(true); menu.findItem(R.id.menu_key_list_debug_read).setVisible(true); menu.findItem(R.id.menu_key_list_debug_write).setVisible(true); menu.findItem(R.id.menu_key_list_debug_first_time).setVisible(true); @@ -470,6 +502,10 @@ public class KeyListFragment extends LoaderFragment consolidate(); return true; + case R.id.menu_key_list_debug_bench: + benchmark(); + return true; + default: return super.onOptionsItemSelected(item); } @@ -483,17 +519,25 @@ public class KeyListFragment extends LoaderFragment @Override public boolean onQueryTextChange(String s) { Log.d(Constants.TAG, "onQueryTextChange s:" + s); - // Called when the action bar search text has changed. Update - // the search filter, and restart the loader to do a new query - // with this filter. + // Called when the action bar search text has changed. Update the + // search filter, and restart the loader to do a new query with this + // filter. // If the nav drawer is opened, onQueryTextChange("") is executed. // This hack prevents restarting the loader. - // TODO: better way to fix this? - String tmp = (mQuery == null) ? "" : mQuery; - if (!s.equals(tmp)) { + if (!s.equals(mQuery)) { mQuery = s; getLoaderManager().restartLoader(0, null, this); } + + if (s.length() > 2) { + vSearchButton.setText(getString(R.string.btn_search_for_query, mQuery)); + vSearchContainer.setDisplayedChild(1); + vSearchContainer.setVisibility(View.VISIBLE); + } else { + vSearchContainer.setDisplayedChild(0); + vSearchContainer.setVisibility(View.GONE); + } + return true; } @@ -560,8 +604,8 @@ public class KeyListFragment extends LoaderFragment mKeyserver = cloudPrefs.keyserver; } - mImportOpHelper = new CryptoOperationHelper<>(1, this, - this, R.string.progress_updating); + mImportOpHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_updating); + mImportOpHelper.setProgressCancellable(true); mImportOpHelper.cryptoOperation(); } @@ -602,6 +646,43 @@ public class KeyListFragment extends LoaderFragment mConsolidateOpHelper.cryptoOperation(); } + private void benchmark() { + + CryptoOperationHelper.Callback<BenchmarkInputParcel, BenchmarkResult> callback + = new CryptoOperationHelper.Callback<BenchmarkInputParcel, BenchmarkResult>() { + + @Override + public BenchmarkInputParcel createOperationInput() { + return new BenchmarkInputParcel(); // we want to perform a full consolidate + } + + @Override + public void onCryptoOperationSuccess(BenchmarkResult result) { + result.createNotify(getActivity()).show(); + } + + @Override + public void onCryptoOperationCancelled() { + + } + + @Override + public void onCryptoOperationError(BenchmarkResult result) { + result.createNotify(getActivity()).show(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return false; + } + }; + + CryptoOperationHelper opHelper = + new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing); + + opHelper.cryptoOperation(); + } + @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (mImportOpHelper != null) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java index 43c8d2643..411dac3ef 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java @@ -43,7 +43,8 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogEntryParcel; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogLevel; import org.sufficientlysecure.keychain.operations.results.OperationResult.SubLogEntryParcel; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; +import org.sufficientlysecure.keychain.ui.dialog.ShareLogDialogFragment; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; @@ -139,7 +140,7 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe // if there is no log temp file yet, create one if (mLogTempFile == null) { - mLogTempFile = TemporaryStorageProvider.createFile(getActivity(), "openkeychain_log.txt", "text/plain"); + mLogTempFile = TemporaryFileProvider.createFile(getActivity(), "openkeychain_log.txt", "text/plain"); try { OutputStream outputStream = activity.getContentResolver().openOutputStream(mLogTempFile); outputStream.write(log.getBytes()); @@ -149,11 +150,9 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe } } - Intent intent = new Intent(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_STREAM, mLogTempFile); - intent.setType("text/plain"); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startActivity(intent); + + ShareLogDialogFragment shareLogDialog = ShareLogDialogFragment.newInstance(mLogTempFile); + shareLogDialog.show(getActivity().getSupportFragmentManager(), "shareLogDialog"); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java index 6f5d98afd..7bd7bafcc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/MainActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2012-2015 Dominik Schürmann <dominik@dominikschuermann.de> * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> * Copyright (C) 2015 Kai Jiang <jiangkai@gmail.com> * @@ -27,13 +27,13 @@ import android.support.v4.app.FragmentManager.OnBackStackChangedListener; import android.support.v4.app.FragmentTransaction; import android.support.v7.widget.Toolbar; import android.view.View; -import android.widget.AdapterView; import com.mikepenz.community_material_typeface_library.CommunityMaterial; +import com.mikepenz.fontawesome_typeface_library.FontAwesome; import com.mikepenz.google_material_typeface_library.GoogleMaterial; -import com.mikepenz.iconics.typeface.FontAwesome; import com.mikepenz.materialdrawer.Drawer; import com.mikepenz.materialdrawer.DrawerBuilder; +import com.mikepenz.materialdrawer.model.DividerDrawerItem; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; @@ -75,25 +75,23 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac .withToolbar(mToolbar) .addDrawerItems( new PrimaryDrawerItem().withName(R.string.nav_keys).withIcon(CommunityMaterial.Icon.cmd_key) - .withIdentifier(ID_KEYS).withCheckable(false), + .withIdentifier(ID_KEYS).withSelectable(false), new PrimaryDrawerItem().withName(R.string.nav_encrypt_decrypt).withIcon(FontAwesome.Icon.faw_lock) - .withIdentifier(ID_ENCRYPT_DECRYPT).withCheckable(false), + .withIdentifier(ID_ENCRYPT_DECRYPT).withSelectable(false), new PrimaryDrawerItem().withName(R.string.title_api_registered_apps).withIcon(CommunityMaterial.Icon.cmd_apps) - .withIdentifier(ID_APPS).withCheckable(false), + .withIdentifier(ID_APPS).withSelectable(false), new PrimaryDrawerItem().withName(R.string.nav_backup).withIcon(CommunityMaterial.Icon.cmd_backup_restore) - .withIdentifier(ID_BACKUP).withCheckable(false) - ) - .addStickyDrawerItems( - // display and stick on bottom of drawer - new PrimaryDrawerItem().withName(R.string.menu_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(ID_SETTINGS).withCheckable(false), - new PrimaryDrawerItem().withName(R.string.menu_help).withIcon(CommunityMaterial.Icon.cmd_help_circle).withIdentifier(ID_HELP).withCheckable(false) + .withIdentifier(ID_BACKUP).withSelectable(false), + new DividerDrawerItem(), + new PrimaryDrawerItem().withName(R.string.menu_preferences).withIcon(GoogleMaterial.Icon.gmd_settings).withIdentifier(ID_SETTINGS).withSelectable(false), + new PrimaryDrawerItem().withName(R.string.menu_help).withIcon(CommunityMaterial.Icon.cmd_help_circle).withIdentifier(ID_HELP).withSelectable(false) ) .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { @Override - public boolean onItemClick(AdapterView<?> parent, View view, int position, long id, IDrawerItem drawerItem) { + public boolean onItemClick(View view, int position, IDrawerItem drawerItem) { if (drawerItem != null) { Intent intent = null; - switch(drawerItem.getIdentifier()) { + switch (drawerItem.getIdentifier()) { case ID_KEYS: onKeysSelected(); break; @@ -182,29 +180,29 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac private void onKeysSelected() { mToolbar.setTitle(R.string.app_name); - mDrawer.setSelectionByIdentifier(ID_KEYS, false); + mDrawer.setSelection(ID_KEYS, false); Fragment frag = new KeyListFragment(); setFragment(frag, false); } private void onEnDecryptSelected() { mToolbar.setTitle(R.string.nav_encrypt_decrypt); - mDrawer.setSelectionByIdentifier(ID_ENCRYPT_DECRYPT, false); - Fragment frag = new EncryptDecryptOverviewFragment(); + mDrawer.setSelection(ID_ENCRYPT_DECRYPT, false); + Fragment frag = new EncryptDecryptFragment(); setFragment(frag, true); } private void onAppsSelected() { mToolbar.setTitle(R.string.nav_apps); - mDrawer.setSelectionByIdentifier(ID_APPS, false); + mDrawer.setSelection(ID_APPS, false); Fragment frag = new AppsListFragment(); setFragment(frag, true); } private void onBackupSelected() { mToolbar.setTitle(R.string.nav_backup); - mDrawer.setSelectionByIdentifier(ID_APPS, false); - Fragment frag = new BackupFragment(); + mDrawer.setSelection(ID_BACKUP, false); + Fragment frag = new BackupRestoreFragment(); setFragment(frag, true); } @@ -258,16 +256,16 @@ public class MainActivity extends BaseNfcActivity implements FabContainer, OnBac // make sure the selected icon is the one shown at this point if (frag instanceof KeyListFragment) { mToolbar.setTitle(R.string.app_name); - mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_KEYS), false); - } else if (frag instanceof EncryptDecryptOverviewFragment) { + mDrawer.setSelection(mDrawer.getPosition(ID_KEYS), false); + } else if (frag instanceof EncryptDecryptFragment) { mToolbar.setTitle(R.string.nav_encrypt_decrypt); - mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_ENCRYPT_DECRYPT), false); + mDrawer.setSelection(mDrawer.getPosition(ID_ENCRYPT_DECRYPT), false); } else if (frag instanceof AppsListFragment) { mToolbar.setTitle(R.string.nav_apps); - mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_APPS), false); - } else if (frag instanceof BackupFragment) { + mDrawer.setSelection(mDrawer.getPosition(ID_APPS), false); + } else if (frag instanceof BackupRestoreFragment) { mToolbar.setTitle(R.string.nav_backup); - mDrawer.setSelection(mDrawer.getPositionFromIdentifier(ID_BACKUP), false); + mDrawer.setSelection(mDrawer.getPosition(ID_BACKUP), false); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java index b811b218e..86b0a36d0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java @@ -27,6 +27,7 @@ import android.view.View; import android.view.WindowManager; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.Constants; @@ -72,7 +73,7 @@ public class NfcOperationActivity extends BaseNfcActivity { private RequiredInputParcel mRequiredInput; private Intent mServiceIntent; - 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 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; @@ -91,7 +92,9 @@ public class NfcOperationActivity extends BaseNfcActivity { // prevent annoying orientation changes while fumbling with the device OrientationUtils.lockOrientation(this); - + // prevent close when touching outside of the dialog (happens easily when fumbling with the device) + setFinishOnTouchOutside(false); + // keep screen on getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); mInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT); @@ -107,13 +110,18 @@ public class NfcOperationActivity extends BaseNfcActivity { public void onClick(View v) { resumeTagHandling(); - // obtain passphrase for this subkey - if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD) { - obtainYubiKeyPin(mRequiredInput); - } + obtainPassphraseIfRequired(); vAnimator.setDisplayedChild(0); } }); + Button vCancel = (Button) findViewById(R.id.nfc_activity_0_cancel); + vCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); Intent intent = getIntent(); Bundle data = intent.getExtras(); @@ -121,8 +129,13 @@ public class NfcOperationActivity extends BaseNfcActivity { mRequiredInput = data.getParcelable(EXTRA_REQUIRED_INPUT); mServiceIntent = data.getParcelable(EXTRA_SERVICE_INTENT); + obtainPassphraseIfRequired(); + } + + private void obtainPassphraseIfRequired() { // obtain passphrase for this subkey - if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD) { + if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD + && mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_RESET_CARD) { obtainYubiKeyPin(mRequiredInput); } } @@ -237,6 +250,11 @@ public class NfcOperationActivity extends BaseNfcActivity { break; } + case NFC_RESET_CARD: { + nfcResetCard(); + + break; + } default: { throw new AssertionError("Unhandled mRequiredInput.mType"); } @@ -245,7 +263,7 @@ public class NfcOperationActivity extends BaseNfcActivity { } @Override - protected void onNfcPostExecute() throws IOException { + protected void onNfcPostExecute() { if (mServiceIntent != null) { // if we're triggered by OpenPgpService // save updated cryptoInputParcel in cache @@ -276,6 +294,7 @@ public class NfcOperationActivity extends BaseNfcActivity { } } } + @Override protected void onPostExecute(Void result) { super.onPostExecute(result); @@ -292,11 +311,27 @@ public class NfcOperationActivity extends BaseNfcActivity { vAnimator.setDisplayedChild(3); } + @Override + public void onNfcPinError(String error) { + onNfcError(error); + + // clear (invalid) passphrase + PassphraseCacheService.clearCachedPassphrase( + this, mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId()); + } + private boolean shouldPutKey(byte[] fingerprint, int idx) throws IOException { - byte[] cardFingerprint = nfcGetFingerprint(idx); + byte[] cardFingerprint = nfcGetMasterKeyFingerprint(idx); + + // Note: special case: This should not happen, but happens with + // https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true + if (cardFingerprint == null) { + return true; + } + // Slot is empty, or contains this key already. PUT KEY operation is safe if (Arrays.equals(cardFingerprint, BLANK_FINGERPRINT) || - Arrays.equals(cardFingerprint, fingerprint)) { + Arrays.equals(cardFingerprint, fingerprint)) { return true; } @@ -304,21 +339,4 @@ public class NfcOperationActivity extends BaseNfcActivity { return false; } - @Override - public void handlePinError() { - - // avoid a loop - Preferences prefs = Preferences.getPreferences(this); - if (prefs.useDefaultYubiKeyPin()) { - toast(getString(R.string.error_pin_nodefault)); - setResult(RESULT_CANCELED); - finish(); - return; - } - - // clear (invalid) passphrase - PassphraseCacheService.clearCachedPassphrase( - this, mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId()); - } - } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java index e71349880..c3a33fc92 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java @@ -29,7 +29,9 @@ import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentActivity; import android.support.v7.app.AlertDialog; +import android.text.Editable; import android.text.InputType; +import android.text.TextWatcher; import android.text.method.PasswordTransformationMethod; import android.view.ContextThemeWrapper; import android.view.KeyEvent; @@ -117,6 +119,10 @@ public class PassphraseDialogActivity extends FragmentActivity { mSubKeyId = Constants.key.symmetric; break; } + case BACKUP_CODE: { + mSubKeyId = Constants.key.backup_code; + break; + } case PASSPHRASE: { // handle empty passphrases by directly returning an empty crypto input parcel @@ -186,6 +192,7 @@ public class PassphraseDialogActivity extends FragmentActivity { private EditText mPassphraseEditText; private TextView mPassphraseText; private View mInput, mProgress; + private EditText[] mBackupCodeEditText; private CanonicalizedSecretKeyRing mSecretRing = null; private boolean mIsCancelled = false; @@ -208,6 +215,24 @@ public class PassphraseDialogActivity extends FragmentActivity { // No title, see http://www.google.com/design/spec/components/dialogs.html#dialogs-alerts //alert.setTitle() + if (mSubKeyId == Constants.key.backup_code) { + LayoutInflater inflater = LayoutInflater.from(theme); + View view = inflater.inflate(R.layout.passphrase_dialog_backup_code, null); + alert.setView(view); + + mBackupCodeEditText = new EditText[4]; + mBackupCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1); + mBackupCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2); + mBackupCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3); + mBackupCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4); + setupEditTextFocusNext(mBackupCodeEditText); + + AlertDialog dialog = alert.create(); + dialog.setButton(DialogInterface.BUTTON_POSITIVE, + activity.getString(R.string.btn_unlock), (DialogInterface.OnClickListener) null); + return dialog; + } + LayoutInflater inflater = LayoutInflater.from(theme); View view = inflater.inflate(R.layout.passphrase_dialog, null); alert.setView(view); @@ -229,8 +254,10 @@ public class PassphraseDialogActivity extends FragmentActivity { CanonicalizedSecretKey.SecretKeyType keyType = CanonicalizedSecretKey.SecretKeyType.PASSPHRASE; String message; + String hint; if (mSubKeyId == Constants.key.symmetric || mSubKeyId == Constants.key.none) { message = getString(R.string.passphrase_for_symmetric_encryption); + hint = getString(R.string.label_passphrase); } else { try { ProviderHelper helper = new ProviderHelper(activity); @@ -255,12 +282,15 @@ public class PassphraseDialogActivity extends FragmentActivity { switch (keyType) { case PASSPHRASE: message = getString(R.string.passphrase_for, userId); + hint = getString(R.string.label_passphrase); break; case PIN: message = getString(R.string.pin_for, userId); + hint = getString(R.string.label_pin); break; case DIVERT_TO_CARD: message = getString(R.string.yubikey_pin_for, userId); + hint = getString(R.string.label_pin); break; // special case: empty passphrase just returns the empty passphrase case PASSPHRASE_EMPTY: @@ -283,6 +313,7 @@ public class PassphraseDialogActivity extends FragmentActivity { } mPassphraseText.setText(message); + mPassphraseEditText.setHint(hint); // Hack to open keyboard. // This is the only method that I found to work across all Android versions @@ -327,6 +358,34 @@ public class PassphraseDialogActivity extends FragmentActivity { return dialog; } + 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) == 6; + + if (inserting && cursorAtEnd) { + backupCodes[next].requestFocus(); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + } + } + @Override public void onStart() { super.onStart(); @@ -336,7 +395,23 @@ public class PassphraseDialogActivity extends FragmentActivity { positive.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - final Passphrase passphrase = new Passphrase(mPassphraseEditText); + + final Passphrase passphrase; + if (mSubKeyId == Constants.key.backup_code) { + StringBuilder backupCodeInput = new StringBuilder(26); + for (EditText editText : mBackupCodeEditText) { + if (editText.getText().length() < 6) { + return; + } + backupCodeInput.append(editText.getText()); + backupCodeInput.append('-'); + } + backupCodeInput.deleteCharAt(backupCodeInput.length() - 1); + + passphrase = new Passphrase(backupCodeInput.toString()); + } else { + passphrase = new Passphrase(mPassphraseEditText); + } CryptoInputParcel cryptoInputParcel = ((PassphraseDialogActivity) getActivity()).mCryptoInputParcel; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeCaptureActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeCaptureActivity.java new file mode 100644 index 000000000..b5d3948be --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/QrCodeCaptureActivity.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015 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.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.FragmentActivity; +import android.support.v4.content.ContextCompat; +import android.view.KeyEvent; + +import com.journeyapps.barcodescanner.CaptureManager; +import com.journeyapps.barcodescanner.CompoundBarcodeView; + +import org.sufficientlysecure.keychain.R; + +public class QrCodeCaptureActivity extends FragmentActivity { + private CaptureManager capture; + private CompoundBarcodeView barcodeScannerView; + + public static final int MY_PERMISSIONS_REQUEST_CAMERA = 42; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.qr_code_capture_activity); + + barcodeScannerView = (CompoundBarcodeView) findViewById(R.id.zxing_barcode_scanner); + barcodeScannerView.setStatusText(getString(R.string.import_qr_code_text)); + + if (savedInstanceState != null) { + init(barcodeScannerView, getIntent(), savedInstanceState); + } + + // check Android 6 permission + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + init(barcodeScannerView, getIntent(), null); + } else { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, + MY_PERMISSIONS_REQUEST_CAMERA); + } + } + + private void init(CompoundBarcodeView barcodeScannerView, Intent intent, Bundle savedInstanceState) { + capture = new CaptureManager(this, barcodeScannerView); + capture.initializeFromIntent(intent, savedInstanceState); + capture.decode(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], + @NonNull int[] grantResults) { + switch (requestCode) { + case MY_PERMISSIONS_REQUEST_CAMERA: { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // permission was granted + init(barcodeScannerView, getIntent(), null); + } else { + setResult(Activity.RESULT_CANCELED); + finish(); + } + } + } + } + + @Override + protected void onResume() { + super.onResume(); + if (capture != null) { + capture.onResume(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (capture != null) { + capture.onPause(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (capture != null) { + capture.onDestroy(); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (capture != null) { + capture.onSaveInstanceState(outState); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); + } +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java index eb9ee05af..f5c239558 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java @@ -54,14 +54,8 @@ import java.util.List; public class SettingsActivity extends AppCompatPreferenceActivity { - public static final String ACTION_PREFS_CLOUD = "org.sufficientlysecure.keychain.ui.PREFS_CLOUD"; - public static final String ACTION_PREFS_ADV = "org.sufficientlysecure.keychain.ui.PREFS_ADV"; - public static final String ACTION_PREFS_PROXY = "org.sufficientlysecure.keychain.ui.PREFS_PROXY"; - public static final String ACTION_PREFS_GUI = "org.sufficientlysecure.keychain.ui.PREFS_GUI"; - public static final int REQUEST_CODE_KEYSERVER_PREF = 0x00007005; - private PreferenceScreen mKeyServerPreference = null; private static Preferences sPreferences; private ThemeChanger mThemeChanger; @@ -74,52 +68,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity { super.onCreate(savedInstanceState); setupToolbar(); - - String action = getIntent().getAction(); - - if (ACTION_PREFS_CLOUD.equals(action)) { - addPreferencesFromResource(R.xml.cloud_search_prefs); - - mKeyServerPreference = (PreferenceScreen) findPreference(Constants.Pref.KEY_SERVERS); - mKeyServerPreference.setSummary(keyserverSummary(this)); - mKeyServerPreference - .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - public boolean onPreferenceClick(Preference preference) { - Intent intent = new Intent(SettingsActivity.this, - SettingsKeyServerActivity.class); - intent.putExtra(SettingsKeyServerActivity.EXTRA_KEY_SERVERS, - sPreferences.getKeyServers()); - startActivityForResult(intent, REQUEST_CODE_KEYSERVER_PREF); - return false; - } - }); - initializeSearchKeyserver( - (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) - ); - initializeSearchKeybase( - (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) - ); - - } else if (ACTION_PREFS_ADV.equals(action)) { - addPreferencesFromResource(R.xml.passphrase_preferences); - - initializePassphraseCacheSubs( - (CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS)); - - initializePassphraseCacheTtl( - (IntegerListPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_TTL)); - - initializeUseDefaultYubiKeyPin( - (CheckBoxPreference) findPreference(Constants.Pref.USE_DEFAULT_YUBIKEY_PIN)); - - initializeUseNumKeypadForYubiKeyPin( - (CheckBoxPreference) findPreference(Constants.Pref.USE_NUMKEYPAD_FOR_YUBIKEY_PIN)); - - } else if (ACTION_PREFS_GUI.equals(action)) { - addPreferencesFromResource(R.xml.gui_preferences); - - initializeTheme((ListPreference) findPreference(Constants.Pref.THEME)); - } } @Override @@ -237,9 +185,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity { initializePassphraseCacheTtl( (IntegerListPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_TTL)); - initializeUseDefaultYubiKeyPin( - (CheckBoxPreference) findPreference(Constants.Pref.USE_DEFAULT_YUBIKEY_PIN)); - initializeUseNumKeypadForYubiKeyPin( (CheckBoxPreference) findPreference(Constants.Pref.USE_NUMKEYPAD_FOR_YUBIKEY_PIN)); } @@ -454,23 +399,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity { } /** - * This fragment shows gui preferences. - */ - public static class GuiPrefsFragment extends PreferenceFragment { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - - // Load the preferences from an XML resource - addPreferencesFromResource(R.xml.gui_preferences); - - initializeTheme((ListPreference) findPreference(Constants.Pref.THEME)); - } - } - - /** * This fragment shows the keyserver/contacts sync preferences */ public static class SyncPrefsFragment extends PreferenceFragment { @@ -582,7 +510,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity { return PassphrasePrefsFragment.class.getName().equals(fragmentName) || CloudSearchPrefsFragment.class.getName().equals(fragmentName) || ProxyPrefsFragment.class.getName().equals(fragmentName) - || GuiPrefsFragment.class.getName().equals(fragmentName) || SyncPrefsFragment.class.getName().equals(fragmentName) || ExperimentalPrefsFragment.class.getName().equals(fragmentName) || super.isValidFragment(fragmentName); @@ -665,17 +592,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity { .getPreferredKeyserver(); } - private static void initializeUseDefaultYubiKeyPin(final CheckBoxPreference mUseDefaultYubiKeyPin) { - mUseDefaultYubiKeyPin.setChecked(sPreferences.useDefaultYubiKeyPin()); - mUseDefaultYubiKeyPin.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference preference, Object newValue) { - mUseDefaultYubiKeyPin.setChecked((Boolean) newValue); - sPreferences.setUseDefaultYubiKeyPin((Boolean) newValue); - return false; - } - }); - } - private static void initializeUseNumKeypadForYubiKeyPin(final CheckBoxPreference mUseNumKeypadForYubiKeyPin) { mUseNumKeypadForYubiKeyPin.setChecked(sPreferences.useNumKeypadForYubiKeyPin()); mUseNumKeypadForYubiKeyPin.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java index d8edbe4f8..5a8ab36bc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyserverFragment.java @@ -155,7 +155,7 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC data.getBoolean(AddEditKeyserverDialogFragment.MESSAGE_VERIFIED); if (verified) { Notify.create(getActivity(), - R.string.add_keyserver_verified, Notify.Style.OK).show(); + R.string.add_keyserver_connection_verified, Notify.Style.OK).show(); } else { Notify.create(getActivity(), R.string.add_keyserver_without_verification, @@ -177,26 +177,6 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC } break; } - case AddEditKeyserverDialogFragment.MESSAGE_VERIFICATION_FAILED: { - AddEditKeyserverDialogFragment.FailureReason failureReason = - (AddEditKeyserverDialogFragment.FailureReason) data.getSerializable( - AddEditKeyserverDialogFragment.MESSAGE_FAILURE_REASON); - switch (failureReason) { - case CONNECTION_FAILED: { - Notify.create(getActivity(), - R.string.add_keyserver_connection_failed, - Notify.Style.ERROR).show(); - break; - } - case INVALID_URL: { - Notify.create(getActivity(), - R.string.add_keyserver_invalid_url, - Notify.Style.ERROR).show(); - break; - } - } - break; - } } } }; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java index 0415128a2..f38e4928d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/UploadKeyActivity.java @@ -17,6 +17,7 @@ package org.sufficientlysecure.keychain.ui; + import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -29,10 +30,12 @@ import android.widget.Spinner; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.ExportResult; +import org.sufficientlysecure.keychain.operations.results.UploadResult; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.UploadKeyringParcel; import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.util.Log; @@ -42,7 +45,7 @@ import org.sufficientlysecure.keychain.util.Preferences; * Sends the selected public key to a keyserver */ public class UploadKeyActivity extends BaseActivity - implements CryptoOperationHelper.Callback<ExportKeyringParcel, ExportResult> { + implements CryptoOperationHelper.Callback<UploadKeyringParcel, UploadResult> { private View mUploadButton; private Spinner mKeyServerSpinner; @@ -50,8 +53,8 @@ public class UploadKeyActivity extends BaseActivity // CryptoOperationHelper.Callback vars private String mKeyserver; - private Uri mUnifiedKeyringUri; - private CryptoOperationHelper<ExportKeyringParcel, ExportResult> mUploadOpHelper; + private long mMasterKeyId; + private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper; @Override protected void onCreate(Bundle savedInstanceState) { @@ -85,6 +88,16 @@ public class UploadKeyActivity extends BaseActivity finish(); 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 @@ -101,13 +114,10 @@ public class UploadKeyActivity extends BaseActivity } private void uploadKey() { - Uri blobUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); - mUnifiedKeyringUri = blobUri; - String server = (String) mKeyServerSpinner.getSelectedItem(); mKeyserver = server; - mUploadOpHelper = new CryptoOperationHelper(1, this, this, R.string.progress_uploading); + mUploadOpHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_uploading); mUploadOpHelper.cryptoOperation(); } @@ -125,12 +135,12 @@ public class UploadKeyActivity extends BaseActivity } @Override - public ExportKeyringParcel createOperationInput() { - return new ExportKeyringParcel(mKeyserver, mUnifiedKeyringUri); + public UploadKeyringParcel createOperationInput() { + return new UploadKeyringParcel(mKeyserver, mMasterKeyId); } @Override - public void onCryptoOperationSuccess(ExportResult result) { + public void onCryptoOperationSuccess(UploadResult result) { result.createNotify(this).show(); } @@ -140,7 +150,7 @@ public class UploadKeyActivity extends BaseActivity } @Override - public void onCryptoOperationError(ExportResult result) { + public void onCryptoOperationError(UploadResult result) { result.createNotify(this).show(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index a09e74abe..0f538cd1b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -19,7 +19,6 @@ package org.sufficientlysecure.keychain.ui; -import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -39,6 +38,7 @@ import android.os.Handler; import android.provider.ContactsContract; import android.support.design.widget.AppBarLayout; import android.support.design.widget.CollapsingToolbarLayout; +import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; @@ -65,12 +65,14 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType; import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; @@ -85,7 +87,6 @@ import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; import org.sufficientlysecure.keychain.util.ContactHelper; -import org.sufficientlysecure.keychain.util.ExportHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.NfcHelper; import org.sufficientlysecure.keychain.util.Preferences; @@ -355,7 +356,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements startActivity(homeIntent); return true; } - case R.id.menu_key_view_export_file: { + case R.id.menu_key_view_backup: { startPassphraseActivity(REQUEST_BACKUP); return true; } @@ -405,8 +406,8 @@ public class ViewKeyActivity extends BaseNfcActivity implements MenuItem editKey = menu.findItem(R.id.menu_key_view_edit); editKey.setVisible(mIsSecret); - MenuItem exportKey = menu.findItem(R.id.menu_key_view_export_file); - exportKey.setVisible(mIsSecret); + MenuItem backupKey = menu.findItem(R.id.menu_key_view_backup); + backupKey.setVisible(mIsSecret); MenuItem addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity); addLinked.setVisible(mIsSecret @@ -460,15 +461,40 @@ public class ViewKeyActivity extends BaseNfcActivity implements } private void startPassphraseActivity(int requestCode) { - Intent intent = new Intent(this, PassphraseDialogActivity.class); - intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mMasterKeyId); - startActivityForResult(intent, requestCode); + + if (keyHasPassphrase()) { + Intent intent = new Intent(this, PassphraseDialogActivity.class); + intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mMasterKeyId); + startActivityForResult(intent, requestCode); + } else { + startBackupActivity(); + } + } + + private boolean keyHasPassphrase() { + try { + SecretKeyType secretKeyType = + mProviderHelper.getCachedPublicKeyRing(mMasterKeyId).getSecretKeyType(mMasterKeyId); + switch (secretKeyType) { + // all of these make no sense to ask + case PASSPHRASE_EMPTY: + case GNU_DUMMY: + case DIVERT_TO_CARD: + case UNAVAILABLE: + return false; + default: + return true; + } + } catch (NotFoundException e) { + return false; + } } - private void backupToFile() { - new ExportHelper(this).showExportKeysDialog( - mMasterKeyId, new File(Constants.Path.APP_DIR, - KeyFormattingUtils.convertKeyIdToHex(mMasterKeyId) + ".sec.asc"), true); + private void startBackupActivity() { + Intent intent = new Intent(this, BackupActivity.class); + intent.putExtra(BackupActivity.EXTRA_MASTER_KEY_IDS, new long[] { mMasterKeyId }); + intent.putExtra(BackupActivity.EXTRA_SECRET, true); + startActivity(intent); } private void deleteKey() { @@ -524,7 +550,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements return; } - backupToFile(); + startBackupActivity(); return; } @@ -564,7 +590,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements } @Override - protected void onNfcPostExecute() throws IOException { + protected void onNfcPostExecute() { long yubiKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints); @@ -854,7 +880,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements mActionEncryptFile.setVisibility(View.INVISIBLE); mActionEncryptText.setVisibility(View.INVISIBLE); mActionNfc.setVisibility(View.INVISIBLE); - mFab.setVisibility(View.GONE); + hideFab(); mQrCodeLayout.setVisibility(View.GONE); } else if (mIsExpired) { if (mIsSecret) { @@ -870,7 +896,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements mActionEncryptFile.setVisibility(View.INVISIBLE); mActionEncryptText.setVisibility(View.INVISIBLE); mActionNfc.setVisibility(View.INVISIBLE); - mFab.setVisibility(View.GONE); + hideFab(); mQrCodeLayout.setVisibility(View.GONE); } else if (mIsSecret) { mStatusText.setText(R.string.view_key_my_key); @@ -912,7 +938,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements } else { mActionNfc.setVisibility(View.GONE); } - mFab.setVisibility(View.VISIBLE); + showFab(); // noinspection deprecation (no getDrawable with theme at current minApi level 15!) mFab.setImageDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp)); } else { @@ -929,7 +955,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements color = getResources().getColor(R.color.key_flag_green); photoTask.execute(mMasterKeyId); - mFab.setVisibility(View.GONE); + hideFab(); } else { mStatusText.setText(R.string.view_key_unverified); mStatusImage.setVisibility(View.VISIBLE); @@ -937,7 +963,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements State.UNVERIFIED, R.color.icons, true); color = getResources().getColor(R.color.key_flag_orange); - mFab.setVisibility(View.VISIBLE); + showFab(); } } @@ -967,6 +993,28 @@ public class ViewKeyActivity extends BaseNfcActivity implements } } + /** + * Helper to show Fab, from http://stackoverflow.com/a/31047038 + */ + private void showFab() { + CoordinatorLayout.LayoutParams p = (CoordinatorLayout.LayoutParams) mFab.getLayoutParams(); + p.setBehavior(new FloatingActionButton.Behavior()); + p.setAnchorId(R.id.app_bar_layout); + mFab.setLayoutParams(p); + mFab.setVisibility(View.VISIBLE); + } + + /** + * Helper to hide Fab, from http://stackoverflow.com/a/31047038 + */ + private void hideFab() { + CoordinatorLayout.LayoutParams p = (CoordinatorLayout.LayoutParams) mFab.getLayoutParams(); + p.setBehavior(null); //should disable default animations + p.setAnchorId(View.NO_ID); //should let you set visibility + mFab.setLayoutParams(p); + mFab.setVisibility(View.GONE); + } + @Override public void onLoaderReset(Loader<Cursor> loader) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java index edd9feec9..bba6a6dc1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java @@ -41,13 +41,11 @@ import org.sufficientlysecure.keychain.ui.adapter.PagerTabStripAdapter; import org.sufficientlysecure.keychain.ui.base.BaseActivity; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.ContactHelper; -import org.sufficientlysecure.keychain.util.ExportHelper; import org.sufficientlysecure.keychain.util.Log; public class ViewKeyAdvActivity extends BaseActivity implements LoaderManager.LoaderCallbacks<Cursor> { - ExportHelper mExportHelper; ProviderHelper mProviderHelper; protected Uri mDataUri; @@ -75,7 +73,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements } }); - mExportHelper = new ExportHelper(this); mProviderHelper = new ProviderHelper(this); mViewPager = (ViewPager) findViewById(R.id.pager); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java index 4a46896bc..c5e575e32 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvShareFragment.java @@ -52,18 +52,18 @@ import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider; +import org.sufficientlysecure.keychain.provider.TemporaryFileProvider; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; 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.ui.util.QrCodeUtils; -import org.sufficientlysecure.keychain.util.ExportHelper; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.NfcHelper; @@ -84,7 +84,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements private Uri mDataUri; private byte[] mFingerprint; - private long mMasterKeyId; + private String mUserId; @Override public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { @@ -107,7 +107,6 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements View vFingerprintShareButton = view.findViewById(R.id.view_key_action_fingerprint_share); View vFingerprintClipboardButton = view.findViewById(R.id.view_key_action_fingerprint_clipboard); View vKeyShareButton = view.findViewById(R.id.view_key_action_key_share); - View vKeySafeButton = view.findViewById(R.id.view_key_action_key_export); View vKeyNfcButton = view.findViewById(R.id.view_key_action_key_nfc); View vKeyClipboardButton = view.findViewById(R.id.view_key_action_key_clipboard); ImageButton vKeySafeSlingerButton = (ImageButton) view.findViewById(R.id.view_key_action_key_safeslinger); @@ -118,31 +117,25 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements vFingerprintShareButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - share(true, false); + shareFingerprint(false); } }); vFingerprintClipboardButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - share(true, true); + shareFingerprint(true); } }); vKeyShareButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - share(false, false); - } - }); - vKeySafeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - exportToFile(); + shareKey(false); } }); vKeyClipboardButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - share(false, true); + shareKey(true); } }); @@ -174,11 +167,6 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements return root; } - private void exportToFile() { - new ExportHelper(getActivity()).showExportKeysDialog( - mMasterKeyId, Constants.Path.APP_DIR_FILE, false); - } - private void startSafeSlinger(Uri dataUri) { long keyId = 0; try { @@ -193,7 +181,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements startActivityForResult(safeSlingerIntent, 0); } - private void share(boolean fingerprintOnly, boolean toClipboard) { + private void shareKey(boolean toClipboard) { Activity activity = getActivity(); if (activity == null || mFingerprint == null) { return; @@ -201,18 +189,8 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements ProviderHelper providerHelper = new ProviderHelper(activity); try { - String content; - if (fingerprintOnly) { - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(mFingerprint); - if (!toClipboard) { - content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; - } else { - content = fingerprint; - } - } else { - content = providerHelper.getKeyRingAsArmoredString( - KeychainContract.KeyRingData.buildPublicKeyRingUri(mDataUri)); - } + String content = providerHelper.getKeyRingAsArmoredString( + KeychainContract.KeyRingData.buildPublicKeyRingUri(mDataUri)); if (toClipboard) { ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); @@ -224,29 +202,26 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements ClipData clip = ClipData.newPlainText(Constants.CLIPBOARD_LABEL, content); clipMan.setPrimaryClip(clip); - Notify.create(activity, fingerprintOnly ? R.string.fingerprint_copied_to_clipboard - : R.string.key_copied_to_clipboard, Notify.Style.OK).show(); - return; - } - - // Android will fail with android.os.TransactionTooLargeException if key is too big - // see http://www.lonestarprod.com/?p=34 - if (content.length() >= 86389) { - Notify.create(activity, R.string.key_too_big_for_sharing, Notify.Style.ERROR).show(); + Notify.create(activity, R.string.key_copied_to_clipboard, Notify.Style.OK).show(); return; } // let user choose application Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, content); sendIntent.setType("text/plain"); - // Bluetooth Share will convert text/plain sent via EXTRA_TEXT to HTML - // Add replacement extra to send a text/plain file instead. + // NOTE: Don't use Intent.EXTRA_TEXT to send the key + // better send it via a Uri! + // example: Bluetooth Share will convert text/plain sent via Intent.EXTRA_TEXT to HTML try { - TemporaryStorageProvider shareFileProv = new TemporaryStorageProvider(); - Uri contentUri = TemporaryStorageProvider.createFile(activity, - KeyFormattingUtils.convertFingerprintToHex(mFingerprint) + Constants.FILE_EXTENSION_ASC); + TemporaryFileProvider shareFileProv = new TemporaryFileProvider(); + + String filename = KeyFormattingUtils.convertFingerprintToHex(mFingerprint); + KeyRing.UserId mainUserId = KeyRing.splitUserId(mUserId); + if (mainUserId.name != null) { + filename = mainUserId.name; + } + Uri contentUri = TemporaryFileProvider.createFile(activity, filename + Constants.FILE_EXTENSION_ASC); BufferedWriter contentWriter = new BufferedWriter(new OutputStreamWriter( new ParcelFileDescriptor.AutoCloseOutputStream( @@ -256,18 +231,15 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements sendIntent.putExtra(Intent.EXTRA_STREAM, contentUri); } catch (FileNotFoundException e) { - Log.e(Constants.TAG, "error creating temporary Bluetooth key share file!", e); + Log.e(Constants.TAG, "Error creating temporary key share file!", e); // no need for a snackbar because one sharing option doesn't work // Notify.create(getActivity(), R.string.error_temp_file, Notify.Style.ERROR).show(); } - - String title = getString(fingerprintOnly - ? R.string.title_share_fingerprint_with : R.string.title_share_key); + String title = getString(R.string.title_share_key); Intent shareChooser = Intent.createChooser(sendIntent, title); startActivity(shareChooser); - } catch (PgpGeneralException | IOException e) { Log.e(Constants.TAG, "error processing key!", e); Notify.create(activity, R.string.error_key_processing, Notify.Style.ERROR).show(); @@ -277,6 +249,45 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements } } + private void shareFingerprint(boolean toClipboard) { + Activity activity = getActivity(); + if (activity == null || mFingerprint == null) { + return; + } + + String content; + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(mFingerprint); + if (!toClipboard) { + content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; + } else { + content = fingerprint; + } + + if (toClipboard) { + ClipboardManager clipMan = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipMan == null) { + Notify.create(activity, R.string.error_clipboard_copy, Style.ERROR); + return; + } + + ClipData clip = ClipData.newPlainText(Constants.CLIPBOARD_LABEL, content); + clipMan.setPrimaryClip(clip); + + Notify.create(activity, R.string.fingerprint_copied_to_clipboard, Notify.Style.OK).show(); + return; + } + + // let user choose application + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, content); + sendIntent.setType("text/plain"); + + String title = getString(R.string.title_share_fingerprint_with); + Intent shareChooser = Intent.createChooser(sendIntent, title); + + startActivity(shareChooser); + } + private void showQrCodeDialog() { Intent qrCodeIntent = new Intent(getActivity(), QrCodeViewActivity.class); @@ -318,11 +329,12 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements mNfcHelper.initNfc(mDataUri); } - static final String[] UNIFIED_PROJECTION = new String[] { - KeyRings._ID, KeyRings.FINGERPRINT + static final String[] UNIFIED_PROJECTION = new String[]{ + KeyRings._ID, KeyRings.FINGERPRINT, KeyRings.USER_ID }; static final int INDEX_UNIFIED_FINGERPRINT = 1; + static final int INDEX_UNIFIED_USER_ID = 2; public Loader<Cursor> onCreateLoader(int id, Bundle args) { setContentShown(false); @@ -351,6 +363,8 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); setFingerprint(fingerprintBlob); + mUserId = data.getString(INDEX_UNIFIED_USER_ID); + break; } } @@ -367,10 +381,11 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements mFingerprint = null; } - /** Load QR Code asynchronously and with a fade in animation */ + /** + * Load QR Code asynchronously and with a fade in animation + */ private void setFingerprint(byte[] fingerprintBlob) { mFingerprint = fingerprintBlob; - mMasterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(fingerprintBlob); final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); mFingerprintView.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint)); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java index 266633061..11c032517 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java @@ -40,6 +40,7 @@ import android.widget.TableRow; import android.widget.TextView; import com.textuality.keybase.lib.KeybaseException; +import com.textuality.keybase.lib.KeybaseQuery; import com.textuality.keybase.lib.Proof; import com.textuality.keybase.lib.User; @@ -51,6 +52,7 @@ import org.sufficientlysecure.keychain.service.KeybaseVerificationParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.OkHttpKeybaseClient; import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; @@ -224,8 +226,9 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements } } - // look for evidence from keybase in the background, make tabular version of result - // + /** + * look for evidence from keybase in the background, make tabular version of result + */ private class DescribeKey extends AsyncTask<String, Void, ResultPage> { ParcelableProxy mParcelableProxy; @@ -240,7 +243,9 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements final ArrayList<CharSequence> proofList = new ArrayList<CharSequence>(); final Hashtable<Integer, ArrayList<Proof>> proofs = new Hashtable<Integer, ArrayList<Proof>>(); try { - User keybaseUser = User.findByFingerprint(fingerprint, mParcelableProxy.getProxy()); + KeybaseQuery keybaseQuery = new KeybaseQuery(new OkHttpKeybaseClient()); + keybaseQuery.setProxy(mParcelableProxy.getProxy()); + User keybaseUser = User.findByFingerprint(keybaseQuery, fingerprint); for (Proof proof : keybaseUser.getProofs()) { Integer proofType = proof.getType(); appendIfOK(proofs, proofType, proof); @@ -266,7 +271,12 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements } catch (KeybaseException ignored) { } - return new ResultPage(getString(R.string.key_trust_results_prefix), proofList); + String prefix = ""; + if (isAdded()) { + prefix = getString(R.string.key_trust_results_prefix); + } + + return new ResultPage(prefix, proofList); } private SpannableStringBuilder formatSpannableString(SpannableStringBuilder proofLinks, String proofType) { @@ -291,7 +301,10 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements if (haveProofFor(proof.getType())) { ssb.append("\u00a0["); startAt = ssb.length(); - String verify = getString(R.string.keybase_verify); + String verify = ""; + if (isAdded()) { + verify = getString(R.string.keybase_verify); + } ssb.append(verify); ClickableSpan clicker = new ClickableSpan() { @Override @@ -308,6 +321,11 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements @Override protected void onPostExecute(ResultPage result) { super.onPostExecute(result); + // stop if fragment is no longer added to an activity + if(!isAdded()) { + return; + } + if (result.mProofs.isEmpty()) { result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence); } @@ -356,7 +374,12 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements default: stringIndex = R.string.keybase_narrative_unknown; } - return getActivity().getString(stringIndex); + + if (isAdded()) { + return getString(stringIndex); + } else { + return ""; + } } private void appendIfOK(Hashtable<Integer, ArrayList<Proof>> table, Integer proofType, Proof proof) throws KeybaseException { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java index 139512ba9..038ebd5dd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java @@ -17,6 +17,14 @@ package org.sufficientlysecure.keychain.ui.adapter; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + import android.content.Context; import android.support.v4.content.AsyncTaskLoader; import android.support.v4.util.LongSparseArray; @@ -28,28 +36,26 @@ import org.sufficientlysecure.keychain.operations.results.GetKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow; +import org.sufficientlysecure.keychain.ui.ImportKeysListFragment.BytesLoaderState; +import org.sufficientlysecure.keychain.util.FileHelper; import org.sufficientlysecure.keychain.util.InputData; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.PositionAwareInputStream; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.util.ArrayList; - public class ImportKeysListLoader extends AsyncTaskLoader<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>> { final Context mContext; - final InputData mInputData; + final BytesLoaderState mLoaderState; ArrayList<ImportKeysListEntry> mData = new ArrayList<>(); LongSparseArray<ParcelableKeyRing> mParcelableRings = new LongSparseArray<>(); AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> mEntryListWrapper; - public ImportKeysListLoader(Context context, InputData inputData) { + public ImportKeysListLoader(Context context, BytesLoaderState inputData) { super(context); this.mContext = context; - this.mInputData = inputData; + this.mLoaderState = inputData; } @Override @@ -62,12 +68,13 @@ public class ImportKeysListLoader GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_OK, null); mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult); - if (mInputData == null) { + if (mLoaderState == null) { Log.e(Constants.TAG, "Input data is null!"); return mEntryListWrapper; } - generateListOfKeyrings(mInputData); + InputData inputData = getInputData(getContext(), mLoaderState); + generateListOfKeyrings(inputData); return mEntryListWrapper; } @@ -99,12 +106,7 @@ public class ImportKeysListLoader return mParcelableRings; } - /** - * Reads all PGPKeyRing objects from input - * - * @param inputData - * @return - */ + /** Reads all PGPKeyRing objects from the bytes of an InputData object. */ private void generateListOfKeyrings(InputData inputData) { PositionAwareInputStream progressIn = new PositionAwareInputStream( inputData.getInputStream()); @@ -132,4 +134,23 @@ public class ImportKeysListLoader } } + private static InputData getInputData(Context context, BytesLoaderState loaderState) { + InputData inputData = null; + if (loaderState.mKeyBytes != null) { + inputData = new InputData(new ByteArrayInputStream(loaderState.mKeyBytes), loaderState.mKeyBytes.length); + } else if (loaderState.mDataUri != null) { + try { + InputStream inputStream = context.getContentResolver().openInputStream(loaderState.mDataUri); + long length = FileHelper.getFileSize(context, loaderState.mDataUri, -1); + + inputData = new InputData(inputStream, length); + } catch (FileNotFoundException e) { + Log.e(Constants.TAG, "FileNotFoundException!", e); + return null; + } + } + + return inputData; + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java index 471a20411..7cc37b3a3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySelectableAdapter.java @@ -1,13 +1,8 @@ package org.sufficientlysecure.keychain.ui.adapter; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - import android.content.Context; import android.database.Cursor; -import android.support.v7.internal.widget.AdapterViewCompat; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; @@ -18,6 +13,10 @@ import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.util.Log; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + public class KeySelectableAdapter extends KeyAdapter implements OnItemClickListener { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java index 5cf0e6e08..5566c725b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java @@ -24,6 +24,7 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; +import android.os.Build.VERSION_CODES; import android.support.v4.content.CursorLoader; import android.util.Log; import android.view.LayoutInflater; @@ -228,9 +229,11 @@ public class LinkedIdsAdapter extends UserAttributesAdapter { } public void seekAttention() { - ObjectAnimator anim = SubtleAttentionSeeker.tintText(vComment, 1000); - anim.setStartDelay(200); - anim.start(); + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + ObjectAnimator anim = SubtleAttentionSeeker.tintText(vComment, 1000); + anim.setStartDelay(200); + anim.start(); + } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java index 972421abe..3e0bc7890 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseNfcActivity.java @@ -35,7 +35,6 @@ import android.nfc.TagLostException; import android.nfc.tech.IsoDep; import android.os.AsyncTask; import android.os.Bundle; -import android.widget.Toast; import org.spongycastle.bcpg.HashAlgorithmTags; import org.spongycastle.util.Arrays; @@ -103,7 +102,7 @@ public abstract class BaseNfcActivity extends BaseActivity { /** * Override to handle result of NFC operations (UI thread) */ - protected void onNfcPostExecute() throws IOException { + protected void onNfcPostExecute() { final long subKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints); @@ -134,9 +133,16 @@ public abstract class BaseNfcActivity extends BaseActivity { Notify.create(this, error, Style.WARN).show(); } + /** + * Override to do something when PIN is wrong, e.g., clear passphrases (UI thread) + */ + protected void onNfcPinError(String error) { + onNfcError(error); + } + public void handleIntentInBackground(final Intent intent) { // Actual NFC operations are executed in doInBackground to not block the UI thread - new AsyncTask<Void, Void, Exception>() { + new AsyncTask<Void, Void, IOException>() { @Override protected void onPreExecute() { super.onPreExecute(); @@ -144,11 +150,9 @@ public abstract class BaseNfcActivity extends BaseActivity { } @Override - protected Exception doInBackground(Void... params) { + protected IOException doInBackground(Void... params) { try { handleTagDiscoveredIntent(intent); - } catch (CardException e) { - return e; } catch (IOException e) { return e; } @@ -157,7 +161,7 @@ public abstract class BaseNfcActivity extends BaseActivity { } @Override - protected void onPostExecute(Exception exception) { + protected void onPostExecute(IOException exception) { super.onPostExecute(exception); if (exception != null) { @@ -165,11 +169,7 @@ public abstract class BaseNfcActivity extends BaseActivity { return; } - try { - onNfcPostExecute(); - } catch (IOException e) { - handleNfcError(e); - } + onNfcPostExecute(); } }.execute(); } @@ -221,24 +221,30 @@ public abstract class BaseNfcActivity extends BaseActivity { } } - private void handleNfcError(Exception e) { - Log.e(Constants.TAG, "nfc error", e); + private void handleNfcError(IOException e) { if (e instanceof TagLostException) { onNfcError(getString(R.string.error_nfc_tag_lost)); return; } + if (e instanceof IsoDepNotSupportedException) { + onNfcError(getString(R.string.error_nfc_iso_dep_not_supported)); + return; + } + short status; if (e instanceof CardException) { status = ((CardException) e).getResponseCode(); } else { status = -1; } - // When entering a PIN, a status of 63CX indicates X attempts remaining. - if ((status & (short)0xFFF0) == 0x63C0) { + + // Wrong PIN, a status of 63CX indicates X attempts remaining. + if ((status & (short) 0xFFF0) == 0x63C0) { int tries = status & 0x000F; - onNfcError(getResources().getQuantityString(R.plurals.error_pin, tries, tries)); + // hook to do something different when PIN is wrong + onNfcPinError(getResources().getQuantityString(R.plurals.error_pin, tries, tries)); return; } @@ -272,7 +278,7 @@ public abstract class BaseNfcActivity extends BaseActivity { break; } case 0x6700: { - onNfcError(getString(R.string.error_nfc_wrong_length)); + onNfcPinError(getString(R.string.error_nfc_wrong_length)); break; } case 0x6982: { @@ -307,12 +313,6 @@ public abstract class BaseNfcActivity extends BaseActivity { } - public void handlePinError() { - toast("Wrong PIN!"); - setResult(RESULT_CANCELED); - finish(); - } - /** * Called when the system is about to start resuming a previous activity, * disables NFC Foreground Dispatch @@ -337,18 +337,11 @@ public abstract class BaseNfcActivity extends BaseActivity { protected void obtainYubiKeyPin(RequiredInputParcel requiredInput) { - // shortcut if we only use the default yubikey pin - Preferences prefs = Preferences.getPreferences(this); - if (prefs.useDefaultYubiKeyPin()) { - mPin = new Passphrase("123456"); - return; - } - try { - Passphrase phrase = PassphraseCacheService.getCachedPassphrase(this, + Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this, requiredInput.getMasterKeyId(), requiredInput.getSubKeyId()); - if (phrase != null) { - mPin = phrase; + if (passphrase != null) { + mPin = passphrase; return; } @@ -363,10 +356,6 @@ public abstract class BaseNfcActivity extends BaseActivity { } - protected void setYubiKeyPin(Passphrase pin) { - mPin = pin; - } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { @@ -406,6 +395,9 @@ public abstract class BaseNfcActivity extends BaseActivity { // Connect to the detected tag, setting a couple of settings mIsoDep = IsoDep.get(detectedTag); + if (mIsoDep == null) { + throw new IsoDepNotSupportedException("Tag does not support ISO-DEP (ISO 14443-4)"); + } mIsoDep.setTimeout(TIMEOUT); // timeout is set to 100 seconds to avoid cancellation during calculation mIsoDep.connect(); @@ -448,7 +440,7 @@ public abstract class BaseNfcActivity extends BaseActivity { * @return The long key id of the requested key, or null if not found. */ public Long nfcGetKeyId(int idx) throws IOException { - byte[] fp = nfcGetFingerprint(idx); + byte[] fp = nfcGetMasterKeyFingerprint(idx); if (fp == null) { return null; } @@ -469,7 +461,7 @@ public abstract class BaseNfcActivity extends BaseActivity { byte[] buf = mIsoDep.transceive(Hex.decode(data)); Iso7816TLV tlv = Iso7816TLV.readSingle(buf, true); - Log.d(Constants.TAG, "nfc tlv data:\n" + tlv.prettyPrint()); + Log.d(Constants.TAG, "nfcGetFingerprints() Iso7816TLV tlv data:\n" + tlv.prettyPrint()); Iso7816TLV fptlv = Iso7816TLV.findRecursive(tlv, 0xc5); if (fptlv == null) { @@ -494,8 +486,11 @@ public abstract class BaseNfcActivity extends BaseActivity { * @param idx Index of the key to return the fingerprint from. * @return The fingerprint of the requested key, or null if not found. */ - public byte[] nfcGetFingerprint(int idx) throws IOException { + public byte[] nfcGetMasterKeyFingerprint(int idx) throws IOException { byte[] data = nfcGetFingerprints(); + if (data == null) { + return null; + } // return the master key fingerprint ByteBuffer fpbuf = ByteBuffer.wrap(data); @@ -507,14 +502,11 @@ public abstract class BaseNfcActivity extends BaseActivity { } public byte[] nfcGetAid() throws IOException { - String info = "00CA004F00"; return mIsoDep.transceive(Hex.decode(info)); - } public String nfcGetUserId() throws IOException { - String info = "00CA006500"; return nfcGetHolderName(nfcCommunicate(info)); } @@ -648,8 +640,6 @@ public abstract class BaseNfcActivity extends BaseActivity { String decryptedSessionKey = nfcGetDataField(second); - Log.d(Constants.TAG, "decryptedSessionKey: " + decryptedSessionKey); - return Hex.decode(decryptedSessionKey); } @@ -671,18 +661,8 @@ public abstract class BaseNfcActivity extends BaseActivity { // SW1/2 0x9000 is the generic "ok" response, which we expect most of the time. // See specification, page 51 String accepted = "9000"; - - // 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); - String response = nfcCommunicate(login); // login + String response = tryPin(mode, pin); // login if (!response.equals(accepted)) { - handlePinError(); throw new CardException("Bad PIN!", parseCardStatus(response)); } @@ -696,6 +676,51 @@ public abstract class BaseNfcActivity extends BaseActivity { } } + public void nfcResetCard() throws IOException { + String accepted = "9000"; + + // try wrong PIN 4 times until counter goes to C0 + byte[] pin = "XXXXXX".getBytes(); + for (int i = 0; i <= 4; i++) { + String response = tryPin(0x81, pin); + if (response.equals(accepted)) { // Should NOT accept! + throw new CardException("Should never happen, XXXXXX has been accepted!", parseCardStatus(response)); + } + } + + // try wrong Admin PIN 4 times until counter goes to C0 + byte[] adminPin = "XXXXXXXX".getBytes(); + for (int i = 0; i <= 4; i++) { + String response = tryPin(0x83, adminPin); + if (response.equals(accepted)) { // Should NOT accept! + throw new CardException("Should never happen, XXXXXXXX has been accepted", parseCardStatus(response)); + } + } + + // reactivate card! + String reactivate1 = "00" + "e6" + "00" + "00"; + String reactivate2 = "00" + "44" + "00" + "00"; + String response1 = nfcCommunicate(reactivate1); + String response2 = nfcCommunicate(reactivate2); + if (!response1.equals(accepted) || !response2.equals(accepted)) { + throw new CardException("Reactivating failed!", parseCardStatus(response1)); + } + + } + + private String tryPin(int mode, byte[] pin) throws IOException { + // Command APDU for VERIFY command (page 32) + String login = + "00" // CLA + + "20" // INS + + "00" // P1 + + String.format("%02x", mode) // P2 + + String.format("%02x", pin.length) // Lc + + Hex.toHexString(pin); + + return nfcCommunicate(login); + } + /** Modifies the user's PW1 or PW3. Before sending, the new PIN will be validated for * conformance to the card's requirements for key length. * @@ -737,7 +762,6 @@ public abstract class BaseNfcActivity extends BaseActivity { + getHex(newPin); String response = nfcCommunicate(changeReferenceDataApdu); // change PIN if (!response.equals("9000")) { - handlePinError(); throw new CardException("Failed to change PIN", parseCardStatus(response)); } } @@ -906,15 +930,6 @@ public abstract class BaseNfcActivity extends BaseActivity { } /** - * Prints a message to the screen - * - * @param text the text which should be contained within the toast - */ - protected void toast(String text) { - Toast.makeText(this, text, Toast.LENGTH_LONG).show(); - } - - /** * Receive new NFC Intents to this activity only by enabling foreground dispatch. * This can only be done in onResume! */ @@ -930,12 +945,10 @@ public abstract class BaseNfcActivity extends BaseActivity { new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED) }; - // https://code.google.com/p/android/issues/detail?id=62918 - // maybe mNfcAdapter.enableReaderMode(); ? try { mNfcAdapter.enableForegroundDispatch(this, nfcPendingIntent, writeTagFilters, null); } catch (IllegalStateException e) { - Log.i(Constants.TAG, "NfcForegroundDispatch Error!", e); + Log.i(Constants.TAG, "NfcForegroundDispatch Exception: Activity is not currently in the foreground?", e); } Log.d(Constants.TAG, "NfcForegroundDispatch has been enabled!"); } @@ -952,14 +965,23 @@ public abstract class BaseNfcActivity extends BaseActivity { } public String nfcGetHolderName(String name) { - 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); + try { + String slength; + int ilength; + name = name.substring(6); + slength = name.substring(0, 2); + ilength = Integer.parseInt(slength, 16) * 2; + name = name.substring(2, ilength + 2); + name = (new String(Hex.decode(name))).replace('<', ' '); + return name; + } catch (IndexOutOfBoundsException e) { + // try-catch for https://github.com/FluffyKaon/OpenPGP-Card + // Note: This should not happen, but happens with + // https://github.com/FluffyKaon/OpenPGP-Card, thus return an empty string for now! + + Log.e(Constants.TAG, "Couldn't get holder name, returning empty string!", e); + return ""; + } } private String nfcGetDataField(String output) { @@ -974,6 +996,14 @@ public abstract class BaseNfcActivity extends BaseActivity { return new String(Hex.encode(raw)); } + public class IsoDepNotSupportedException extends IOException { + + public IsoDepNotSupportedException(String detailMessage) { + super(detailMessage); + } + + } + public class CardException extends IOException { private short mResponseCode; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java index 52c6797d5..7ab9c7237 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java @@ -84,6 +84,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu public static final int REQUEST_CODE_RETRY_UPLOAD = 4; private Integer mProgressMessageResource; + private boolean mCancellable = false; private FragmentActivity mActivity; private Fragment mFragment; @@ -118,6 +119,10 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu mProgressMessageResource = id; } + public void setProgressCancellable(boolean cancellable) { + mCancellable = cancellable; + } + private void initiateInputActivity(RequiredInputParcel requiredInput, CryptoInputParcel cryptoInputParcel) { @@ -136,7 +141,8 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu } case PASSPHRASE: - case PASSPHRASE_SYMMETRIC: { + case PASSPHRASE_SYMMETRIC: + case BACKUP_CODE: { Intent intent = new Intent(activity, PassphraseDialogActivity.class); intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT, requiredInput); intent.putExtra(PassphraseDialogActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel); @@ -310,7 +316,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu if (mProgressMessageResource != null) { saveHandler.showProgressDialog( activity.getString(mProgressMessageResource), - ProgressDialog.STYLE_HORIZONTAL, false); + ProgressDialog.STYLE_HORIZONTAL, mCancellable); } activity.startService(intent); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java index 47bc7dfda..3d96f3c6d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddEditKeyserverDialogFragment.java @@ -24,6 +24,7 @@ import java.net.URI; import java.net.URISyntaxException; import android.app.Activity; +import android.support.design.widget.TextInputLayout; import android.support.v7.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; @@ -44,6 +45,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.CheckBox; +import android.widget.CompoundButton; import android.widget.EditText; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; @@ -54,6 +56,7 @@ import com.squareup.okhttp.Request; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.HkpKeyserver; +import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.TlsHelper; @@ -68,11 +71,9 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On private static final String ARG_KEYSERVER = "arg_keyserver"; public static final int MESSAGE_OKAY = 1; - public static final int MESSAGE_VERIFICATION_FAILED = 2; public static final String MESSAGE_KEYSERVER = "new_keyserver"; public static final String MESSAGE_VERIFIED = "verified"; - public static final String MESSAGE_FAILURE_REASON = "failure_reason"; public static final String MESSAGE_KEYSERVER_DELETED = "keyserver_deleted"; public static final String MESSAGE_DIALOG_ACTION = "message_dialog_action"; public static final String MESSAGE_EDIT_POSITION = "keyserver_edited_position"; @@ -82,7 +83,9 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On private int mPosition; private EditText mKeyserverEditText; + private TextInputLayout mKeyserverEditTextLayout; private CheckBox mVerifyKeyserverCheckBox; + private CheckBox mOnlyTrustedKeyserverCheckBox; public enum DialogAction { ADD, @@ -91,7 +94,8 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On public enum FailureReason { INVALID_URL, - CONNECTION_FAILED + CONNECTION_FAILED, + NO_PINNED_CERTIFICATE } public static AddEditKeyserverDialogFragment newInstance(Messenger messenger, @@ -126,7 +130,15 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On alert.setView(view); mKeyserverEditText = (EditText) view.findViewById(R.id.keyserver_url_edit_text); - mVerifyKeyserverCheckBox = (CheckBox) view.findViewById(R.id.verify_keyserver_checkbox); + mKeyserverEditTextLayout = (TextInputLayout) view.findViewById(R.id.keyserver_url_edit_text_layout); + mVerifyKeyserverCheckBox = (CheckBox) view.findViewById(R.id.verify_connection_checkbox); + mOnlyTrustedKeyserverCheckBox = (CheckBox) view.findViewById(R.id.only_trusted_keyserver_checkbox); + mVerifyKeyserverCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mOnlyTrustedKeyserverCheckBox.setEnabled(isChecked); + } + }); switch (mDialogAction) { case ADD: { @@ -212,6 +224,8 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On positiveButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + mKeyserverEditTextLayout.setErrorEnabled(false); + // behaviour same for edit and add final String keyserverUrl = mKeyserverEditText.getText().toString(); if (mVerifyKeyserverCheckBox.isChecked()) { @@ -220,13 +234,20 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() { @Override public void onOrbotStarted() { - verifyConnection(keyserverUrl, - proxyPrefs.parcelableProxy.getProxy()); + verifyConnection( + keyserverUrl, + proxyPrefs.parcelableProxy.getProxy(), + mOnlyTrustedKeyserverCheckBox.isChecked() + ); } @Override public void onNeutralButton() { - verifyConnection(keyserverUrl, null); + verifyConnection( + keyserverUrl, + null, + mOnlyTrustedKeyserverCheckBox.isChecked() + ); } @Override @@ -236,7 +257,11 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On }; if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) { - verifyConnection(keyserverUrl, proxyPrefs.parcelableProxy.getProxy()); + verifyConnection( + keyserverUrl, + proxyPrefs.parcelableProxy.getProxy(), + mOnlyTrustedKeyserverCheckBox.isChecked() + ); } } else { dismiss(); @@ -272,14 +297,28 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On sendMessageToHandler(MESSAGE_OKAY, data); } - public void verificationFailed(FailureReason reason) { - Bundle data = new Bundle(); - data.putSerializable(MESSAGE_FAILURE_REASON, reason); + public void verificationFailed(FailureReason failureReason) { + switch (failureReason) { + case CONNECTION_FAILED: { + mKeyserverEditTextLayout.setError( + getString(R.string.add_keyserver_connection_failed)); + break; + } + case INVALID_URL: { + mKeyserverEditTextLayout.setError( + getString(R.string.add_keyserver_invalid_url)); + break; + } + case NO_PINNED_CERTIFICATE: { + mKeyserverEditTextLayout.setError( + getString(R.string.add_keyserver_keyserver_not_trusted)); + break; + } + } - sendMessageToHandler(MESSAGE_VERIFICATION_FAILED, data); } - public void verifyConnection(String keyserver, final Proxy proxy) { + public void verifyConnection(String keyserver, final Proxy proxy, final boolean onlyTrustedKeyserver) { new AsyncTask<String, Void, FailureReason>() { ProgressDialog mProgressDialog; @@ -288,7 +327,7 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On @Override protected void onPreExecute() { mProgressDialog = new ProgressDialog(getActivity()); - mProgressDialog.setMessage(getString(R.string.progress_verifying_keyserver_url)); + mProgressDialog.setMessage(getString(R.string.progress_verifying_keyserver_connection)); mProgressDialog.setCancelable(false); mProgressDialog.show(); } @@ -316,7 +355,18 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On Log.d("Converted URL", newKeyserver.toString()); OkHttpClient client = HkpKeyserver.getClient(newKeyserver.toURL(), proxy); - TlsHelper.pinCertificateIfNecessary(client, newKeyserver.toURL()); + + // don't follow any redirects + client.setFollowRedirects(false); + client.setFollowSslRedirects(false); + + if (onlyTrustedKeyserver + && !TlsHelper.usePinnedCertificateIfAvailable(client, newKeyserver.toURL())) { + Log.w(Constants.TAG, "No pinned certificate for this host in OpenKeychain's assets."); + reason = FailureReason.NO_PINNED_CERTIFICATE; + return reason; + } + client.newCall(new Request.Builder().url(newKeyserver.toURL()).build()).execute(); } catch (TlsHelper.TlsHelperException e) { reason = FailureReason.CONNECTION_FAILED; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java index b51d081e1..cd5281c7c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/AddSubkeyDialogFragment.java @@ -348,30 +348,30 @@ public class AddSubkeyDialogFragment extends DialogFragment { /** * <h3>RSA</h3> - * <p>for RSA algorithm, key length must be greater than 1024 (according to - * <a href="https://github.com/open-keychain/open-keychain/issues/102">#102</a>). Possibility to generate keys bigger + * <p>for RSA algorithm, key length must be greater than 2048. Possibility to generate keys bigger * than 8192 bits is currently disabled, because it's almost impossible to generate them on a mobile device (check * <a href="http://www.javamex.com/tutorials/cryptography/rsa_key_length.shtml">RSA key length plot</a> and * <a href="http://www.keylength.com/">Cryptographic Key Length Recommendation</a>). Also, key length must be a * multiplicity of 8.</p> * <h3>ElGamal</h3> - * <p>For ElGamal algorithm, supported key lengths are 1536, 2048, 3072, 4096 or 8192 bits.</p> + * <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 512 and 1024. Also, it must me dividable by 64.</p> + * <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 SpongyCastle specification. Returns <code>-1</code>, if key length is * inappropriate. */ private int getProperKeyLength(Algorithm algorithm, int currentKeyLength) { - final int[] elGamalSupportedLengths = {1536, 2048, 3072, 4096, 8192}; + final int[] elGamalSupportedLengths = {2048, 3072, 4096, 8192}; int properKeyLength = -1; switch (algorithm) { - case RSA: - if (currentKeyLength > 1024 && currentKeyLength <= 16384) { + case RSA: { + if (currentKeyLength >= 2048 && currentKeyLength <= 16384) { properKeyLength = currentKeyLength + ((8 - (currentKeyLength % 8)) % 8); } break; - case ELGAMAL: + } + case ELGAMAL: { int[] elGammalKeyDiff = new int[elGamalSupportedLengths.length]; for (int i = 0; i < elGamalSupportedLengths.length; i++) { elGammalKeyDiff[i] = Math.abs(elGamalSupportedLengths[i] - currentKeyLength); @@ -386,11 +386,14 @@ public class AddSubkeyDialogFragment extends DialogFragment { } properKeyLength = elGamalSupportedLengths[minimalIndex]; break; - case DSA: - if (currentKeyLength >= 512 && currentKeyLength <= 1024) { + } + case DSA: { + // Bouncy Castle supports 4096 maximum + if (currentKeyLength >= 2048 && currentKeyLength <= 4096) { properKeyLength = currentKeyLength + ((64 - (currentKeyLength % 64)) % 64); } break; + } } return properKeyLength; } @@ -424,7 +427,7 @@ public class AddSubkeyDialogFragment extends DialogFragment { final ArrayAdapter<CharSequence> keySizeAdapter = (ArrayAdapter<CharSequence>) mKeySizeSpinner.getAdapter(); keySizeAdapter.clear(); switch (algorithm) { - case RSA: + case RSA: { replaceArrayAdapterContent(keySizeAdapter, R.array.rsa_key_size_spinner_values); mKeySizeSpinner.setSelection(1); mKeySizeRow.setVisibility(View.VISIBLE); @@ -450,7 +453,8 @@ public class AddSubkeyDialogFragment extends DialogFragment { } mFlagAuthenticate.setChecked(false); break; - case ELGAMAL: + } + case ELGAMAL: { replaceArrayAdapterContent(keySizeAdapter, R.array.elgamal_key_size_spinner_values); mKeySizeSpinner.setSelection(3); mKeySizeRow.setVisibility(View.VISIBLE); @@ -466,7 +470,8 @@ public class AddSubkeyDialogFragment extends DialogFragment { mFlagAuthenticate.setChecked(false); mFlagAuthenticate.setEnabled(false); break; - case DSA: + } + case DSA: { replaceArrayAdapterContent(keySizeAdapter, R.array.dsa_key_size_spinner_values); mKeySizeSpinner.setSelection(2); mKeySizeRow.setVisibility(View.VISIBLE); @@ -482,7 +487,8 @@ public class AddSubkeyDialogFragment extends DialogFragment { mFlagAuthenticate.setChecked(false); mFlagAuthenticate.setEnabled(false); break; - case ECDSA: + } + case ECDSA: { mKeySizeRow.setVisibility(View.GONE); mCurveRow.setVisibility(View.VISIBLE); mCustomKeyInfoTextView.setText(""); @@ -496,7 +502,8 @@ public class AddSubkeyDialogFragment extends DialogFragment { mFlagAuthenticate.setEnabled(true); mFlagAuthenticate.setChecked(false); break; - case ECDH: + } + case ECDH: { mKeySizeRow.setVisibility(View.GONE); mCurveRow.setVisibility(View.VISIBLE); mCustomKeyInfoTextView.setText(""); @@ -510,6 +517,7 @@ public class AddSubkeyDialogFragment extends DialogFragment { mFlagAuthenticate.setChecked(false); mFlagAuthenticate.setEnabled(false); break; + } } keySizeAdapter.notifyDataSetChanged(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java deleted file mode 100644 index 84774ae5e..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.ui.dialog; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; -import android.support.v4.app.DialogFragment; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.ui.util.Notify; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.Log; - -import java.io.File; - -/** - * This is a file chooser dialog no longer used with KitKat - */ -public class FileDialogFragment extends DialogFragment { - private static final String ARG_MESSENGER = "messenger"; - private static final String ARG_TITLE = "title"; - private static final String ARG_MESSAGE = "message"; - private static final String ARG_DEFAULT_FILE = "default_file"; - private static final String ARG_CHECKBOX_TEXT = "checkbox_text"; - - public static final int MESSAGE_OKAY = 1; - - public static final String MESSAGE_DATA_FILE = "file"; - public static final String MESSAGE_DATA_CHECKED = "checked"; - - private Messenger mMessenger; - - private EditText mFilename; - private ImageButton mBrowse; - private CheckBox mCheckBox; - private TextView mMessageTextView; - - private File mFile; - - private static final int REQUEST_CODE = 0x00007004; - - /** - * Creates new instance of this file dialog fragment - */ - public static FileDialogFragment newInstance(Messenger messenger, String title, String message, - File defaultFile, String checkboxText) { - FileDialogFragment frag = new FileDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_MESSENGER, messenger); - - args.putString(ARG_TITLE, title); - args.putString(ARG_MESSAGE, message); - args.putString(ARG_DEFAULT_FILE, defaultFile.getAbsolutePath()); - args.putString(ARG_CHECKBOX_TEXT, checkboxText); - - frag.setArguments(args); - - return frag; - } - - /** - * Creates dialog - */ - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity activity = getActivity(); - - mMessenger = getArguments().getParcelable(ARG_MESSENGER); - - String title = getArguments().getString(ARG_TITLE); - String message = getArguments().getString(ARG_MESSAGE); - mFile = new File(getArguments().getString(ARG_DEFAULT_FILE)); - if (!mFile.isAbsolute()) { - // We use OK dir by default - mFile = new File(Constants.Path.APP_DIR.getAbsolutePath(), mFile.getName()); - } - String checkboxText = getArguments().getString(ARG_CHECKBOX_TEXT); - - LayoutInflater inflater = (LayoutInflater) activity - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(activity); - alert.setTitle(title); - - View view = inflater.inflate(R.layout.file_dialog, null); - - mMessageTextView = (TextView) view.findViewById(R.id.message); - mMessageTextView.setText(message); - - mFilename = (EditText) view.findViewById(R.id.input); - mFilename.setText(mFile.getName()); - mBrowse = (ImageButton) view.findViewById(R.id.btn_browse); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - mBrowse.setVisibility(View.GONE); - } else { - mBrowse.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - // only .asc or .gpg files - // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc - // or gpg types! - FileHelper.saveDocumentKitKat( - FileDialogFragment.this, "*/*", mFile.getName(), REQUEST_CODE); - } - }); - } - - mCheckBox = (CheckBox) view.findViewById(R.id.checkbox); - if (checkboxText == null) { - mCheckBox.setEnabled(false); - mCheckBox.setVisibility(View.GONE); - } else { - mCheckBox.setEnabled(true); - mCheckBox.setVisibility(View.VISIBLE); - mCheckBox.setText(checkboxText); - mCheckBox.setChecked(true); - } - - alert.setView(view); - - alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int id) { - dismiss(); - - String currentFilename = mFilename.getText().toString(); - if (currentFilename == null || currentFilename.isEmpty()) { - // No file is like pressing cancel, UI: maybe disable positive button in this case? - return; - } - - if (mFile == null || currentFilename.startsWith("/")) { - mFile = new File(currentFilename); - } else if (!mFile.getName().equals(currentFilename)) { - // We update our File object if user changed name! - mFile = new File(mFile.getParentFile(), currentFilename); - } - - boolean checked = mCheckBox.isEnabled() && mCheckBox.isChecked(); - - // return resulting data back to activity - Bundle data = new Bundle(); - data.putString(MESSAGE_DATA_FILE, mFile.getAbsolutePath()); - data.putBoolean(MESSAGE_DATA_CHECKED, checked); - - sendMessageToHandler(MESSAGE_OKAY, data); - } - }); - - alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int id) { - dismiss(); - } - }); - return alert.show(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode & 0xFFFF) { - case REQUEST_CODE: { - if (resultCode == Activity.RESULT_OK && data != null) { - File file = new File(data.getData().getPath()); - if (file.getParentFile().exists()) { - mFile = file; - mFilename.setText(mFile.getName()); - } else { - Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR).show(); - } - } - - break; - } - - default: - super.onActivityResult(requestCode, resultCode, data); - - break; - } - } - - /** - * Send message back to handler which is initialized in a activity - * - * @param what Message integer you want to send - */ - private void sendMessageToHandler(Integer what, Bundle data) { - Message msg = Message.obtain(); - msg.what = what; - if (data != null) { - msg.setData(data); - } - - try { - mMessenger.send(msg); - } catch (RemoteException e) { - Log.w(Constants.TAG, "Exception sending message, Is handler present?", e); - } catch (NullPointerException e) { - Log.w(Constants.TAG, "Messenger is null!", e); - } - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareLogDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareLogDialogFragment.java new file mode 100644 index 000000000..d4783fad2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/ShareLogDialogFragment.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 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.dialog; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.view.ContextThemeWrapper; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; + +public class ShareLogDialogFragment extends DialogFragment { + private static final String ARG_STREAM = "stream"; + + public static ShareLogDialogFragment newInstance(Uri stream) { + Bundle args = new Bundle(); + args.putParcelable(ARG_STREAM, stream); + + ShareLogDialogFragment fragment = new ShareLogDialogFragment(); + fragment.setArguments(args); + + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + + final Uri stream = getArguments().getParcelable(ARG_STREAM); + + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(getActivity()); + + CustomAlertDialogBuilder builder = new CustomAlertDialogBuilder(theme); + builder.setTitle(R.string.share_log_dialog_title) + .setMessage(R.string.share_log_dialog_message) + .setNegativeButton(R.string.share_log_dialog_cancel_button, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }) + .setPositiveButton(R.string.share_log_dialog_share_button, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_STREAM, stream); + intent.setType("text/plain"); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(intent); + } + }); + + return builder.show(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java index 22a201ba3..a320ea3b2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java @@ -27,7 +27,6 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.EditText; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource; @@ -35,7 +34,6 @@ import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.FileHelper; -import java.io.File; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.net.URI; @@ -134,10 +132,8 @@ public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragmen String targetName = "pgpkey.txt"; - FileHelper.saveDocument(this, - targetName, Uri.fromFile(new File(Constants.Path.APP_DIR, targetName)), - "text/plain", R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, - REQUEST_CODE_OUTPUT); + // TODO: not supported on Android < 4.4 + FileHelper.saveDocument(this, targetName, "text/plain", REQUEST_CODE_OUTPUT); } private void saveFile(Uri uri) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java index 8f5753dae..b9b837d71 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/KeyFormattingUtils.java @@ -28,6 +28,7 @@ import android.text.style.ForegroundColorSpan; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import android.widget.ViewAnimator; import org.openintents.openpgp.OpenPgpDecryptionResult; import org.openintents.openpgp.OpenPgpSignatureResult; @@ -440,14 +441,15 @@ public class KeyFormattingUtils { View getSignatureLayout(); TextView getSignatureUserName(); TextView getSignatureUserEmail(); - TextView getSignatureAction(); + ViewAnimator getSignatureAction(); boolean hasEncrypt(); } @SuppressWarnings("deprecation") // context.getDrawable is api lvl 21, need to use deprecated - public static void setStatus(Resources resources, StatusHolder holder, DecryptVerifyResult result) { + public static void setStatus(Resources resources, StatusHolder holder, DecryptVerifyResult result, + boolean processingkeyLookup) { if (holder.hasEncrypt()) { OpenPgpDecryptionResult decryptionResult = result.getDecryptionResult(); @@ -488,7 +490,7 @@ public class KeyFormattingUtils { OpenPgpSignatureResult signatureResult = result.getSignatureResult(); int sigText, sigIcon, sigColor; - int sigActionText, sigActionIcon; + int sigActionDisplayedChild; switch (signatureResult.getResult()) { @@ -500,8 +502,7 @@ public class KeyFormattingUtils { sigColor = R.color.key_flag_gray; // won't be used, but makes compiler happy - sigActionText = 0; - sigActionIcon = 0; + sigActionDisplayedChild = -1; break; } @@ -510,8 +511,7 @@ public class KeyFormattingUtils { sigIcon = R.drawable.status_signature_verified_cutout_24dp; sigColor = R.color.key_flag_green; - sigActionText = R.string.decrypt_result_action_show; - sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + sigActionDisplayedChild = 0; break; } @@ -520,8 +520,7 @@ public class KeyFormattingUtils { sigIcon = R.drawable.status_signature_unverified_cutout_24dp; sigColor = R.color.key_flag_orange; - sigActionText = R.string.decrypt_result_action_show; - sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + sigActionDisplayedChild = 0; break; } @@ -530,8 +529,7 @@ public class KeyFormattingUtils { sigIcon = R.drawable.status_signature_revoked_cutout_24dp; sigColor = R.color.key_flag_red; - sigActionText = R.string.decrypt_result_action_show; - sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + sigActionDisplayedChild = 0; break; } @@ -540,8 +538,7 @@ public class KeyFormattingUtils { sigIcon = R.drawable.status_signature_expired_cutout_24dp; sigColor = R.color.key_flag_red; - sigActionText = R.string.decrypt_result_action_show; - sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + sigActionDisplayedChild = 0; break; } @@ -550,8 +547,7 @@ public class KeyFormattingUtils { sigIcon = R.drawable.status_signature_unknown_cutout_24dp; sigColor = R.color.key_flag_red; - sigActionText = R.string.decrypt_result_action_Lookup; - sigActionIcon = R.drawable.ic_file_download_grey_24dp; + sigActionDisplayedChild = 1; break; } @@ -560,8 +556,7 @@ public class KeyFormattingUtils { sigIcon = R.drawable.status_signature_invalid_cutout_24dp; sigColor = R.color.key_flag_red; - sigActionText = R.string.decrypt_result_action_show; - sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + sigActionDisplayedChild = 0; break; } @@ -571,27 +566,31 @@ public class KeyFormattingUtils { sigIcon = R.drawable.status_signature_invalid_cutout_24dp; sigColor = R.color.key_flag_red; - sigActionText = R.string.decrypt_result_action_show; - sigActionIcon = R.drawable.ic_vpn_key_grey_24dp; + // won't be used, but makes compiler happy + sigActionDisplayedChild = -1; break; } } + // possibly switch out "Lookup" button for progress bar + if (sigActionDisplayedChild == 1 && processingkeyLookup) { + sigActionDisplayedChild = 2; + } + int sigColorRes = resources.getColor(sigColor); holder.getSignatureStatusIcon().setColorFilter(sigColorRes, PorterDuff.Mode.SRC_IN); holder.getSignatureStatusIcon().setImageDrawable(resources.getDrawable(sigIcon)); holder.getSignatureStatusText().setText(sigText); holder.getSignatureStatusText().setTextColor(sigColorRes); - if (signatureResult.getResult() != OpenPgpSignatureResult.RESULT_NO_SIGNATURE) { + if (signatureResult.getResult() != OpenPgpSignatureResult.RESULT_NO_SIGNATURE + && signatureResult.getResult() != OpenPgpSignatureResult.RESULT_INVALID_SIGNATURE) { // has a signature, thus display layouts holder.getSignatureLayout().setVisibility(View.VISIBLE); - holder.getSignatureAction().setText(sigActionText); - holder.getSignatureAction().setCompoundDrawablesWithIntrinsicBounds( - 0, 0, sigActionIcon, 0); + holder.getSignatureAction().setDisplayedChild(sigActionDisplayedChild); String userId = result.getSignatureResult().getPrimaryUserId(); KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java index 7dfd56430..71f6ecc1a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/Notify.java @@ -37,7 +37,7 @@ import org.sufficientlysecure.keychain.util.FabContainer; */ public class Notify { - public static enum Style { + public enum Style { OK (R.color.android_green_light), WARN(R.color.android_orange_light), ERROR(R.color.android_red_light); public final int mLineColor; @@ -142,6 +142,11 @@ public class Notify { return create(activity, text, LENGTH_LONG, style); } + public static Showable create(Activity activity, int textResId, Style style, + ActionListener actionListener, int actionResId) { + return create(activity, textResId, LENGTH_LONG, style, actionListener, actionResId); + } + public static Showable create(Activity activity, int textResId, int duration, Style style, ActionListener actionListener, int actionResId) { return create(activity, activity.getString(textResId), duration, style, actionListener, actionResId); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java index 48e6c2cee..01d51af48 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/EncryptKeyCompletionView.java @@ -23,11 +23,13 @@ import android.database.Cursor; import android.graphics.Rect; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -46,14 +48,14 @@ import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; import org.sufficientlysecure.keychain.util.Log; -public class EncryptKeyCompletionView extends TokenCompleteTextView +public class EncryptKeyCompletionView extends TokenCompleteTextView<KeyItem> implements LoaderCallbacks<Cursor> { public static final String ARG_QUERY = "query"; private KeyAdapter mAdapter; private LoaderManager mLoaderManager; - private String mPrefix; + private CharSequence mPrefix; public EncryptKeyCompletionView(Context context) { super(context); @@ -79,30 +81,27 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView } @Override - public void setPrefix(String p) { + public void setPrefix(CharSequence p) { // this one is private in the superclass, but we need it here mPrefix = p; super.setPrefix(p); } @Override - protected View getViewForObject(Object object) { - if (object instanceof KeyItem) { - LayoutInflater l = LayoutInflater.from(getContext()); - View view = l.inflate(R.layout.recipient_box_entry, null); - ((TextView) view.findViewById(android.R.id.text1)).setText(((KeyItem) object).getReadableName()); - return view; - } - return null; + protected View getViewForObject(KeyItem keyItem) { + LayoutInflater l = LayoutInflater.from(getContext()); + View view = l.inflate(R.layout.recipient_box_entry, null); + ((TextView) view.findViewById(android.R.id.text1)).setText(keyItem.getReadableName()); + return view; } @Override - protected Object defaultObject(String completionText) { + protected KeyItem defaultObject(String completionText) { // TODO: We could try to automagically download the key if it's unknown but a key id /*if (completionText.startsWith("0x")) { }*/ - return ""; + return null; } @Override @@ -128,7 +127,7 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView // These are the rows that we will retrieve. Uri baseUri = KeyRings.buildUnifiedKeyRingsUri(); - String[] projection = KeyAdapter.getProjectionWith(new String[] { + String[] projection = KeyAdapter.getProjectionWith(new String[]{ KeychainContract.KeyRings.HAS_ENCRYPT, }); @@ -136,18 +135,19 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView + KeyRings.IS_EXPIRED + " = 0 AND " + Tables.KEYS + "." + KeyRings.IS_REVOKED + " = 0"; - if (args != null && args.containsKey(ARG_QUERY)) { - String query = args.getString(ARG_QUERY); - mAdapter.setSearchQuery(query); + if (args == null || !args.containsKey(ARG_QUERY)) { + // mAdapter.setSearchQuery(null); + // return new CursorLoader(getContext(), baseUri, projection, where, null, null); + return null; + } - where += " AND " + KeyRings.USER_ID + " LIKE ?"; + String query = args.getString(ARG_QUERY); + mAdapter.setSearchQuery(query); - return new CursorLoader(getContext(), baseUri, projection, where, - new String[]{"%" + query + "%"}, null); - } + where += " AND " + KeyRings.USER_ID + " LIKE ?"; - mAdapter.setSearchQuery(null); - return new CursorLoader(getContext(), baseUri, projection, where, null, null); + return new CursorLoader(getContext(), baseUri, projection, where, + new String[]{"%" + query + "%"}, null); } @@ -169,6 +169,8 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView super.showDropDown(); } + + @Override public void onFocusChanged(boolean hasFocus, int direction, Rect previous) { super.onFocusChanged(hasFocus, direction, previous); @@ -179,13 +181,18 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView } @Override - protected void performFiltering(CharSequence text, int start, int end, int keyCode) { + protected void performFiltering(@NonNull CharSequence text, int start, int end, int keyCode) { super.performFiltering(text, start, end, keyCode); if (start < mPrefix.length()) { start = mPrefix.length(); } + String query = text.subSequence(start, end).toString(); + if (TextUtils.isEmpty(query) || query.length() < 2) { + mLoaderManager.destroyLoader(0); + return; + } Bundle args = new Bundle(); - args.putString(ARG_QUERY, text.subSequence(start, end).toString()); + args.putString(ARG_QUERY, query); mLoaderManager.restartLoader(0, args, this); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java index 18e830139..a8274e45a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/ToolableViewAnimator.java @@ -31,6 +31,7 @@ import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import android.view.animation.Animation; import android.widget.ViewAnimator; import org.sufficientlysecure.keychain.R; @@ -73,4 +74,29 @@ public class ToolableViewAnimator extends ViewAnimator { } super.addView(child, index, params); } + + @Override + public void setDisplayedChild(int whichChild) { + if (whichChild != getDisplayedChild()) { + super.setDisplayedChild(whichChild); + } + } + + public void setDisplayedChild(int whichChild, boolean animate) { + if (animate) { + setDisplayedChild(whichChild); + return; + } + + Animation savedInAnim = getInAnimation(); + Animation savedOutAnim = getOutAnimation(); + setInAnimation(null); + setOutAnimation(null); + + setDisplayedChild(whichChild); + + setInAnimation(savedInAnim); + setOutAnimation(savedOutAnim); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java index d7491ab26..a55249842 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/EmailKeyHelper.java @@ -74,9 +74,9 @@ public class EmailKeyHelper { // Try _hkp._tcp SRV record first String[] mailparts = mail.split("@"); if (mailparts.length == 2) { - HkpKeyserver hkp = HkpKeyserver.resolve(mailparts[1]); + HkpKeyserver hkp = HkpKeyserver.resolve(mailparts[1], proxy); if (hkp != null) { - keys.addAll(getEmailKeys(mail, hkp, proxy)); + keys.addAll(getEmailKeys(mail, hkp)); } } @@ -84,18 +84,17 @@ public class EmailKeyHelper { // Most users don't have the SRV record, so ask a default server as well String server = Preferences.getPreferences(context).getPreferredKeyserver(); if (server != null) { - HkpKeyserver hkp = new HkpKeyserver(server); - keys.addAll(getEmailKeys(mail, hkp, proxy)); + HkpKeyserver hkp = new HkpKeyserver(server, proxy); + keys.addAll(getEmailKeys(mail, hkp)); } } return keys; } - public static List<ImportKeysListEntry> getEmailKeys(String mail, Keyserver keyServer, - Proxy proxy) { + public static List<ImportKeysListEntry> getEmailKeys(String mail, Keyserver keyServer) { Set<ImportKeysListEntry> keys = new HashSet<>(); try { - for (ImportKeysListEntry key : keyServer.search(mail, proxy)) { + for (ImportKeysListEntry key : keyServer.search(mail)) { if (key.isRevoked() || key.isExpired()) continue; for (String userId : key.getUserIds()) { if (userId.toLowerCase().contains(mail.toLowerCase(Locale.ENGLISH))) { @@ -103,8 +102,7 @@ public class EmailKeyHelper { } } } - } catch (Keyserver.QueryFailedException ignored) { - } catch (Keyserver.QueryNeedsRepairException ignored) { + } catch (Keyserver.CloudSearchFailureException ignored) { } return new ArrayList<>(keys); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java deleted file mode 100644 index 45dc33906..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.util; - - -import java.io.File; - -import android.content.Intent; -import android.net.Uri; -import android.support.v4.app.FragmentActivity; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.operations.results.ExportResult; -import org.sufficientlysecure.keychain.service.ExportKeyringParcel; -import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; - -public class ExportHelper - implements CryptoOperationHelper.Callback <ExportKeyringParcel, ExportResult> { - protected File mExportFile; - - FragmentActivity mActivity; - - private boolean mExportSecret; - private long[] mMasterKeyIds; - - public ExportHelper(FragmentActivity activity) { - super(); - this.mActivity = activity; - } - - /** Show dialog where to export keys */ - public void showExportKeysDialog(final Long masterKeyId, final File exportFile, - final boolean exportSecret) { - mExportFile = exportFile; - - String title; - if (masterKeyId == null) { - // export all keys - title = mActivity.getString(R.string.title_export_keys); - } else { - // export only key specified at data uri - title = mActivity.getString(R.string.title_export_key); - } - - String message; - if (exportSecret) { - message = mActivity.getString(masterKeyId == null - ? R.string.specify_backup_dest_secret - : R.string.specify_backup_dest_secret_single); - } else { - message = mActivity.getString(masterKeyId == null - ? R.string.specify_backup_dest - : R.string.specify_backup_dest_single); - } - - FileHelper.saveDocumentDialog(new FileHelper.FileDialogCallback() { - @Override - public void onFileSelected(File file, boolean checked) { - mExportFile = file; - exportKeys(masterKeyId == null ? null : new long[] { masterKeyId }, exportSecret); - } - }, mActivity.getSupportFragmentManager(), title, message, exportFile, null); - } - - // TODO: If ExportHelper requires pending data (see CryptoOPerationHelper), activities using - // TODO: this class should be able to call mExportOpHelper.handleActivity - - /** - * Export keys - */ - public void exportKeys(long[] masterKeyIds, boolean exportSecret) { - Log.d(Constants.TAG, "exportKeys started"); - mExportSecret = exportSecret; - mMasterKeyIds = masterKeyIds; // if masterKeyIds is null it means export all - - CryptoOperationHelper<ExportKeyringParcel, ExportResult> exportOpHelper = - new CryptoOperationHelper<>(1, mActivity, this, R.string.progress_exporting); - exportOpHelper.cryptoOperation(); - } - - @Override - public ExportKeyringParcel createOperationInput() { - return new ExportKeyringParcel(mMasterKeyIds, mExportSecret, mExportFile.getAbsolutePath()); - } - - @Override - final public void onCryptoOperationSuccess(ExportResult result) { - // trigger scan of the created 'media' file so it shows up on MTP - // http://stackoverflow.com/questions/13737261/nexus-4-not-showing-files-via-mtp - mActivity.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mExportFile))); - result.createNotify(mActivity).show(); - } - - @Override - public void onCryptoOperationCancelled() { - - } - - @Override - public void onCryptoOperationError(ExportResult result) { - result.createNotify(mActivity).show(); - } - - @Override - public boolean onCryptoSetProgress(String msg, int progress, int max) { - return false; - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java index 9fb362412..bae119700 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java @@ -17,8 +17,20 @@ package org.sufficientlysecure.keychain.util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.SecureRandom; +import java.text.DecimalFormat; + import android.annotation.TargetApi; -import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Context; @@ -30,30 +42,12 @@ import android.net.Uri; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Environment; -import android.os.Handler; -import android.os.Message; -import android.os.Messenger; import android.provider.DocumentsContract; import android.provider.OpenableColumns; -import android.support.annotation.StringRes; import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; import android.widget.Toast; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; -import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.text.DecimalFormat; - /** This class offers a number of helper functions for saving documents. * @@ -81,53 +75,31 @@ import java.text.DecimalFormat; */ public class FileHelper { - public static void openDocument(Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - openDocumentPreKitKat(fragment, last, mimeType, multiple, requestCode); - } else { - openDocumentKitKat(fragment, mimeType, multiple, requestCode); - } + @TargetApi(VERSION_CODES.KITKAT) + public static void saveDocument(Fragment fragment, String targetName, int requestCode) { + saveDocument(fragment, targetName, "*/*", requestCode); } - public static void saveDocument(Fragment fragment, String targetName, Uri inputUri, - @StringRes int title, @StringRes int message, int requestCode) { - saveDocument(fragment, targetName, inputUri, "*/*", title, message, requestCode); + /** Opens the storage browser on Android 4.4 or later for saving a file. */ + @TargetApi(VERSION_CODES.KITKAT) + public static void saveDocument(Fragment fragment, String suggestedName, String mimeType, int requestCode) { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(mimeType); + // Note: This is not documented, but works: Show the Internal Storage menu item in the drawer! + intent.putExtra("android.content.extra.SHOW_ADVANCED", true); + intent.putExtra(Intent.EXTRA_TITLE, suggestedName); + fragment.startActivityForResult(intent, requestCode); } - public static void saveDocument(Fragment fragment, String targetName, Uri inputUri, String mimeType, - @StringRes int title, @StringRes int message, int requestCode) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - saveDocumentDialog(fragment, targetName, inputUri, title, message, requestCode); + public static void openDocument(Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + openDocumentKitKat(fragment, mimeType, multiple, requestCode); } else { - saveDocumentKitKat(fragment, mimeType, targetName, requestCode); + openDocumentPreKitKat(fragment, last, mimeType, multiple, requestCode); } } - public static void saveDocumentDialog(final Fragment fragment, String targetName, Uri inputUri, - @StringRes int title, @StringRes int message, final int requestCode) { - - saveDocumentDialog(fragment, targetName, inputUri, title, message, new FileDialogCallback() { - // is this a good idea? seems hacky... - @Override - public void onFileSelected(File file, boolean checked) { - Intent intent = new Intent(); - intent.setData(Uri.fromFile(file)); - fragment.onActivityResult(requestCode, Activity.RESULT_OK, intent); - } - }); - } - - public static void saveDocumentDialog(final Fragment fragment, String targetName, Uri inputUri, - @StringRes int title, @StringRes int message, FileDialogCallback callback) { - - File file = inputUri == null ? null : new File(inputUri.getPath()); - File parentDir = file != null && file.exists() ? file.getParentFile() : Constants.Path.APP_DIR; - File targetFile = new File(parentDir, targetName); - saveDocumentDialog(callback, fragment.getActivity().getSupportFragmentManager(), - fragment.getString(title), fragment.getString(message), targetFile, null); - - } - /** Opens the preferred installed file manager on Android and shows a toast * if no manager is installed. */ private static void openDocumentPreKitKat( @@ -157,51 +129,12 @@ public class FileHelper { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(mimeType); + // Note: This is not documented, but works: Show the Internal Storage menu item in the drawer! + intent.putExtra("android.content.extra.SHOW_ADVANCED", true); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple); fragment.startActivityForResult(intent, requestCode); } - /** Opens the storage browser on Android 4.4 or later for saving a file. */ - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void saveDocumentKitKat(Fragment fragment, String mimeType, String suggestedName, int requestCode) { - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType(mimeType); - intent.putExtra("android.content.extra.SHOW_ADVANCED", true); // Note: This is not documented, but works - intent.putExtra(Intent.EXTRA_TITLE, suggestedName); - fragment.startActivityForResult(intent, requestCode); - } - - public static void saveDocumentDialog( - final FileDialogCallback callback, final FragmentManager fragmentManager, - final String title, final String message, final File defaultFile, - final String checkMsg) { - // Message is received after file is selected - Handler returnHandler = new Handler() { - @Override - public void handleMessage(Message message) { - if (message.what == FileDialogFragment.MESSAGE_OKAY) { - callback.onFileSelected( - new File(message.getData().getString(FileDialogFragment.MESSAGE_DATA_FILE)), - message.getData().getBoolean(FileDialogFragment.MESSAGE_DATA_CHECKED)); - } - } - }; - - // Create a new Messenger for the communication back - final Messenger messenger = new Messenger(returnHandler); - - DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() { - @Override - public void run() { - FileDialogFragment fileDialog = FileDialogFragment.newInstance(messenger, title, message, - defaultFile, checkMsg); - - fileDialog.show(fragmentManager, "fileDialog"); - } - }); - } - public static String getFilename(Context context, Uri uri) { String filename = null; try { @@ -228,6 +161,14 @@ public class FileHelper { } public static long getFileSize(Context context, Uri uri, long def) { + if ("file".equals(uri.getScheme())) { + long size = new File(uri.getPath()).length(); + if (size == 0) { + size = def; + } + return size; + } + long size = def; try { Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null); @@ -322,6 +263,44 @@ public class FileHelper { } } + /** + * Deletes data at a URI securely by overwriting it with random data + * before deleting it. This method is fail-fast - if we can't securely + * delete the file, we don't delete it at all. + */ + public static int deleteFileSecurely(Context context, Uri uri) + throws IOException { + + ContentResolver resolver = context.getContentResolver(); + long lengthLeft = FileHelper.getFileSize(context, uri); + + if (lengthLeft == -1) { + throw new IOException("Error opening file!"); + } + + SecureRandom random = new SecureRandom(); + byte[] randomData = new byte[1024]; + + OutputStream out = resolver.openOutputStream(uri, "w"); + if (out == null) { + throw new IOException("Error opening file!"); + } + out = new BufferedOutputStream(out); + while (lengthLeft > 0) { + random.nextBytes(randomData); + out.write(randomData, 0, lengthLeft > randomData.length ? randomData.length : (int) lengthLeft); + lengthLeft -= randomData.length; + } + out.close(); + + if ("file".equals(uri.getScheme())) { + return new File(uri.getPath()).delete() ? 1 : 0; + } else { + return resolver.delete(uri, null, null); + } + + } + /** Checks if external storage is mounted if file is located on external storage. */ public static boolean isStorageMounted(String file) { if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) { @@ -333,7 +312,24 @@ public class FileHelper { return true; } - public interface FileDialogCallback { - void onFileSelected(File file, boolean checked); + /** A replacement for ContentResolver.openInputStream() that does not allow + * the usage of "file" Uris that point to private files owned by the + * application only, *on Lollipop devices*. + * + * The check will be performed on devices >= Lollipop only, which have the + * necessary API to stat filedescriptors. + * + * @see FileHelperLollipop + */ + public static InputStream openInputStreamSafe(ContentResolver resolver, Uri uri) + throws FileNotFoundException { + + // Not supported on Android < 5 + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + return FileHelperLollipop.openInputStreamSafe(resolver, uri); + } else { + return resolver.openInputStream(uri); + } } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelperLollipop.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelperLollipop.java new file mode 100644 index 000000000..f89d679bc --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelperLollipop.java @@ -0,0 +1,82 @@ +package org.sufficientlysecure.keychain.util; + + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructStat; + +import org.sufficientlysecure.keychain.Constants; + +import static android.system.OsConstants.S_IROTH; + + +/** FileHelper methods which use Lollipop-exclusive API. + * Some of the methods and static fields used here cause VerifyErrors because + * they do not exist in pre-lollipop API, so they must be kept in a + * lollipop-only class. All methods here should only be called by FileHelper, + * and consequently have package visibility. + */ +@TargetApi(VERSION_CODES.LOLLIPOP) +class FileHelperLollipop { + /** + * Tests whether a file is readable by others + */ + private static boolean S_IROTH(int mode) { + return (mode & S_IROTH) == S_IROTH; + } + + /** + * A replacement for ContentResolver.openInputStream() that does not allow the usage of + * "file" Uris that point to private files owned by the application only. + * + * This is not allowed: + * am start -a android.intent.action.SEND -t text/plain -n + * "org.sufficientlysecure.keychain.debug/org.sufficientlysecure.keychain.ui.EncryptFilesActivity" --eu + * android.intent.extra.STREAM + * file:///data/data/org.sufficientlysecure.keychain.debug/databases/openkeychain.db + * + * @throws FileNotFoundException + */ + static InputStream openInputStreamSafe(ContentResolver resolver, Uri uri) + throws FileNotFoundException { + + String scheme = uri.getScheme(); + if (ContentResolver.SCHEME_FILE.equals(scheme)) { + ParcelFileDescriptor pfd = ParcelFileDescriptor.open( + new File(uri.getPath()), ParcelFileDescriptor.parseMode("r")); + + try { + final StructStat st = Os.fstat(pfd.getFileDescriptor()); + if (!S_IROTH(st.st_mode)) { + Log.e(Constants.TAG, "File is not readable by others, aborting!"); + throw new FileNotFoundException("Unable to create stream"); + } + } catch (ErrnoException e) { + Log.e(Constants.TAG, "fstat() failed: " + e); + throw new FileNotFoundException("fstat() failed"); + } + + AssetFileDescriptor fd = new AssetFileDescriptor(pfd, 0, -1); + try { + return fd.createInputStream(); + } catch (IOException e) { + throw new FileNotFoundException("Unable to create stream"); + } + } else { + return resolver.openInputStream(uri); + } + + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java index ab73f59b8..d06f2ab65 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + package org.sufficientlysecure.keychain.util; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/NfcHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/NfcHelper.java index 2b47fd623..af4e0d4f8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/NfcHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/NfcHelper.java @@ -98,7 +98,7 @@ public class NfcHelper { * guarantee that this activity starts when receiving a beamed message. For now, this code * uses the tag dispatch system. */ - return new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME, + return new NdefMessage(NdefRecord.createMime(Constants.MIME_TYPE_KEYS, mNfcKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME)); } }; @@ -141,6 +141,10 @@ public class NfcHelper { } protected void onPostExecute(Void unused) { + if (mActivity.isFinishing()) { + return; + } + // Register callback to set NDEF message mNfcAdapter.setNdefPushMessageCallback(mNdefCallback, mActivity); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpKeybaseClient.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpKeybaseClient.java new file mode 100644 index 000000000..d2c90cfcd --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/OkHttpKeybaseClient.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.util; + +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.OkUrlFactory; +import com.textuality.keybase.lib.KeybaseUrlConnectionClient; + +import org.sufficientlysecure.keychain.Constants; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.util.concurrent.TimeUnit; + +/** + * Wrapper for Keybase Lib + */ +public class OkHttpKeybaseClient implements KeybaseUrlConnectionClient { + + private OkUrlFactory generateUrlFactory() { + OkHttpClient client = new OkHttpClient(); + return new OkUrlFactory(client); + } + + @Override + public URLConnection openConnection(URL url, Proxy proxy, boolean isKeybase) throws IOException { + OkUrlFactory factory = generateUrlFactory(); + if (proxy != null) { + factory.client().setProxy(proxy); + factory.client().setConnectTimeout(30000, TimeUnit.MILLISECONDS); + factory.client().setReadTimeout(40000, TimeUnit.MILLISECONDS); + } else { + factory.client().setConnectTimeout(5000, TimeUnit.MILLISECONDS); + factory.client().setReadTimeout(25000, TimeUnit.MILLISECONDS); + } + + factory.client().setFollowSslRedirects(false); + + // forced the usage of api.keybase.io pinned certificate + if (isKeybase) { + try { + if (!TlsHelper.usePinnedCertificateIfAvailable(factory.client(), url)) { + throw new IOException("no pinned certificate found for URL!"); + } + } catch (TlsHelper.TlsHelperException e) { + Log.e(Constants.TAG, "TlsHelper failed", e); + throw new IOException("TlsHelper failed"); + } + } + + return factory.open(url); + } + + @Override + public String getKeybaseBaseUrl() { + return "https://api.keybase.io/"; + } + +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java index 7e788d04c..7e2328e99 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ParcelableProxy.java @@ -17,12 +17,14 @@ package org.sufficientlysecure.keychain.util; -import android.os.Parcel; -import android.os.Parcelable; import java.net.InetSocketAddress; import java.net.Proxy; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; + /** * used to simply transport java.net.Proxy objects created using InetSockets between services/activities */ @@ -47,9 +49,10 @@ public class ParcelableProxy implements Parcelable { return new ParcelableProxy(null, -1, null); } + @NonNull public Proxy getProxy() { if (mProxyHost == null) { - return null; + return Proxy.NO_PROXY; } /* * InetSocketAddress.createUnresolved so we can use this method even in the main thread diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java index 4ef215036..559c5556f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -23,6 +23,9 @@ import android.content.SharedPreferences; import android.content.res.Resources; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants.Pref; import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; @@ -123,16 +126,6 @@ public class Preferences { return mSharedPreferences.getBoolean(Constants.Pref.FIRST_TIME, true); } - public boolean useDefaultYubiKeyPin() { - return mSharedPreferences.getBoolean(Pref.USE_DEFAULT_YUBIKEY_PIN, false); - } - - public void setUseDefaultYubiKeyPin(boolean useDefaultYubikeyPin) { - SharedPreferences.Editor editor = mSharedPreferences.edit(); - editor.putBoolean(Pref.USE_DEFAULT_YUBIKEY_PIN, useDefaultYubikeyPin); - editor.commit(); - } - public boolean useNumKeypadForYubiKeyPin() { return mSharedPreferences.getBoolean(Pref.USE_NUMKEYPAD_FOR_YUBIKEY_PIN, true); } @@ -332,6 +325,12 @@ public class Preferences { if (!torEnabled && !normalPorxyEnabled) this.parcelableProxy = new ParcelableProxy(null, -1, null); else this.parcelableProxy = new ParcelableProxy(hostName, port, type); } + + @NonNull + public Proxy getProxy() { + return parcelableProxy.getProxy(); + } + } // cloud prefs diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java deleted file mode 100644 index 0297d149c..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ShareHelper.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.util; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.LabeledIntent; -import android.content.pm.ResolveInfo; -import android.os.Build; -import android.os.Parcelable; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -public class ShareHelper { - Context mContext; - - public ShareHelper(Context context) { - mContext = context; - } - - /** - * Create Intent Chooser but exclude specific activites, e.g., EncryptActivity to prevent encrypting again - * <p/> - * Put together from some stackoverflow posts... - */ - public Intent createChooserExcluding(Intent prototype, String title, String[] activityBlacklist) { - // Produced an empty list on Huawei U8860 with Android Version 4.0.3 - // TODO: test on 4.1, 4.2, 4.3, only tested on 4.4 - // Disabled on 5.0 because using EXTRA_INITIAL_INTENTS prevents the usage based sorting - // introduced in 5.0: https://medium.com/@xXxXxXxXxXam/how-lollipops-share-menu-is-organized-d204888f606d - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return Intent.createChooser(prototype, title); - } - - List<LabeledIntent> targetedShareIntents = new ArrayList<>(); - - List<ResolveInfo> resInfoList = mContext.getPackageManager().queryIntentActivities(prototype, 0); - List<ResolveInfo> resInfoListFiltered = new ArrayList<>(); - if (!resInfoList.isEmpty()) { - for (ResolveInfo resolveInfo : resInfoList) { - // do not add blacklisted ones - if (resolveInfo.activityInfo == null || Arrays.asList(activityBlacklist).contains(resolveInfo.activityInfo.name)) - continue; - - resInfoListFiltered.add(resolveInfo); - } - - if (!resInfoListFiltered.isEmpty()) { - // sorting for nice readability - Collections.sort(resInfoListFiltered, new Comparator<ResolveInfo>() { - @Override - public int compare(ResolveInfo first, ResolveInfo second) { - String firstName = first.loadLabel(mContext.getPackageManager()).toString(); - String secondName = second.loadLabel(mContext.getPackageManager()).toString(); - return firstName.compareToIgnoreCase(secondName); - } - }); - - // create the custom intent list - for (ResolveInfo resolveInfo : resInfoListFiltered) { - Intent targetedShareIntent = (Intent) prototype.clone(); - targetedShareIntent.setPackage(resolveInfo.activityInfo.packageName); - targetedShareIntent.setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); - - LabeledIntent lIntent = new LabeledIntent(targetedShareIntent, - resolveInfo.activityInfo.packageName, - resolveInfo.loadLabel(mContext.getPackageManager()), - resolveInfo.activityInfo.icon); - targetedShareIntents.add(lIntent); - } - - // Create chooser with only one Intent in it - Intent chooserIntent = Intent.createChooser(targetedShareIntents.remove(targetedShareIntents.size() - 1), title); - // append all other Intents - // TODO this line looks wrong?! - chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedShareIntents.toArray(new Parcelable[]{})); - return chooserIntent; - } - - } - - // fallback to Android's default chooser - return Intent.createChooser(prototype, title); - } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java index d1d1ada2a..1492abdeb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/TlsHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de> + * Copyright (C) 2013-2015 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 @@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.util; import android.content.res.AssetManager; import com.squareup.okhttp.OkHttpClient; + import org.sufficientlysecure.keychain.Constants; import java.io.ByteArrayInputStream; @@ -37,7 +38,6 @@ import java.security.cert.CertificateFactory; import java.util.HashMap; import java.util.Map; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; @@ -49,15 +49,14 @@ public class TlsHelper { } } - private static Map<String, byte[]> sStaticCA = new HashMap<>(); - - public static void addStaticCA(String domain, byte[] certificate) { - sStaticCA.put(domain, certificate); - } + private static Map<String, byte[]> sPinnedCertificates = new HashMap<>(); - public static void addStaticCA(String domain, AssetManager assetManager, String name) { + /** + * Add certificate from assets to pinned certificate map. + */ + public static void addPinnedCertificate(String host, AssetManager assetManager, String cerFilename) { try { - InputStream is = assetManager.open(name); + InputStream is = assetManager.open(cerFilename); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int reads = is.read(); @@ -68,27 +67,36 @@ public class TlsHelper { is.close(); - addStaticCA(domain, baos.toByteArray()); + sPinnedCertificates.put(host, baos.toByteArray()); } catch (IOException e) { Log.w(Constants.TAG, e); } } - public static void pinCertificateIfNecessary(OkHttpClient client, URL url) throws TlsHelperException, IOException { + /** + * Use pinned certificate for OkHttpClient if we have one. + * + * @return true, if certificate is available, false if not + * @throws TlsHelperException + * @throws IOException + */ + public static boolean usePinnedCertificateIfAvailable(OkHttpClient client, URL url) throws TlsHelperException, IOException { if (url.getProtocol().equals("https")) { - for (String domain : sStaticCA.keySet()) { - if (url.getHost().endsWith(domain)) { - pinCertificate(sStaticCA.get(domain), client); + // use certificate PIN from assets if we have one + for (String host : sPinnedCertificates.keySet()) { + if (url.getHost().endsWith(host)) { + pinCertificate(sPinnedCertificates.get(host), client); + return true; } } } + return false; } /** * Modifies the client to accept only requests with a given certificate. Applies to all URLs requested by the * client. * Therefore a client that is pinned this way should be used to only make requests to URLs with passed certificate. - * TODO: Refactor - More like SSH StrictHostKeyChecking than pinning? * * @param certificate certificate to pin * @param client OkHttpClient to enforce pinning on @@ -97,8 +105,10 @@ public class TlsHelper { */ private static void pinCertificate(byte[] certificate, OkHttpClient client) throws TlsHelperException, IOException { - // We don't use OkHttp's CertificatePinner since it depends on a TrustManager to verify it too. Refer to - // note at end of description: http://square.github.io/okhttp/javadoc/com/squareup/okhttp/CertificatePinner.html + // We don't use OkHttp's CertificatePinner since it can not be used to pin self-signed + // certificate if such certificate is not accepted by TrustManager. + // (Refer to note at end of description: + // http://square.github.io/okhttp/javadoc/com/squareup/okhttp/CertificatePinner.html ) // Creating our own TrustManager that trusts only our certificate eliminates the need for certificate pinning try { // Load CA @@ -126,42 +136,4 @@ public class TlsHelper { } } - /** - * Opens a Connection that will only accept certificates signed with a specific CA and skips common name check. - * This is required for some distributed Keyserver networks like sks-keyservers.net - * - * @param certificate The X.509 certificate used to sign the servers certificate - * @param url Connection target - */ - public static HttpsURLConnection openCAConnection(byte[] certificate, URL url) - throws TlsHelperException, IOException { - try { - // Load CA - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - Certificate ca = cf.generateCertificate(new ByteArrayInputStream(certificate)); - - // Create a KeyStore containing our trusted CAs - String keyStoreType = KeyStore.getDefaultType(); - KeyStore keyStore = KeyStore.getInstance(keyStoreType); - keyStore.load(null, null); - keyStore.setCertificateEntry("ca", ca); - - // Create a TrustManager that trusts the CAs in our KeyStore - String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); - tmf.init(keyStore); - - // Create an SSLContext that uses our TrustManager - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, tmf.getTrustManagers(), null); - - // Tell the URLConnection to use a SocketFactory from our SSLContext - HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); - urlConnection.setSSLSocketFactory(context.getSocketFactory()); - - return urlConnection; - } catch (CertificateException | KeyManagementException | KeyStoreException | NoSuchAlgorithmException e) { - throw new TlsHelperException(e); - } - } } |