diff options
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure')
95 files changed, 6888 insertions, 2010 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index 3d58602ab..6a9656b28 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -19,14 +19,9 @@ package org.sufficientlysecure.keychain; import android.os.Environment; -import org.spongycastle.bcpg.HashAlgorithmTags; -import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags; import org.spongycastle.jce.provider.BouncyCastleProvider; -import org.sufficientlysecure.keychain.BuildConfig; - import java.io.File; -import java.net.InetSocketAddress; import java.net.Proxy; public final class Constants { @@ -34,6 +29,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_SYNC_REMOVE_CONTACTS = false; + public static final boolean DEBUG_KEYSERVER_SYNC = false; public static final String TAG = DEBUG ? "Keychain D" : "Keychain"; @@ -43,7 +39,7 @@ public final class Constants { public static final String ACCOUNT_TYPE = BuildConfig.ACCOUNT_TYPE; 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.APPLICATION_ID + ".provider"; + public static final String PROVIDER_AUTHORITY = BuildConfig.PROVIDER_CONTENT_AUTHORITY; public static final String TEMPSTORAGE_AUTHORITY = BuildConfig.APPLICATION_ID + ".tempstorage"; public static final String CLIPBOARD_LABEL = "Keychain"; @@ -81,6 +77,11 @@ public final class Constants { public static final File APP_DIR_FILE = new File(APP_DIR, "export.asc"); } + public static final class Notification { + public static final int PASSPHRASE_CACHE = 1; + public static final int KEYSERVER_SYNC_FAIL_ORBOT = 2; + } + public static final class Pref { public static final String PASSPHRASE_CACHE_TTL = "passphraseCacheTtl"; public static final String PASSPHRASE_CACHE_SUBS = "passphraseCacheSubs"; @@ -104,12 +105,24 @@ public final class Constants { public static final String PROXY_PORT = "proxyPort"; public static final String PROXY_TYPE = "proxyType"; public static final String THEME = "theme"; + // keyserver sync settings + public static final String SYNC_CONTACTS = "syncContacts"; + public static final String SYNC_KEYSERVER = "syncKeyserver"; + // other settings + public static final String EXPERIMENTAL_ENABLE_WORD_CONFIRM = "experimentalEnableWordConfirm"; + public static final String EXPERIMENTAL_ENABLE_LINKED_IDENTITIES = "experimentalEnableLinkedIdentities"; + public static final String EXPERIMENTAL_ENABLE_KEYBASE = "experimentalEnableKeybase"; public static final class Theme { public static final String LIGHT = "light"; public static final String DARK = "dark"; public static final String DEFAULT = Constants.Pref.Theme.LIGHT; } + + public static final class ProxyType { + public static final String TYPE_HTTP = "proxyHttp"; + public static final String TYPE_SOCKS = "proxySocks"; + } } /** @@ -123,7 +136,7 @@ public final class Constants { public static final class Defaults { public static final String KEY_SERVERS = "hkps://hkps.pool.sks-keyservers.net, hkps://pgp.mit.edu"; - public static final int PREF_VERSION = 5; + public static final int PREF_VERSION = 6; } public static final class key { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java index cd24394d7..311ef2d3b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/KeychainApplication.java @@ -23,7 +23,6 @@ import android.app.Application; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; @@ -35,7 +34,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.R; +import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.util.Log; @@ -97,7 +96,7 @@ public class KeychainApplication extends Application { setupAccountAsNeeded(this); // Update keyserver list as needed - Preferences.getPreferences(this).upgradePreferences(); + Preferences.getPreferences(this).upgradePreferences(this); TlsHelper.addStaticCA("pool.sks-keyservers.net", getAssets(), "sks-keyservers.netCA.cer"); @@ -136,17 +135,20 @@ public class KeychainApplication extends Application { } /** - * Add OpenKeychain account to Android to link contacts with keys + * Add OpenKeychain account to Android to link contacts with keys and keyserver sync */ public static void setupAccountAsNeeded(Context context) { try { AccountManager manager = AccountManager.get(context); Account[] accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE); - if (accounts == null || accounts.length == 0) { + + if (accounts.length == 0) { Account account = new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE); if (manager.addAccountExplicitly(account, null, null)) { + // for contact sync ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true); + KeyserverSyncAdapterService.enableKeyserverSync(context); } else { Log.e(Constants.TAG, "Adding account failed!"); } 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 2cf6d8b34..558b8ce7d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/HkpKeyserver.java @@ -196,9 +196,9 @@ public class HkpKeyserver extends Keyserver { /** * returns a client with pinned certificate if necessary * - * @param url - * @param proxy - * @return + * @param url url to be queried by client + * @param proxy proxy to be used by client + * @return client with a pinned certificate if necesary */ public static OkHttpClient getClient(URL url, Proxy proxy) throws IOException { OkHttpClient client = new OkHttpClient(); @@ -360,7 +360,7 @@ public class HkpKeyserver extends Keyserver { try { data = query(request, proxy); } catch (HttpError httpError) { - Log.e(Constants.TAG, "Failed to get key at HkpKeyserver", httpError); + Log.d(Constants.TAG, "Failed to get key at HkpKeyserver", httpError); throw new QueryFailedException("not found"); } Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(data); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedAttribute.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedAttribute.java new file mode 100644 index 000000000..3b05afbb3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedAttribute.java @@ -0,0 +1,32 @@ +package org.sufficientlysecure.keychain.linked; + +import java.net.URI; + +import android.content.Context; +import android.support.annotation.DrawableRes; + +public class LinkedAttribute extends UriAttribute { + + public final LinkedResource mResource; + + protected LinkedAttribute(URI uri, LinkedResource resource) { + super(uri); + if (resource == null) { + throw new AssertionError("resource must not be null in a LinkedIdentity!"); + } + mResource = resource; + } + + public @DrawableRes int getDisplayIcon() { + return mResource.getDisplayIcon(); + } + + public String getDisplayTitle(Context context) { + return mResource.getDisplayTitle(context); + } + + public String getDisplayComment(Context context) { + return mResource.getDisplayComment(context); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedResource.java new file mode 100644 index 000000000..dffeea65e --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedResource.java @@ -0,0 +1,25 @@ +package org.sufficientlysecure.keychain.linked; + +import java.net.URI; + +import android.content.Context; +import android.content.Intent; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; + +public abstract class LinkedResource { + + public abstract URI toUri(); + + public abstract @DrawableRes int getDisplayIcon(); + public abstract @StringRes int getVerifiedText(boolean isSecret); + public abstract String getDisplayTitle(Context context); + public abstract String getDisplayComment(Context context); + public boolean isViewable() { + return false; + } + public Intent getViewIntent() { + return null; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java new file mode 100644 index 000000000..998ec3ad4 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedTokenResource.java @@ -0,0 +1,300 @@ +package org.sufficientlysecure.keychain.linked; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.BasicHttpParams; +import 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; +import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.Log; +import org.thoughtcrime.ssl.pinning.util.PinningHelper; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.content.Context; + + +public abstract class LinkedTokenResource extends LinkedResource { + + protected final URI mSubUri; + protected final Set<String> mFlags; + protected final HashMap<String,String> mParams; + + public static Pattern magicPattern = + Pattern.compile("\\[Verifying my (?:Open|)?PGP key(?::|) openpgp4fpr:([a-zA-Z0-9]+)]"); + + protected LinkedTokenResource(Set<String> flags, HashMap<String, String> params, URI uri) { + mFlags = flags; + mParams = params; + mSubUri = uri; + } + + @SuppressWarnings("unused") + public URI getSubUri () { + return mSubUri; + } + + public Set<String> getFlags () { + return new HashSet<>(mFlags); + } + + public HashMap<String,String> getParams () { + return new HashMap<>(mParams); + } + + public static String generate (byte[] fingerprint) { + return String.format("[Verifying my OpenPGP key: openpgp4fpr:%s]", + KeyFormattingUtils.convertFingerprintToHex(fingerprint)); + } + + protected static LinkedTokenResource fromUri (URI uri) { + + if (!"openpgpid+token".equals(uri.getScheme()) + && !"openpgpid+cookie".equals(uri.getScheme())) { + Log.e(Constants.TAG, "unknown uri scheme in (suspected) linked id packet"); + return null; + } + + if (!uri.isOpaque()) { + Log.e(Constants.TAG, "non-opaque uri in (suspected) linked id packet"); + return null; + } + + String specific = uri.getSchemeSpecificPart(); + if (!specific.contains("@")) { + Log.e(Constants.TAG, "unknown uri scheme in linked id packet"); + return null; + } + + String[] pieces = specific.split("@", 2); + URI subUri = URI.create(pieces[1]); + + Set<String> flags = new HashSet<>(); + HashMap<String,String> params = new HashMap<>(); + if (!pieces[0].isEmpty()) { + String[] rawParams = pieces[0].split(";"); + for (String param : rawParams) { + String[] p = param.split("=", 2); + if (p.length == 1) { + flags.add(param); + } else { + params.put(p[0], p[1]); + } + } + } + + return findResourceType(flags, params, subUri); + + } + + protected static LinkedTokenResource findResourceType (Set<String> flags, + HashMap<String,String> params, URI subUri) { + + LinkedTokenResource res; + + res = GenericHttpsResource.create(flags, params, subUri); + if (res != null) { + return res; + } + // res = DnsResource.create(flags, params, subUri); + // if (res != null) { + // return res; + // } + res = TwitterResource.create(flags, params, subUri); + if (res != null) { + return res; + } + res = GithubResource.create(flags, params, subUri); + if (res != null) { + return res; + } + + return null; + + } + + public URI toUri () { + + StringBuilder b = new StringBuilder(); + b.append("openpgpid+token:"); + + // add flags + if (mFlags != null) { + boolean first = true; + for (String flag : mFlags) { + if (!first) { + b.append(";"); + } + first = false; + b.append(flag); + } + } + + // add parameters + if (mParams != null) { + boolean first = true; + for (Entry<String, String> stringStringEntry : mParams.entrySet()) { + if (!first) { + b.append(";"); + } + first = false; + b.append(stringStringEntry.getKey()).append("=").append(stringStringEntry.getValue()); + } + } + + b.append("@"); + b.append(mSubUri); + + return URI.create(b.toString()); + + } + + public LinkedVerifyResult verify(Context context, byte[] fingerprint) { + + OperationLog log = new OperationLog(); + log.add(LogType.MSG_LV, 0); + + // Try to fetch resource. Logs for itself + String res = null; + try { + res = fetchResource(context, log, 1); + } catch (HttpStatusException e) { + // log verbose output to logcat + Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason()); + log.add(LogType.MSG_LV_FETCH_ERROR, 2, Integer.toString(e.getStatus())); + } catch (MalformedURLException e) { + log.add(LogType.MSG_LV_FETCH_ERROR_URL, 2); + } catch (IOException e) { + Log.e(Constants.TAG, "io error", e); + log.add(LogType.MSG_LV_FETCH_ERROR_IO, 2); + } catch (JSONException e) { + Log.e(Constants.TAG, "json error", e); + log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 2); + } + + if (res == null) { + // if this is null, an error was recorded in fetchResource above + return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log); + } + + Log.d(Constants.TAG, "Resource data: '" + res + "'"); + + return verifyString(log, 1, res, fingerprint); + + } + + protected abstract String fetchResource (Context context, OperationLog log, int indent) + throws HttpStatusException, IOException, JSONException; + + protected Matcher matchResource (OperationLog log, int indent, String res) { + return magicPattern.matcher(res); + } + + protected LinkedVerifyResult verifyString (OperationLog log, int indent, + String res, + byte[] fingerprint) { + + log.add(LogType.MSG_LV_MATCH, indent); + Matcher match = matchResource(log, indent+1, res); + if (!match.find()) { + log.add(LogType.MSG_LV_MATCH_ERROR, 2); + return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log); + } + + String candidateFp = match.group(1).toLowerCase(); + String fp = KeyFormattingUtils.convertFingerprintToHex(fingerprint); + if (!fp.equals(candidateFp)) { + log.add(LogType.MSG_LV_FP_ERROR, indent); + return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log); + } + log.add(LogType.MSG_LV_FP_OK, indent); + + return new LinkedVerifyResult(LinkedVerifyResult.RESULT_OK, log); + + } + + @SuppressWarnings("deprecation") // HttpRequestBase is deprecated + public static String getResponseBody(Context context, HttpRequestBase request) + throws IOException, HttpStatusException { + return getResponseBody(context, request, null); + } + + @SuppressWarnings("deprecation") // HttpRequestBase is deprecated + public static String getResponseBody(Context context, HttpRequestBase request, String[] pins) + throws IOException, HttpStatusException { + StringBuilder sb = new StringBuilder(); + + request.setHeader("User-Agent", "Open Keychain"); + + + HttpClient httpClient; + if (pins == null) { + httpClient = new DefaultHttpClient(new BasicHttpParams()); + } else { + httpClient = PinningHelper.getPinnedHttpClient(context, pins); + } + + HttpResponse response = httpClient.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + String reason = response.getStatusLine().getReasonPhrase(); + + if (statusCode != 200) { + throw new HttpStatusException(statusCode, reason); + } + + HttpEntity entity = response.getEntity(); + InputStream inputStream = entity.getContent(); + + BufferedReader bReader = new BufferedReader( + new InputStreamReader(inputStream, "UTF-8"), 8); + String line; + while ((line = bReader.readLine()) != null) { + sb.append(line); + } + + return sb.toString(); + } + + public static class HttpStatusException extends Throwable { + + private final int mStatusCode; + private final String mReason; + + HttpStatusException(int statusCode, String reason) { + super("http status " + statusCode + ": " + reason); + mStatusCode = statusCode; + mReason = reason; + } + + public int getStatus() { + return mStatusCode; + } + + public String getReason() { + return mReason; + } + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/UriAttribute.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/UriAttribute.java new file mode 100644 index 000000000..7a8ece2cb --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/UriAttribute.java @@ -0,0 +1,79 @@ +package org.sufficientlysecure.keychain.linked; + +import org.spongycastle.util.Strings; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.IOException; +import java.net.URI; + +import android.content.Context; +import android.support.annotation.DrawableRes; + +/** The RawLinkedIdentity contains raw parsed data from a Linked Identity subpacket. */ +public class UriAttribute { + + public final URI mUri; + + protected UriAttribute(URI uri) { + mUri = uri; + } + + public byte[] getEncoded() { + return Strings.toUTF8ByteArray(mUri.toASCIIString()); + } + + public static UriAttribute fromAttributeData(byte[] data) throws IOException { + WrappedUserAttribute att = WrappedUserAttribute.fromData(data); + + byte[][] subpackets = att.getSubpackets(); + if (subpackets.length >= 1) { + return fromSubpacketData(subpackets[0]); + } + + throw new IOException("no subpacket data"); + } + + static UriAttribute fromSubpacketData(byte[] data) { + + try { + String uriStr = Strings.fromUTF8ByteArray(data); + URI uri = URI.create(uriStr); + + LinkedResource res = LinkedTokenResource.fromUri(uri); + if (res == null) { + return new UriAttribute(uri); + } + + return new LinkedAttribute(uri, res); + + } catch (IllegalArgumentException e) { + Log.e(Constants.TAG, "error parsing uri in (suspected) linked id packet"); + return null; + } + } + + public static UriAttribute fromResource (LinkedTokenResource res) { + return new UriAttribute(res.toUri()); + } + + + public WrappedUserAttribute toUserAttribute () { + return WrappedUserAttribute.fromSubpacket(WrappedUserAttribute.UAT_URI_ATTRIBUTE, getEncoded()); + } + + public @DrawableRes int getDisplayIcon() { + return R.drawable.ic_warning_grey_24dp; + } + + public String getDisplayTitle(Context context) { + return "Unknown Identity"; + } + + public String getDisplayComment(Context context) { + return null; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/DnsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/DnsResource.java new file mode 100644 index 000000000..86b672cc1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/DnsResource.java @@ -0,0 +1,130 @@ +package org.sufficientlysecure.keychain.linked.resources; + +import android.content.Context; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.linked.LinkedTokenResource; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; + +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.CLASS; +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.record.TXT; + +public class DnsResource extends LinkedTokenResource { + + final static Pattern magicPattern = + Pattern.compile("openpgpid\\+token=([a-zA-Z0-9]+)(?:#|;)([a-zA-Z0-9]+)"); + + String mFqdn; + CLASS mClass; + TYPE mType; + + DnsResource(Set<String> flags, HashMap<String, String> params, URI uri, + String fqdn, CLASS clazz, TYPE type) { + super(flags, params, uri); + + mFqdn = fqdn; + mClass = clazz; + mType = type; + } + + public static String generateText(byte[] fingerprint) { + + return String.format("openpgp4fpr=%s", + KeyFormattingUtils.convertFingerprintToHex(fingerprint)); + + } + + public static DnsResource createNew (String domain) { + HashSet<String> flags = new HashSet<>(); + HashMap<String,String> params = new HashMap<>(); + URI uri = URI.create("dns:" + domain + "?TYPE=TXT"); + return create(flags, params, uri); + } + + public static DnsResource create(Set<String> flags, HashMap<String,String> params, URI uri) { + if ( ! ("dns".equals(uri.getScheme()) + && (flags == null || flags.isEmpty()) + && (params == null || params.isEmpty()))) { + return null; + } + + // + String spec = uri.getSchemeSpecificPart(); + // If there are // at the beginning, this includes an authority - we don't support those! + if (spec.startsWith("//")) { + return null; + } + + String[] pieces = spec.split("\\?", 2); + // In either case, part before a ? is the fqdn + String fqdn = pieces[0]; + // There may be a query part + /* + if (pieces.length > 1) { + // parse CLASS and TYPE query paramters + } + */ + + CLASS clazz = CLASS.IN; + TYPE type = TYPE.TXT; + + return new DnsResource(flags, params, uri, fqdn, clazz, type); + } + + @SuppressWarnings("unused") + public String getFqdn() { + return mFqdn; + } + + @Override + protected String fetchResource (Context context, OperationLog log, int indent) { + + Client c = new Client(); + DNSMessage msg = c.query(new Question(mFqdn, mType, mClass)); + Record aw = msg.getAnswers()[0]; + TXT txt = (TXT) aw.getPayload(); + return txt.getText().toLowerCase(); + + } + + @Override + protected Matcher matchResource(OperationLog log, int indent, String res) { + return magicPattern.matcher(res); + } + + @Override + public @StringRes + int getVerifiedText(boolean isSecret) { + return isSecret ? R.string.linked_verified_secret_dns : R.string.linked_verified_dns; + } + + @Override + public @DrawableRes int getDisplayIcon() { + return R.drawable.linked_dns; + } + + @Override + public String getDisplayTitle(Context context) { + return context.getString(R.string.linked_title_dns); + } + + @Override + public String getDisplayComment(Context context) { + return mFqdn; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java new file mode 100644 index 000000000..82240c405 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java @@ -0,0 +1,95 @@ +package org.sufficientlysecure.keychain.linked.resources; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; + +import org.apache.http.client.methods.HttpGet; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.linked.LinkedTokenResource; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +public class GenericHttpsResource extends LinkedTokenResource { + + GenericHttpsResource(Set<String> flags, HashMap<String,String> params, URI uri) { + super(flags, params, uri); + } + + public static String generateText (Context context, byte[] fingerprint) { + String token = LinkedTokenResource.generate(fingerprint); + + return String.format(context.getResources().getString(R.string.linked_id_generic_text), + token, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24)); + } + + @SuppressWarnings("deprecation") // HttpGet is deprecated + @Override + protected String fetchResource (Context context, OperationLog log, int indent) + throws HttpStatusException, IOException { + + log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString()); + HttpGet httpGet = new HttpGet(mSubUri); + return getResponseBody(context, httpGet); + + } + + public static GenericHttpsResource createNew (URI uri) { + HashSet<String> flags = new HashSet<>(); + flags.add("generic"); + HashMap<String,String> params = new HashMap<>(); + return create(flags, params, uri); + } + + public static GenericHttpsResource create(Set<String> flags, HashMap<String,String> params, URI uri) { + if ( ! ("https".equals(uri.getScheme()) + && flags != null && flags.size() == 1 && flags.contains("generic") + && (params == null || params.isEmpty()))) { + return null; + } + return new GenericHttpsResource(flags, params, uri); + } + + @Override + public @DrawableRes + int getDisplayIcon() { + return R.drawable.linked_https; + } + + @Override + public @StringRes + int getVerifiedText(boolean isSecret) { + return isSecret ? R.string.linked_verified_secret_https : R.string.linked_verified_https; + } + + @Override + public String getDisplayTitle(Context context) { + return context.getString(R.string.linked_title_https); + } + + @Override + public String getDisplayComment(Context context) { + return mSubUri.toString(); + } + + @Override + public boolean isViewable() { + return true; + } + + @Override + public Intent getViewIntent() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(mSubUri.toString())); + return intent; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java new file mode 100644 index 000000000..7a97ffd96 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java @@ -0,0 +1,218 @@ +package org.sufficientlysecure.keychain.linked.resources; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; + +import org.apache.http.client.methods.HttpGet; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.linked.LinkedTokenResource; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class GithubResource extends LinkedTokenResource { + + final String mHandle; + final String mGistId; + + GithubResource(Set<String> flags, HashMap<String,String> params, URI uri, + String handle, String gistId) { + super(flags, params, uri); + + mHandle = handle; + mGistId = gistId; + } + + public static String generate(Context context, byte[] fingerprint) { + String token = LinkedTokenResource.generate(fingerprint); + + return String.format(context.getResources().getString(R.string.linked_id_github_text), token); + } + + @SuppressWarnings("deprecation") // HttpGet is deprecated + @Override + protected String fetchResource (Context context, OperationLog log, int indent) + throws HttpStatusException, IOException, JSONException { + + log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString()); + indent += 1; + + HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + mGistId); + String response = getResponseBody(context, httpGet); + + JSONObject obj = new JSONObject(response); + + JSONObject owner = obj.getJSONObject("owner"); + if (!mHandle.equals(owner.getString("login"))) { + log.add(LogType.MSG_LV_ERROR_GITHUB_HANDLE, indent); + return null; + } + + JSONObject files = obj.getJSONObject("files"); + Iterator<String> it = files.keys(); + if (it.hasNext()) { + // TODO can there be multiple candidates? + JSONObject file = files.getJSONObject(it.next()); + return file.getString("content"); + } + + log.add(LogType.MSG_LV_ERROR_GITHUB_NOT_FOUND, indent); + return null; + + } + + @Deprecated // not used for now, but could be used to pick up earlier posted gist if already present? + @SuppressWarnings({ "deprecation", "unused" }) + public static GithubResource searchInGithubStream( + Context context, String screenName, String needle, OperationLog log) { + + // narrow the needle down to important part + Matcher matcher = magicPattern.matcher(needle); + if (!matcher.find()) { + throw new AssertionError("Needle must contain token pattern! This is a programming error, please report."); + } + needle = matcher.group(); + + try { + + JSONArray array; { + HttpGet httpGet = + new HttpGet("https://api.github.com/users/" + screenName + "/gists"); + httpGet.setHeader("Content-Type", "application/json"); + httpGet.setHeader("User-Agent", "OpenKeychain"); + + String response = getResponseBody(context, httpGet); + array = new JSONArray(response); + } + + for (int i = 0, j = Math.min(array.length(), 5); i < j; i++) { + JSONObject obj = array.getJSONObject(i); + + JSONObject files = obj.getJSONObject("files"); + Iterator<String> it = files.keys(); + if (it.hasNext()) { + + JSONObject file = files.getJSONObject(it.next()); + String type = file.getString("type"); + if (!"text/plain".equals(type)) { + continue; + } + String id = obj.getString("id"); + HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + id); + httpGet.setHeader("User-Agent", "OpenKeychain"); + + JSONObject gistObj = new JSONObject(getResponseBody(context, httpGet)); + JSONObject gistFiles = gistObj.getJSONObject("files"); + Iterator<String> gistIt = gistFiles.keys(); + if (!gistIt.hasNext()) { + continue; + } + // TODO can there be multiple candidates? + JSONObject gistFile = gistFiles.getJSONObject(gistIt.next()); + String content = gistFile.getString("content"); + if (!content.contains(needle)) { + continue; + } + + URI uri = URI.create("https://gist.github.com/" + screenName + "/" + id); + return create(uri); + } + } + + // update the results with the body of the response + log.add(LogType.MSG_LV_FETCH_ERROR_NOTHING, 2); + return null; + + } catch (HttpStatusException e) { + // log verbose output to logcat + Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason()); + log.add(LogType.MSG_LV_FETCH_ERROR, 2, Integer.toString(e.getStatus())); + } catch (MalformedURLException e) { + log.add(LogType.MSG_LV_FETCH_ERROR_URL, 2); + } catch (IOException e) { + Log.e(Constants.TAG, "io error", e); + log.add(LogType.MSG_LV_FETCH_ERROR_IO, 2); + } catch (JSONException e) { + Log.e(Constants.TAG, "json error", e); + log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 2); + } + + return null; + } + + public static GithubResource create(URI uri) { + return create(new HashSet<String>(), new HashMap<String,String>(), uri); + } + + public static GithubResource create(Set<String> flags, HashMap<String,String> params, URI uri) { + + // no params or flags + if (!flags.isEmpty() || !params.isEmpty()) { + return null; + } + + Pattern p = Pattern.compile("https://gist\\.github\\.com/([a-zA-Z0-9_]+)/([0-9a-f]+)"); + Matcher match = p.matcher(uri.toString()); + if (!match.matches()) { + return null; + } + String handle = match.group(1); + String gistId = match.group(2); + + return new GithubResource(flags, params, uri, handle, gistId); + + } + + + @Override + public @DrawableRes + int getDisplayIcon() { + return R.drawable.linked_github; + } + + @Override + public @StringRes + int getVerifiedText(boolean isSecret) { + return isSecret ? R.string.linked_verified_secret_github : R.string.linked_verified_github; + } + + @Override + public String getDisplayTitle(Context context) { + return context.getString(R.string.linked_title_github); + } + + @Override + public String getDisplayComment(Context context) { + return mHandle; + } + + @Override + public boolean isViewable() { + return true; + } + + @Override + public Intent getViewIntent() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(mSubUri.toString())); + return intent; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java new file mode 100644 index 000000000..73e3d3643 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java @@ -0,0 +1,250 @@ +package org.sufficientlysecure.keychain.linked.resources; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; +import android.util.Log; + +import com.textuality.keybase.lib.JWalk; + +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.linked.LinkedTokenResource; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TwitterResource extends LinkedTokenResource { + + public static final String[] CERT_PINS = null; /*(new String[] { + // Symantec Class 3 Secure Server CA - G4 + "513fb9743870b73440418d30930699ff" + };*/ + + final String mHandle; + final String mTweetId; + + TwitterResource(Set<String> flags, HashMap<String,String> params, + URI uri, String handle, String tweetId) { + super(flags, params, uri); + + mHandle = handle; + mTweetId = tweetId; + } + + public static TwitterResource create(URI uri) { + return create(new HashSet<String>(), new HashMap<String,String>(), uri); + } + + public static TwitterResource create(Set<String> flags, HashMap<String,String> params, URI uri) { + + // no params or flags + if (!flags.isEmpty() || !params.isEmpty()) { + return null; + } + + Pattern p = Pattern.compile("https://twitter\\.com/([a-zA-Z0-9_]+)/status/([0-9]+)"); + Matcher match = p.matcher(uri.toString()); + if (!match.matches()) { + return null; + } + String handle = match.group(1); + String tweetId = match.group(2); + + return new TwitterResource(flags, params, uri, handle, tweetId); + + } + + @SuppressWarnings("deprecation") + @Override + protected String fetchResource(Context context, OperationLog log, int indent) + throws IOException, HttpStatusException, JSONException { + + String authToken; + try { + authToken = getAuthToken(context); + } catch (IOException | HttpStatusException | JSONException e) { + log.add(LogType.MSG_LV_ERROR_TWITTER_AUTH, indent); + return null; + } + + HttpGet httpGet = + new HttpGet("https://api.twitter.com/1.1/statuses/show.json" + + "?id=" + mTweetId + + "&include_entities=false"); + + // construct a normal HTTPS request and include an Authorization + // header with the value of Bearer <> + httpGet.setHeader("Authorization", "Bearer " + authToken); + httpGet.setHeader("Content-Type", "application/json"); + + try { + String response = getResponseBody(context, httpGet, CERT_PINS); + JSONObject obj = new JSONObject(response); + JSONObject user = obj.getJSONObject("user"); + if (!mHandle.equalsIgnoreCase(user.getString("screen_name"))) { + log.add(LogType.MSG_LV_ERROR_TWITTER_HANDLE, indent); + return null; + } + + // update the results with the body of the response + return obj.getString("text"); + } catch (JSONException e) { + log.add(LogType.MSG_LV_ERROR_TWITTER_RESPONSE, indent); + return null; + } + + } + + @Override + public @DrawableRes int getDisplayIcon() { + return R.drawable.linked_twitter; + } + + @Override + public @StringRes + int getVerifiedText(boolean isSecret) { + return isSecret ? R.string.linked_verified_secret_twitter : R.string.linked_verified_twitter; + } + + @Override + public String getDisplayTitle(Context context) { + return context.getString(R.string.linked_title_twitter); + } + + @Override + public String getDisplayComment(Context context) { + return "@" + mHandle; + } + + @Override + public boolean isViewable() { + return true; + } + + @Override + public Intent getViewIntent() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(mSubUri.toString())); + return intent; + } + + @SuppressWarnings("deprecation") + public static TwitterResource searchInTwitterStream( + Context context, String screenName, String needle, OperationLog log) { + + String authToken; + try { + authToken = getAuthToken(context); + } catch (IOException | HttpStatusException | JSONException e) { + log.add(LogType.MSG_LV_ERROR_TWITTER_AUTH, 1); + return null; + } + + HttpGet httpGet = + new HttpGet("https://api.twitter.com/1.1/statuses/user_timeline.json" + + "?screen_name=" + screenName + + "&count=15" + + "&include_rts=false" + + "&trim_user=true" + + "&exclude_replies=true"); + + // construct a normal HTTPS request and include an Authorization + // header with the value of Bearer <> + httpGet.setHeader("Authorization", "Bearer " + authToken); + httpGet.setHeader("Content-Type", "application/json"); + + try { + String response = getResponseBody(context, httpGet, CERT_PINS); + JSONArray array = new JSONArray(response); + + for (int i = 0; i < array.length(); i++) { + JSONObject obj = array.getJSONObject(i); + String tweet = obj.getString("text"); + if (tweet.contains(needle)) { + String id = obj.getString("id_str"); + URI uri = URI.create("https://twitter.com/" + screenName + "/status/" + id); + return create(uri); + } + } + + // update the results with the body of the response + log.add(LogType.MSG_LV_FETCH_ERROR_NOTHING, 1); + return null; + + } catch (HttpStatusException e) { + // log verbose output to logcat + Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason()); + log.add(LogType.MSG_LV_FETCH_ERROR, 1, Integer.toString(e.getStatus())); + } catch (MalformedURLException e) { + log.add(LogType.MSG_LV_FETCH_ERROR_URL, 1); + } catch (IOException e) { + Log.e(Constants.TAG, "io error", e); + log.add(LogType.MSG_LV_FETCH_ERROR_IO, 1); + } catch (JSONException e) { + Log.e(Constants.TAG, "json error", e); + log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 1); + } + + return null; + } + + private static String cachedAuthToken; + + @SuppressWarnings("deprecation") + private static String getAuthToken(Context context) + throws IOException, HttpStatusException, JSONException { + if (cachedAuthToken != null) { + return cachedAuthToken; + } + String base64Encoded = rot13("D293FQqanH0jH29KIaWJER5DomqSGRE2Ewc1LJACn3cbD1c" + + "Fq1bmqSAQAz5MI2cIHKOuo3cPoRAQI1OyqmIVFJS6LHMXq2g6MRLkIj") + "=="; + + // Step 2: Obtain a bearer token + HttpPost httpPost = new HttpPost("https://api.twitter.com/oauth2/token"); + httpPost.setHeader("Authorization", "Basic " + base64Encoded); + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); + httpPost.setEntity(new StringEntity("grant_type=client_credentials")); + JSONObject rawAuthorization = new JSONObject(getResponseBody(context, httpPost, CERT_PINS)); + + // Applications should verify that the value associated with the + // token_type key of the returned object is bearer + if (!"bearer".equals(JWalk.getString(rawAuthorization, "token_type"))) { + throw new JSONException("Expected bearer token in response!"); + } + + cachedAuthToken = rawAuthorization.getString("access_token"); + return cachedAuthToken; + + } + + public static String rot13(String input) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c >= 'a' && c <= 'm') c += 13; + else if (c >= 'A' && c <= 'M') c += 13; + else if (c >= 'n' && c <= 'z') c -= 13; + else if (c >= 'N' && c <= 'Z') c -= 13; + sb.append(c); + } + return sb.toString(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java index a5b70a41f..531ac01f2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ExportOperation.java @@ -145,6 +145,8 @@ public class ExportOperation extends BaseOperation<ExportKeyringParcel> { 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); 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 4acfd6e30..89575338f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/ImportOperation.java @@ -22,9 +22,8 @@ package org.sufficientlysecure.keychain.operations; import java.io.IOException; import java.net.Proxy; import java.util.ArrayList; -import java.util.HashSet; +import java.util.GregorianCalendar; import java.util.Iterator; -import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; @@ -99,29 +98,9 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { return serialKeyRingImport(entries, num, keyServerUri, mProgressable, proxy); } - public ImportKeyResult serialKeyRingImport(List<ParcelableKeyRing> entries, - String keyServerUri, Proxy proxy) { - - Iterator<ParcelableKeyRing> it = entries.iterator(); - int numEntries = entries.size(); - - return serialKeyRingImport(it, numEntries, keyServerUri, mProgressable, proxy); - - } - - public ImportKeyResult serialKeyRingImport(List<ParcelableKeyRing> entries, String keyServerUri, - Progressable progressable, Proxy proxy) { - - Iterator<ParcelableKeyRing> it = entries.iterator(); - int numEntries = entries.size(); - - return serialKeyRingImport(it, numEntries, keyServerUri, progressable, proxy); - - } - @NonNull - public ImportKeyResult serialKeyRingImport(ParcelableFileCache<ParcelableKeyRing> cache, - String keyServerUri, Proxy proxy) { + private ImportKeyResult serialKeyRingImport(ParcelableFileCache<ParcelableKeyRing> cache, + String keyServerUri, Proxy proxy) { // get entries from cached file try { @@ -143,7 +122,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { /** * Since the introduction of multithreaded import, we expect calling functions to handle the - * key sync i,eContactSyncAdapterService.requestSync() + * contact-to-key sync i.e ContactSyncAdapterService.requestSync() * * @param entries keys to import * @param num number of keys to import @@ -152,9 +131,9 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { * progress of a single key being imported */ @NonNull - public ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int num, - String keyServerUri, Progressable progressable, - Proxy proxy) { + private ImportKeyResult serialKeyRingImport(Iterator<ParcelableKeyRing> entries, int num, + String keyServerUri, Progressable progressable, + Proxy proxy) { if (progressable != null) { progressable.setProgress(R.string.progress_importing, 0, 100); } @@ -231,8 +210,8 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { log.add(LogType.MSG_IMPORT_FETCH_ERROR_DECODE, 3); } } catch (Keyserver.QueryFailedException e) { - Log.e(Constants.TAG, "query failed", e); - log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3, e.getMessage()); + Log.d(Constants.TAG, "query failed", e); + log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER, 3, e.getMessage()); } } @@ -264,7 +243,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { } catch (Keyserver.QueryFailedException e) { // download failed, too bad. just proceed Log.e(Constants.TAG, "query failed", e); - log.add(LogType.MSG_IMPORT_FETCH_KEYSERVER_ERROR, 3, e.getMessage()); + log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER, 3, e.getMessage()); } } } @@ -275,15 +254,11 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { continue; } - // If we have an expected fingerprint, make sure it matches - if (entry.mExpectedFingerprint != null) { - if (!key.containsSubkey(entry.mExpectedFingerprint)) { - log.add(LogType.MSG_IMPORT_FINGERPRINT_ERROR, 2); - badKeys += 1; - continue; - } else { - log.add(LogType.MSG_IMPORT_FINGERPRINT_OK, 2); - } + // never import secret keys from keyserver! + if (entry.mBytes == null && key.isSecret()) { + log.add(LogType.MSG_IMPORT_FETCH_ERROR_KEYSERVER_SECRET, 2); + badKeys += 1; + continue; } // Another check if we have been cancelled @@ -293,31 +268,44 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { } SaveKeyringResult result; - mProviderHelper.clearLog(); - if (key.isSecret()) { - result = mProviderHelper.saveSecretKeyRing(key, - new ProgressScaler(progressable, (int) (position * progSteps), - (int) ((position + 1) * progSteps), 100)); - } else { - result = mProviderHelper.savePublicKeyRing(key, - new ProgressScaler(progressable, (int) (position * progSteps), - (int) ((position + 1) * progSteps), 100)); + // synchronizing prevents https://github.com/open-keychain/open-keychain/issues/1221 + // and https://github.com/open-keychain/open-keychain/issues/1480 + synchronized (mProviderHelper) { + mProviderHelper.clearLog(); + if (key.isSecret()) { + result = mProviderHelper.saveSecretKeyRing(key, + new ProgressScaler(progressable, (int) (position * progSteps), + (int) ((position + 1) * progSteps), 100)); + } else { + result = mProviderHelper.savePublicKeyRing(key, + new ProgressScaler(progressable, (int) (position * progSteps), + (int) ((position + 1) * progSteps), 100), entry.mExpectedFingerprint); + } } if (!result.success()) { badKeys += 1; - } else if (result.updated()) { - updatedKeys += 1; - importedMasterKeyIds.add(key.getMasterKeyId()); } else { - newKeys += 1; - if (key.isSecret()) { - secret += 1; + if (result.updated()) { + updatedKeys += 1; + importedMasterKeyIds.add(key.getMasterKeyId()); + } else { + newKeys += 1; + if (key.isSecret()) { + secret += 1; + } + importedMasterKeyIds.add(key.getMasterKeyId()); + } + if (entry.mBytes == null) { + // synonymous to isDownloadFromKeyserver. + // If no byte data was supplied, import from keyserver took place + // this prevents file imports being noted as keyserver imports + mProviderHelper.renewKeyLastUpdatedTime(key.getMasterKeyId(), + GregorianCalendar.getInstance().getTimeInMillis(), + TimeUnit.MILLISECONDS); } - importedMasterKeyIds.add(key.getMasterKeyId()); } log.add(result, 2); - } catch (IOException | PgpGeneralException e) { Log.e(Constants.TAG, "Encountered bad key on import!", e); ++badKeys; @@ -327,9 +315,15 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { } // Special: consolidate on secret key import (cannot be cancelled!) + // synchronized on mProviderHelper to prevent + // https://github.com/open-keychain/open-keychain/issues/1221 since a consolidate deletes + // and re-inserts keys, which could conflict with a parallel db key update if (secret > 0) { setPreventCancel(); - ConsolidateResult result = mProviderHelper.consolidateDatabaseStep1(progressable); + ConsolidateResult result; + synchronized (mProviderHelper) { + result = mProviderHelper.consolidateDatabaseStep1(progressable); + } log.add(result, 1); } @@ -386,7 +380,7 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { @NonNull @Override - public OperationResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) { + public ImportKeyResult execute(ImportKeyringParcel importInput, CryptoInputParcel cryptoInput) { ArrayList<ParcelableKeyRing> keyList = importInput.mKeyList; String keyServer = importInput.mKeyserver; @@ -411,20 +405,8 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { } else { proxy = cryptoInput.getParcelableProxy().getProxy(); } - // if there is more than one key with the same fingerprint, we do a serial import to - // prevent - // https://github.com/open-keychain/open-keychain/issues/1221 - HashSet<String> keyFingerprintSet = new HashSet<>(); - for (int i = 0; i < keyList.size(); i++) { - keyFingerprintSet.add(keyList.get(i).mExpectedFingerprint); - } - if (keyFingerprintSet.size() == keyList.size()) { - // all keys have unique fingerprints - result = multiThreadedKeyImport(keyList.iterator(), keyList.size(), keyServer, - proxy); - } else { - result = serialKeyRingImport(keyList, keyServer, proxy); - } + + result = multiThreadedKeyImport(keyList.iterator(), keyList.size(), keyServer, proxy); } ContactSyncAdapterService.requestSync(); @@ -462,7 +444,8 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { ArrayList<ParcelableKeyRing> list = new ArrayList<>(); list.add(pkRing); - return serialKeyRingImport(list, keyServer, ignoreProgressable, proxy); + return serialKeyRingImport(list.iterator(), 1, keyServer, + ignoreProgressable, proxy); } }; @@ -486,18 +469,18 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { } return accumulator.getConsolidatedResult(); } - return null; // TODO: Decide if we should just crash instead of returning null + return new ImportKeyResult(ImportKeyResult.RESULT_FAIL_NOTHING, new OperationLog()); } /** * Used to accumulate the results of individual key imports */ - private class KeyImportAccumulator { + public static class KeyImportAccumulator { private OperationResult.OperationLog mImportLog = new OperationResult.OperationLog(); Progressable mProgressable; private int mTotalKeys; private int mImportedKeys = 0; - ArrayList<Long> mImportedMasterKeyIds = new ArrayList<Long>(); + ArrayList<Long> mImportedMasterKeyIds = new ArrayList<>(); private int mBadKeys = 0; private int mNewKeys = 0; private int mUpdatedKeys = 0; @@ -515,21 +498,17 @@ public class ImportOperation extends BaseOperation<ImportKeyringParcel> { public KeyImportAccumulator(int totalKeys, Progressable externalProgressable) { mTotalKeys = totalKeys; mProgressable = externalProgressable; - mProgressable.setProgress(0, totalKeys); - } - - public int getTotalKeys() { - return mTotalKeys; - } - - public int getImportedKeys() { - return mImportedKeys; + if (mProgressable != null) { + mProgressable.setProgress(0, totalKeys); + } } public synchronized void accumulateKeyImport(ImportKeyResult result) { mImportedKeys++; - mProgressable.setProgress(mImportedKeys, mTotalKeys); + if (mProgressable != null) { + mProgressable.setProgress(mImportedKeys, mTotalKeys); + } mImportLog.addAll(result.getLog().toList());//accumulates log mBadKeys += result.mBadKeys; 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 30f37dd4f..8f1abde83 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/KeybaseVerificationOperation.java @@ -43,7 +43,7 @@ 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.PgpDecryptVerify; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.provider.ProviderHelper; @@ -141,7 +141,7 @@ public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificat } } - PgpDecryptVerify op = new PgpDecryptVerify(mContext, mProviderHelper, mProgressable); + PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable); PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel(messageBytes) .setSignedLiteralData(true) 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 ecf64e1af..975cf541a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/RevokeOperation.java @@ -20,7 +20,6 @@ package org.sufficientlysecure.keychain.operations; import android.content.Context; -import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; @@ -39,7 +38,7 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.Log; -public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> { +public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> { public RevokeOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { super(context, providerHelper, progressable); @@ -71,13 +70,15 @@ public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> { return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); } - SaveKeyringParcel saveKeyringParcel = getRevokedSaveKeyringParcel(masterKeyId, - keyRing.getFingerprint()); + SaveKeyringParcel saveKeyringParcel = + new SaveKeyringParcel(masterKeyId, keyRing.getFingerprint()); // all revoke operations are made atomic as of now saveKeyringParcel.setUpdateOptions(revokeKeyringParcel.mUpload, true, revokeKeyringParcel.mKeyserver); + saveKeyringParcel.mRevokeSubKeys.add(masterKeyId); + InputPendingResult revokeAndUploadResult = new EditKeyOperation(mContext, mProviderHelper, mProgressable, mCancelled) .execute(saveKeyringParcel, cryptoInputParcel); @@ -92,54 +93,15 @@ public class RevokeOperation extends BaseOperation<RevokeKeyringParcel> { log.add(OperationResult.LogType.MSG_REVOKE_OK, 1); return new RevokeResult(RevokeResult.RESULT_OK, log, masterKeyId); } else { - log.add(OperationResult.LogType.MSG_REVOKE_KEY_FAIL, 1); + log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1); return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); } } catch (PgpKeyNotFoundException | ProviderHelper.NotFoundException e) { Log.e(Constants.TAG, "could not find key to revoke", e); - log.add(OperationResult.LogType.MSG_REVOKE_KEY_FAIL, 1); + log.add(OperationResult.LogType.MSG_REVOKE_ERROR_KEY_FAIL, 1); return new RevokeResult(RevokeResult.RESULT_ERROR, log, masterKeyId); } } - private SaveKeyringParcel getRevokedSaveKeyringParcel(long masterKeyId, byte[] fingerprint) { - final String[] SUBKEYS_PROJECTION = new String[]{ - KeychainContract.Keys.KEY_ID - }; - final int INDEX_KEY_ID = 0; - - Uri keysUri = KeychainContract.Keys.buildKeysUri(masterKeyId); - Cursor subKeyCursor = - mContext.getContentResolver().query(keysUri, SUBKEYS_PROJECTION, null, null, null); - - SaveKeyringParcel saveKeyringParcel = - new SaveKeyringParcel(masterKeyId, fingerprint); - - // add all subkeys, for revocation - while (subKeyCursor != null && subKeyCursor.moveToNext()) { - saveKeyringParcel.mRevokeSubKeys.add(subKeyCursor.getLong(INDEX_KEY_ID)); - } - if (subKeyCursor != null) { - subKeyCursor.close(); - } - - final String[] USER_IDS_PROJECTION = new String[]{ - KeychainContract.UserPackets.USER_ID - }; - final int INDEX_USER_ID = 0; - - Uri userIdsUri = KeychainContract.UserPackets.buildUserIdsUri(masterKeyId); - Cursor userIdCursor = mContext.getContentResolver().query( - userIdsUri, USER_IDS_PROJECTION, null, null, null); - - while (userIdCursor != null && userIdCursor.moveToNext()) { - saveKeyringParcel.mRevokeUserIds.add(userIdCursor.getString(INDEX_USER_ID)); - } - if (userIdCursor != null) { - userIdCursor.close(); - } - - return saveKeyringParcel; - } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java new file mode 100644 index 000000000..42e9ec3f0 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/LinkedVerifyResult.java @@ -0,0 +1,49 @@ +/* + * 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 LinkedVerifyResult extends OperationResult { + + public LinkedVerifyResult(int result, OperationLog log) { + super(result, log); + } + + /** Construct from a parcel - trivial because we have no extra data. */ + public LinkedVerifyResult(Parcel source) { + super(source); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + } + + public static Creator<LinkedVerifyResult> CREATOR = new Creator<LinkedVerifyResult>() { + public LinkedVerifyResult createFromParcel(final Parcel source) { + return new LinkedVerifyResult(source); + } + + public LinkedVerifyResult[] newArray(final int size) { + return new LinkedVerifyResult[size]; + } + }; + +} 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 3c15a2e7b..3856209b3 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 @@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.operations.results; import android.app.Activity; import android.content.Intent; +import android.content.res.Resources; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; @@ -52,6 +53,8 @@ import java.util.List; */ public abstract class OperationResult implements Parcelable { + final static String INDENTATION_WHITESPACE = " "; + public static final String EXTRA_RESULT = "operation_result"; /** @@ -166,6 +169,27 @@ public abstract class OperationResult implements Parcelable { ", mIndent=" + mIndent + '}'; } + + StringBuilder getPrintableLogEntry(Resources resources, int indent) { + + StringBuilder result = new StringBuilder(); + int padding = mIndent +indent; + if (padding > INDENTATION_WHITESPACE.length()) { + padding = INDENTATION_WHITESPACE.length(); + } + result.append(INDENTATION_WHITESPACE, 0, padding); + result.append(LOG_LEVEL_NAME[mType.mLevel.ordinal()]).append(' '); + + // special case: first parameter may be a quantity + if (mParameters != null && mParameters.length > 0 && mParameters[0] instanceof Integer) { + result.append(resources.getQuantityString(mType.getMsgId(), (Integer) mParameters[0], mParameters)); + } else { + result.append(resources.getString(mType.getMsgId(), mParameters)); + } + + return result; + } + } public static class SubLogEntryParcel extends LogEntryParcel { @@ -202,6 +226,17 @@ public abstract class OperationResult implements Parcelable { dest.writeParcelable(mSubResult, 0); } + @Override + StringBuilder getPrintableLogEntry(Resources resources, int indent) { + + LogEntryParcel subEntry = mSubResult.getLog().getLast(); + if (subEntry != null) { + return subEntry.getPrintableLogEntry(resources, mIndent +indent); + } else { + return super.getPrintableLogEntry(resources, indent); + } + } + } public Showable createNotify(final Activity activity) { @@ -245,15 +280,15 @@ public abstract class OperationResult implements Parcelable { } return Notify.create(activity, logText, Notify.LENGTH_LONG, style, - new ActionListener() { - @Override - public void onAction() { - Intent intent = new Intent( - activity, LogDisplayActivity.class); - intent.putExtra(LogDisplayFragment.EXTRA_RESULT, OperationResult.this); - activity.startActivity(intent); - } - }, R.string.snackbar_details); + new ActionListener() { + @Override + public void onAction() { + Intent intent = new Intent( + activity, LogDisplayActivity.class); + intent.putExtra(LogDisplayFragment.EXTRA_RESULT, OperationResult.this); + activity.startActivity(intent); + } + }, R.string.snackbar_details); } @@ -289,6 +324,8 @@ public abstract class OperationResult implements Parcelable { MSG_IP_ERROR_IO_EXC (LogLevel.ERROR, R.string.msg_ip_error_io_exc), MSG_IP_ERROR_OP_EXC (LogLevel.ERROR, R.string.msg_ip_error_op_exc), MSG_IP_ERROR_REMOTE_EX (LogLevel.ERROR, R.string.msg_ip_error_remote_ex), + MSG_IP_FINGERPRINT_ERROR (LogLevel.ERROR, R.string.msg_ip_fingerprint_error), + MSG_IP_FINGERPRINT_OK (LogLevel.INFO, R.string.msg_ip_fingerprint_ok), MSG_IP_INSERT_KEYRING (LogLevel.DEBUG, R.string.msg_ip_insert_keyring), MSG_IP_INSERT_SUBKEYS (LogLevel.DEBUG, R.string.msg_ip_insert_keys), MSG_IP_PREPARE (LogLevel.DEBUG, R.string.msg_ip_prepare), @@ -631,6 +668,7 @@ public abstract class OperationResult implements Parcelable { MSG_DC_TRAIL_SYM (LogLevel.DEBUG, R.string.msg_dc_trail_sym), MSG_DC_TRAIL_UNKNOWN (LogLevel.DEBUG, R.string.msg_dc_trail_unknown), MSG_DC_UNLOCKING (LogLevel.INFO, R.string.msg_dc_unlocking), + MSG_DC_INSECURE_ENCRYPTION_KEY (LogLevel.WARN, R.string.msg_dc_insecure_encryption_key), MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO(LogLevel.WARN, R.string.msg_dc_insecure_symmetric_encryption_algo), MSG_DC_INSECURE_HASH_ALGO(LogLevel.ERROR, R.string.msg_dc_insecure_hash_algo), MSG_DC_INSECURE_MDC_MISSING(LogLevel.ERROR, R.string.msg_dc_insecure_mdc_missing), @@ -704,21 +742,21 @@ public abstract class OperationResult implements Parcelable { MSG_IMPORT_FETCH_ERROR (LogLevel.ERROR, R.string.msg_import_fetch_error), MSG_IMPORT_FETCH_ERROR_DECODE (LogLevel.ERROR, R.string.msg_import_fetch_error_decode), + MSG_IMPORT_FETCH_ERROR_KEYSERVER(LogLevel.ERROR, R.string.msg_import_fetch_error_keyserver), + MSG_IMPORT_FETCH_ERROR_KEYSERVER_SECRET (LogLevel.ERROR, R.string.msg_import_fetch_error_keyserver_secret), + MSG_IMPORT_FETCH_KEYBASE (LogLevel.INFO, R.string.msg_import_fetch_keybase), MSG_IMPORT_FETCH_KEYSERVER (LogLevel.INFO, R.string.msg_import_fetch_keyserver), MSG_IMPORT_FETCH_KEYSERVER_OK (LogLevel.DEBUG, R.string.msg_import_fetch_keyserver_ok), - MSG_IMPORT_FETCH_KEYSERVER_ERROR (LogLevel.ERROR, R.string.msg_import_fetch_keyserver_error), - MSG_IMPORT_FETCH_KEYBASE (LogLevel.INFO, R.string.msg_import_fetch_keybase), MSG_IMPORT_KEYSERVER (LogLevel.DEBUG, R.string.msg_import_keyserver), MSG_IMPORT_MERGE (LogLevel.DEBUG, R.string.msg_import_merge), MSG_IMPORT_MERGE_ERROR (LogLevel.ERROR, R.string.msg_import_merge_error), - MSG_IMPORT_FINGERPRINT_ERROR (LogLevel.ERROR, R.string.msg_import_fingerprint_error), - MSG_IMPORT_FINGERPRINT_OK (LogLevel.DEBUG, R.string.msg_import_fingerprint_ok), MSG_IMPORT_ERROR (LogLevel.ERROR, R.string.msg_import_error), MSG_IMPORT_ERROR_IO (LogLevel.ERROR, R.string.msg_import_error_io), 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), @@ -763,10 +801,9 @@ public abstract class OperationResult implements Parcelable { MSG_DEL_FAIL (LogLevel.WARN, R.plurals.msg_del_fail), MSG_REVOKE_ERROR_EMPTY (LogLevel.ERROR, R.string.msg_revoke_error_empty), - MSG_REVOKE_ERROR_MULTI_SECRET (LogLevel.DEBUG, R.string.msg_revoke_error_multi_secret), - MSG_REVOKE_ERROR_NOT_FOUND (LogLevel.DEBUG, R.string.msg_revoke_error_multi_secret), + MSG_REVOKE_ERROR_NOT_FOUND (LogLevel.ERROR, R.string.msg_revoke_error_not_found), MSG_REVOKE (LogLevel.DEBUG, R.string.msg_revoke_key), - MSG_REVOKE_KEY_FAIL (LogLevel.ERROR, R.string.msg_revoke_key_fail), + MSG_REVOKE_ERROR_KEY_FAIL (LogLevel.ERROR, R.string.msg_revoke_key_fail), MSG_REVOKE_OK (LogLevel.OK, R.string.msg_revoke_ok), // keybase verification @@ -781,17 +818,31 @@ public abstract class OperationResult implements Parcelable { MSG_KEYBASE_ERROR_PAYLOAD_MISMATCH(LogLevel.ERROR, R.string.msg_keybase_error_msg_payload_mismatch), - // export log - MSG_EXPORT_LOG(LogLevel.START,R.string.msg_export_log_start), - MSG_EXPORT_LOG_EXPORT_ERROR_NO_FILE(LogLevel.ERROR,R.string.msg_export_log_error_no_file), - MSG_EXPORT_LOG_EXPORT_ERROR_FOPEN(LogLevel.ERROR,R.string.msg_export_log_error_fopen), - MSG_EXPORT_LOG_EXPORT_ERROR_WRITING(LogLevel.ERROR,R.string.msg_export_log_error_writing), - MSG_EXPORT_LOG_EXPORT_SUCCESS (LogLevel.OK, R.string.msg_export_log_success), - - // mim parsing + // mime parsing MSG_MIME_PARSING(LogLevel.START,R.string.msg_mime_parsing_start), MSG_MIME_PARSING_ERROR(LogLevel.ERROR,R.string.msg_mime_parsing_error), MSG_MIME_PARSING_SUCCESS(LogLevel.OK,R.string.msg_mime_parsing_success), + + MSG_LV (LogLevel.START, R.string.msg_lv), + MSG_LV_MATCH (LogLevel.DEBUG, R.string.msg_lv_match), + MSG_LV_MATCH_ERROR (LogLevel.ERROR, R.string.msg_lv_match_error), + MSG_LV_FP_OK (LogLevel.DEBUG, R.string.msg_lv_fp_ok), + MSG_LV_FP_ERROR (LogLevel.ERROR, R.string.msg_lv_fp_error), + + MSG_LV_ERROR_TWITTER_AUTH (LogLevel.ERROR, R.string.msg_lv_error_twitter_auth), + MSG_LV_ERROR_TWITTER_HANDLE (LogLevel.ERROR, R.string.msg_lv_error_twitter_handle), + MSG_LV_ERROR_TWITTER_RESPONSE (LogLevel.ERROR, R.string.msg_lv_error_twitter_response), + MSG_LV_ERROR_GITHUB_HANDLE (LogLevel.ERROR, R.string.msg_lv_error_github_handle), + MSG_LV_ERROR_GITHUB_NOT_FOUND (LogLevel.ERROR, R.string.msg_lv_error_github_not_found), + + MSG_LV_FETCH (LogLevel.DEBUG, R.string.msg_lv_fetch), + MSG_LV_FETCH_REDIR (LogLevel.DEBUG, R.string.msg_lv_fetch_redir), + MSG_LV_FETCH_OK (LogLevel.DEBUG, R.string.msg_lv_fetch_ok), + MSG_LV_FETCH_ERROR (LogLevel.ERROR, R.string.msg_lv_fetch_error), + MSG_LV_FETCH_ERROR_URL (LogLevel.ERROR, R.string.msg_lv_fetch_error_url), + 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), ; public final int mMsgId; @@ -815,6 +866,10 @@ public abstract class OperationResult implements Parcelable { OK, // should occur once at the end of a successful operation CANCELLED, // should occur once at the end of a cancelled operation } + // for print of debug log. keep those in sync with above! + static final String[] LOG_LEVEL_NAME = new String[] { + "[DEBUG]", "[INFO]", "[WARN]", "[ERROR]", "[START]", "[OK]", "[CANCEL]" + }; @Override public int describeContents() { @@ -913,6 +968,20 @@ public abstract class OperationResult implements Parcelable { public Iterator<LogEntryParcel> iterator() { return mParcels.iterator(); } + + /** + * returns an indented String of an entire OperationLog + * @param indent padding to add at the start of all log entries, made for use with SubLogs + * @return printable, indented version of passed operationLog + */ + public String getPrintableOperationLog(Resources resources, int indent) { + StringBuilder log = new StringBuilder(); + for (LogEntryParcel entry : this) { + log.append(entry.getPrintableLogEntry(resources, indent)).append("\n"); + } + return log.toString().substring(0, log.length() -1); // get rid of extra new line + } + } } 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 770e8de91..18a27dd96 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/CanonicalizedKeyRing.java @@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.pgp; import org.spongycastle.openpgp.PGPKeyRing; import org.spongycastle.openpgp.PGPPublicKey; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.IterableIterator; import java.io.IOException; @@ -28,6 +29,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; +import java.util.Iterator; import java.util.Set; @@ -152,4 +154,14 @@ public abstract class CanonicalizedKeyRing extends KeyRing { return getRing().getEncoded(); } + public boolean containsSubkey(String expectedFingerprint) { + for (CanonicalizedPublicKey key : publicKeyIterator()) { + if (KeyFormattingUtils.convertFingerprintToHex( + key.getFingerprint()).equalsIgnoreCase(expectedFingerprint)) { + return true; + } + } + return false; + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java index 1e51403fc..dda15f382 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java @@ -79,9 +79,9 @@ import java.security.SignatureException; import java.util.Date; import java.util.Iterator; -public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> { +public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInputParcel> { - public PgpDecryptVerify(Context context, ProviderHelper providerHelper, Progressable progressable) { + public PgpDecryptVerifyOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { super(context, providerHelper, progressable); } @@ -313,6 +313,27 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> return result; } + private static class EncryptStreamResult { + + // this is non-null iff an error occured, return directly + DecryptVerifyResult errorResult; + + // for verification + PGPEncryptedData encryptedData; + InputStream cleartextStream; + + int symmetricEncryptionAlgo = 0; + + boolean skippedDisallowedKey = false; + boolean insecureEncryptionKey = false; + + // convenience method to return with error + public EncryptStreamResult with(DecryptVerifyResult result) { + errorResult = result; + return this; + } + + } /** Decrypt and/or verify binary or ascii armored pgp data. */ @NonNull @@ -320,42 +341,14 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> PgpDecryptVerifyInputParcel input, CryptoInputParcel cryptoInput, InputStream in, OutputStream out, int indent) throws IOException, PGPException { - OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); - OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder(); OperationLog log = new OperationLog(); log.add(LogType.MSG_DC, indent); indent += 1; - JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in); - PGPEncryptedDataList enc; - Object o = pgpF.nextObject(); - int currentProgress = 0; updateProgress(R.string.progress_reading_data, currentProgress, 100); - if (o instanceof PGPEncryptedDataList) { - enc = (PGPEncryptedDataList) o; - } else { - enc = (PGPEncryptedDataList) pgpF.nextObject(); - } - - if (enc == null) { - log.add(LogType.MSG_DC_ERROR_INVALID_DATA, indent); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); - } - - InputStream clear; - PGPEncryptedData encryptedData; - - PGPPublicKeyEncryptedData encryptedDataAsymmetric = null; - PGPPBEEncryptedData encryptedDataSymmetric = null; - CanonicalizedSecretKey secretEncryptionKey = null; - Iterator<?> it = enc.getEncryptedDataObjects(); - boolean asymmetricPacketFound = false; - boolean symmetricPacketFound = false; - boolean anyPacketFound = false; - // 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; @@ -375,8 +368,332 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> } } + OpenPgpSignatureResultBuilder signatureResultBuilder = new OpenPgpSignatureResultBuilder(); + OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder(); + + JcaPGPObjectFactory plainFact; + Object dataChunk; + EncryptStreamResult esResult = null; + { // resolve encrypted (symmetric and asymmetric) packets + JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in); + Object obj = pgpF.nextObject(); + + if (obj instanceof PGPEncryptedDataList) { + esResult = handleEncryptedPacket( + input, cryptoInput, (PGPEncryptedDataList) obj, log, indent, currentProgress); + + // if there is an error, nothing left to do here + if (esResult.errorResult != null) { + return esResult.errorResult; + } + + // if this worked out so far, the data is encrypted + decryptionResultBuilder.setEncrypted(true); + + if (esResult.insecureEncryptionKey) { + log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1); + decryptionResultBuilder.setInsecure(true); + } + + // Check for insecure encryption algorithms! + if (!PgpSecurityConstants.isSecureSymmetricAlgorithm(esResult.symmetricEncryptionAlgo)) { + log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1); + decryptionResultBuilder.setInsecure(true); + } + + plainFact = new JcaPGPObjectFactory(esResult.cleartextStream); + dataChunk = plainFact.nextObject(); + + } else { + decryptionResultBuilder.setEncrypted(false); + + plainFact = pgpF; + dataChunk = obj; + } + + } + + 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; + + // resolve compressed data + if (dataChunk instanceof PGPCompressedData) { + log.add(LogType.MSG_DC_CLEAR_DECOMPRESS, indent + 1); + currentProgress += 2; + updateProgress(R.string.progress_decompressing_data, currentProgress, 100); + + PGPCompressedData compressedData = (PGPCompressedData) dataChunk; + + JcaPGPObjectFactory fact = new JcaPGPObjectFactory(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); + } + + dataChunk = plainFact.nextObject(); + } + + if (dataChunk instanceof PGPSignatureList) { + // skip + dataChunk = plainFact.nextObject(); + } + + OpenPgpMetadata metadata; + + if ( ! (dataChunk instanceof PGPLiteralData)) { + + log.add(LogType.MSG_DC_ERROR_INVALID_DATA, indent); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + + } + + log.add(LogType.MSG_DC_CLEAR_DATA, indent + 1); + indent += 2; + currentProgress += 4; + updateProgress(R.string.progress_decrypting, currentProgress, 100); + + PGPLiteralData literalData = (PGPLiteralData) dataChunk; + + String originalFilename = literalData.getFileName(); + if (originalFilename.contains("/")) { + originalFilename = originalFilename.substring(originalFilename.lastIndexOf('/')); + } + String mimeType = null; + if (literalData.getFormat() == PGPLiteralData.TEXT + || literalData.getFormat() == PGPLiteralData.UTF8) { + mimeType = "text/plain"; + } else { + // try to guess from file ending + String extension = MimeTypeMap.getFileExtensionFromUrl(originalFilename); + if (extension != null) { + MimeTypeMap mime = MimeTypeMap.getSingleton(); + mimeType = mime.getMimeTypeFromExtension(extension); + } + if (mimeType == null) { + mimeType = "application/octet-stream"; + } + } + + 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()) { + + // this operation skips the entire stream to find the data length! + Long originalSize = literalData.findDataLength(); + + if (originalSize != null) { + log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1, + Long.toString(originalSize)); + } else { + log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1); + } + + metadata = new OpenPgpMetadata( + originalFilename, + mimeType, + literalData.getModificationTime().getTime(), + originalSize == null ? 0 : originalSize); + + log.add(LogType.MSG_DC_OK_META_ONLY, indent); + DecryptVerifyResult result = + new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); + result.setCharset(charset); + result.setDecryptionMetadata(metadata); + 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); + + InputStream dataIn = literalData.getInputStream(); + + long alreadyWritten = 0; + long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition(); + int length; + byte[] buffer = new byte[1 << 16]; + while ((length = dataIn.read(buffer)) > 0) { + // Log.d(Constants.TAG, "read bytes: " + length); + if (out != null) { + out.write(buffer, 0, length); + } + + // update signature buffer if signature is also present + if (signature != null) { + signature.update(buffer, 0, length); + } + + alreadyWritten += length; + if (wholeSize > 0) { + long progress = 100 * alreadyWritten / wholeSize; + // stop at 100% for wrong file sizes... + if (progress > 100) { + progress = 100; + } + progressScaler.setProgress((int) progress, 100); + } + // TODO: slow annealing to fake a progress? + } + + metadata = new OpenPgpMetadata( + originalFilename, mimeType, literalData.getModificationTime().getTime(), alreadyWritten); + + if (signature != null) { + updateProgress(R.string.progress_verifying_signature, 90, 100); + log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); + + 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); + } + + // 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); + } + + indent -= 1; + + if (esResult != null) { + if (esResult.encryptedData.isIntegrityProtected()) { + updateProgress(R.string.progress_verifying_integrity, 95, 100); + + if (esResult.encryptedData.verify()) { + log.add(LogType.MSG_DC_INTEGRITY_CHECK_OK, indent); + } else { + log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent); + return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + } + } else if (signature == null) { + // 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! + log.add(LogType.MSG_DC_INSECURE_MDC_MISSING, indent); + decryptionResultBuilder.setInsecure(true); + } + } + + updateProgress(R.string.progress_done, 100, 100); + + log.add(LogType.MSG_DC_OK, indent); + + // Return a positive result, with metadata and verification info + DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); + + result.setCachedCryptoInputParcel(cryptoInput); + result.setSignatureResult(signatureResultBuilder.build()); + result.setCharset(charset); + result.setDecryptionResult(decryptionResultBuilder.build()); + result.setDecryptionMetadata(metadata); + + 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(); + } + */ + + EncryptStreamResult result = new EncryptStreamResult(); + + boolean asymmetricPacketFound = false; + boolean symmetricPacketFound = false; + boolean anyPacketFound = false; + + PGPPublicKeyEncryptedData encryptedDataAsymmetric = null; + PGPPBEEncryptedData encryptedDataSymmetric = null; + CanonicalizedSecretKey secretEncryptionKey = null; + Passphrase passphrase = null; - boolean skippedDisallowedKey = false; + + Iterator<?> it = enc.getEncryptedDataObjects(); // go through all objects and find one we can decrypt while (it.hasNext()) { @@ -420,7 +737,7 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> if (!input.getAllowedKeyIds().contains(masterKeyId)) { // this key is in our db, but NOT allowed! // continue with the next packet in the while loop - skippedDisallowedKey = true; + result.skippedDisallowedKey = true; log.add(LogType.MSG_DC_ASKIP_NOT_ALLOWED, indent + 1); continue; } @@ -451,23 +768,23 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> log.add(LogType.MSG_DC_PASS_CACHED, indent + 1); } catch (PassphraseCacheInterface.NoSecretKeyException e) { log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); } // if passphrase was not cached, return here indicating that a passphrase is missing! if (passphrase == null) { log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1); - return new DecryptVerifyResult(log, + return result.with(new DecryptVerifyResult(log, RequiredInputParcel.createRequiredDecryptPassphrase( - secretKeyRing.getMasterKeyId(), secretEncryptionKey.getKeyId()), - cryptoInput); + secretKeyRing.getMasterKeyId(), secretEncryptionKey.getKeyId()), + cryptoInput)); } } // check for insecure encryption key if ( ! PgpSecurityConstants.isSecureKey(secretEncryptionKey)) { log.add(LogType.MSG_DC_INSECURE_KEY, indent + 1); - decryptionResultBuilder.setInsecure(true); + result.insecureEncryptionKey = true; } // break out of while, only decrypt the first packet where we have a key @@ -504,9 +821,9 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> if (passphrase == null) { log.add(LogType.MSG_DC_PENDING_PASSPHRASE, indent + 1); - return new DecryptVerifyResult(log, + return result.with(new DecryptVerifyResult(log, RequiredInputParcel.createRequiredSymmetricPassphrase(), - cryptoInput); + cryptoInput)); } } else { @@ -533,10 +850,7 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> } } - log.add(LogType.MSG_DC_PREP_STREAMS, indent); - // we made sure above one of these two would be true - int symmetricEncryptionAlgo; if (symmetricPacketFound) { currentProgress += 2; updateProgress(R.string.progress_preparing_streams, currentProgress, 100); @@ -548,16 +862,16 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> passphrase.getCharArray()); try { - clear = encryptedDataSymmetric.getDataStream(decryptorFactory); + result.cleartextStream = encryptedDataSymmetric.getDataStream(decryptorFactory); } catch (PGPDataValidationException e) { - log.add(LogType.MSG_DC_ERROR_SYM_PASSPHRASE, indent +1); - return new DecryptVerifyResult(log, - RequiredInputParcel.createRequiredSymmetricPassphrase(), cryptoInput); + log.add(LogType.MSG_DC_ERROR_SYM_PASSPHRASE, indent + 1); + return result.with(new DecryptVerifyResult(log, + RequiredInputParcel.createRequiredSymmetricPassphrase(), cryptoInput)); } - encryptedData = encryptedDataSymmetric; + result.encryptedData = encryptedDataSymmetric; - symmetricEncryptionAlgo = encryptedDataSymmetric.getSymmetricAlgorithm(decryptorFactory); + result.symmetricEncryptionAlgo = encryptedDataSymmetric.getSymmetricAlgorithm(decryptorFactory); } else if (asymmetricPacketFound) { currentProgress += 2; updateProgress(R.string.progress_extracting_key, currentProgress, 100); @@ -566,11 +880,11 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> log.add(LogType.MSG_DC_UNLOCKING, indent + 1); if (!secretEncryptionKey.unlock(passphrase)) { log.add(LogType.MSG_DC_ERROR_BAD_PASSPHRASE, indent + 1); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); } } catch (PgpGeneralException e) { log.add(LogType.MSG_DC_ERROR_EXTRACT_KEY, indent + 1); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); } currentProgress += 2; @@ -585,300 +899,41 @@ public class PgpDecryptVerify extends BaseOperation<PgpDecryptVerifyInputParcel> && !decryptorFactory.hasCachedSessionData(encryptedDataAsymmetric)) { log.add(LogType.MSG_DC_PENDING_NFC, indent + 1); - return new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation( + return result.with(new DecryptVerifyResult(log, RequiredInputParcel.createNfcDecryptOperation( secretEncryptionKey.getRing().getMasterKeyId(), secretEncryptionKey.getKeyId(), encryptedDataAsymmetric.getSessionKey()[0] - ), - cryptoInput); + ), cryptoInput)); } try { - clear = encryptedDataAsymmetric.getDataStream(decryptorFactory); + result.cleartextStream = encryptedDataAsymmetric.getDataStream(decryptorFactory); } catch (PGPKeyValidationException | ArrayIndexOutOfBoundsException e) { log.add(LogType.MSG_DC_ERROR_CORRUPT_DATA, indent + 1); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); } - symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory); + result.symmetricEncryptionAlgo = encryptedDataAsymmetric.getSymmetricAlgorithm(decryptorFactory); + result.encryptedData = encryptedDataAsymmetric; cryptoInput.addCryptoData(decryptorFactory.getCachedSessionKeys()); - encryptedData = encryptedDataAsymmetric; } else { // there wasn't even any useful data if (!anyPacketFound) { log.add(LogType.MSG_DC_ERROR_NO_DATA, indent + 1); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_NO_DATA, log); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_NO_DATA, log)); } // there was data but key wasn't allowed - if (skippedDisallowedKey) { + if (result.skippedDisallowedKey) { log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_KEY_DISALLOWED, log); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_KEY_DISALLOWED, log)); } // no packet has been found where we have the corresponding secret key in our db log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); - } - decryptionResultBuilder.setEncrypted(true); - - // Check for insecure encryption algorithms! - if (!PgpSecurityConstants.isSecureSymmetricAlgorithm(symmetricEncryptionAlgo)) { - log.add(LogType.MSG_DC_INSECURE_SYMMETRIC_ENCRYPTION_ALGO, indent + 1); - decryptionResultBuilder.setInsecure(true); - } - - JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear); - Object dataChunk = plainFact.nextObject(); - int signatureIndex = -1; - CanonicalizedPublicKeyRing signingRing = null; - CanonicalizedPublicKey signingKey = null; - - log.add(LogType.MSG_DC_CLEAR, indent); - indent += 1; - - if (dataChunk instanceof PGPCompressedData) { - log.add(LogType.MSG_DC_CLEAR_DECOMPRESS, indent + 1); - currentProgress += 2; - updateProgress(R.string.progress_decompressing_data, currentProgress, 100); - - PGPCompressedData compressedData = (PGPCompressedData) dataChunk; - - JcaPGPObjectFactory fact = new JcaPGPObjectFactory(compressedData.getDataStream()); - dataChunk = fact.nextObject(); - plainFact = fact; - } - - 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); - } - - dataChunk = plainFact.nextObject(); + return result.with(new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log)); } - if (dataChunk instanceof PGPSignatureList) { - // skip - dataChunk = plainFact.nextObject(); - } - - OpenPgpMetadata metadata; - - if (dataChunk instanceof PGPLiteralData) { - log.add(LogType.MSG_DC_CLEAR_DATA, indent + 1); - indent += 2; - currentProgress += 4; - updateProgress(R.string.progress_decrypting, currentProgress, 100); - - PGPLiteralData literalData = (PGPLiteralData) dataChunk; - - String originalFilename = literalData.getFileName(); - String mimeType = null; - if (literalData.getFormat() == PGPLiteralData.TEXT - || literalData.getFormat() == PGPLiteralData.UTF8) { - mimeType = "text/plain"; - } else { - // try to guess from file ending - String extension = MimeTypeMap.getFileExtensionFromUrl(originalFilename); - if (extension != null) { - MimeTypeMap mime = MimeTypeMap.getSingleton(); - mimeType = mime.getMimeTypeFromExtension(extension); - } - if (mimeType == null) { - mimeType = "application/octet-stream"; - } - } - - 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()) { - - // this operation skips the entire stream to find the data length! - Long originalSize = literalData.findDataLength(); - - if (originalSize != null) { - log.add(LogType.MSG_DC_CLEAR_META_SIZE, indent + 1, - Long.toString(originalSize)); - } else { - log.add(LogType.MSG_DC_CLEAR_META_SIZE_UNKNOWN, indent + 1); - } - - metadata = new OpenPgpMetadata( - originalFilename, - mimeType, - literalData.getModificationTime().getTime(), - originalSize == null ? 0 : originalSize); - - log.add(LogType.MSG_DC_OK_META_ONLY, indent); - DecryptVerifyResult result = - new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); - result.setCharset(charset); - result.setDecryptionMetadata(metadata); - return result; - } - - int endProgress; - if (signature != null) { - endProgress = 90; - } else if (encryptedData.isIntegrityProtected()) { - endProgress = 95; - } else { - endProgress = 100; - } - ProgressScaler progressScaler = - new ProgressScaler(mProgressable, currentProgress, endProgress, 100); - - InputStream dataIn = literalData.getInputStream(); - - long alreadyWritten = 0; - long wholeSize = 0; // TODO inputData.getSize() - inputData.getStreamPosition(); - int length; - byte[] buffer = new byte[1 << 16]; - while ((length = dataIn.read(buffer)) > 0) { - // Log.d(Constants.TAG, "read bytes: " + length); - if (out != null) { - out.write(buffer, 0, length); - } - - // update signature buffer if signature is also present - if (signature != null) { - signature.update(buffer, 0, length); - } - - alreadyWritten += length; - if (wholeSize > 0) { - long progress = 100 * alreadyWritten / wholeSize; - // stop at 100% for wrong file sizes... - if (progress > 100) { - progress = 100; - } - progressScaler.setProgress((int) progress, 100); - } - // TODO: slow annealing to fake a progress? - } - - metadata = new OpenPgpMetadata( - originalFilename, - mimeType, - literalData.getModificationTime().getTime(), - alreadyWritten); - - if (signature != null) { - updateProgress(R.string.progress_verifying_signature, 90, 100); - log.add(LogType.MSG_DC_CLEAR_SIGNATURE_CHECK, indent); - - PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject(); - PGPSignature messageSignature = signatureList.get(signatureIndex); - - // TODO: what about binary signatures? - - // 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); - } - - // 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); - } - - indent -= 1; - } else { - // If there is no literalData, we don't have any metadata - metadata = null; - } - - if (encryptedData.isIntegrityProtected()) { - updateProgress(R.string.progress_verifying_integrity, 95, 100); - - if (encryptedData.verify()) { - log.add(LogType.MSG_DC_INTEGRITY_CHECK_OK, indent); - } else { - log.add(LogType.MSG_DC_ERROR_INTEGRITY_CHECK, indent); - return new DecryptVerifyResult(DecryptVerifyResult.RESULT_ERROR, log); - } - } else { - // If no valid signature is present: - // Handle missing integrity protection like failed integrity protection! - // The MDC packet can be stripped by an attacker! - Log.d(Constants.TAG, "MDC fail"); - if (!signatureResultBuilder.isValidSignature()) { - log.add(LogType.MSG_DC_INSECURE_MDC_MISSING, indent); - decryptionResultBuilder.setInsecure(true); - } - } - - updateProgress(R.string.progress_done, 100, 100); - - log.add(LogType.MSG_DC_OK, indent); - - // Return a positive result, with metadata and verification info - DecryptVerifyResult result = new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log); - result.setCachedCryptoInputParcel(cryptoInput); - result.setSignatureResult(signatureResultBuilder.build()); - result.setCharset(charset); - result.setDecryptionResult(decryptionResultBuilder.build()); - result.setDecryptionMetadata(metadata); return result; } 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 32718fb4e..e8d1d3111 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpHelper.java @@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.pgp; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; +import android.support.annotation.NonNull; import android.text.TextUtils; import org.sufficientlysecure.keychain.Constants; @@ -116,32 +117,27 @@ public class PgpHelper { } } - public static String getPgpContent(CharSequence input) { - // only decrypt if clipboard content is available and a pgp message or cleartext signature - if (!TextUtils.isEmpty(input)) { - Log.dEscaped(Constants.TAG, "input: " + input); + public static String getPgpContent(@NonNull CharSequence input) { + Log.dEscaped(Constants.TAG, "input: " + input); + + Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input); + if (matcher.matches()) { + String text = matcher.group(1); + text = fixPgpMessage(text); - Matcher matcher = PgpHelper.PGP_MESSAGE.matcher(input); + Log.dEscaped(Constants.TAG, "input fixed: " + text); + return text; + } else { + matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input); if (matcher.matches()) { String text = matcher.group(1); - text = fixPgpMessage(text); + text = fixPgpCleartextSignature(text); Log.dEscaped(Constants.TAG, "input fixed: " + text); return text; } else { - matcher = PgpHelper.PGP_CLEARTEXT_SIGNATURE.matcher(input); - if (matcher.matches()) { - String text = matcher.group(1); - text = fixPgpCleartextSignature(text); - - Log.dEscaped(Constants.TAG, "input fixed: " + text); - return text; - } else { - return null; - } + return null; } - } else { - 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 3fa549946..cbd8ce47a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSecurityConstants.java @@ -23,6 +23,7 @@ import org.spongycastle.bcpg.HashAlgorithmTags; import org.spongycastle.bcpg.PublicKeyAlgorithmTags; import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags; +import java.util.Arrays; import java.util.HashSet; /** @@ -42,24 +43,23 @@ public class PgpSecurityConstants { * Whitelist of accepted symmetric encryption algorithms * all other algorithms are rejected with OpenPgpDecryptionResult.RESULT_INSECURE */ - private static HashSet<Integer> sSymmetricAlgorithmsWhitelist = new HashSet<>(); - static { - // General remarks: We try to keep the whitelist short to reduce attack surface - // TODO: block IDEA?: Bad key schedule (weak keys), implementation difficulties (easy to make errors) - sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.IDEA); - sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.TRIPLE_DES); // a MUST in RFC - sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.CAST5); // default in many gpg, pgp versions, 128 bit key - // BLOWFISH: Twofish is the successor - // SAFER: not used widely - // DES: < 128 bit security - sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_128); - sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_192); - sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.AES_256); - sSymmetricAlgorithmsWhitelist.add(SymmetricKeyAlgorithmTags.TWOFISH); // 128 bit - // CAMELLIA_128: not used widely - // CAMELLIA_192: not used widely - // CAMELLIA_256: not used widely - } + private static HashSet<Integer> sSymmetricAlgorithmsWhitelist = new HashSet<>(Arrays.asList( + // General remarks: We try to keep the whitelist short to reduce attack surface + // TODO: block IDEA?: Bad key schedule (weak keys), implementation difficulties (easy to make errors) + SymmetricKeyAlgorithmTags.IDEA, + SymmetricKeyAlgorithmTags.TRIPLE_DES, // a MUST in RFC + SymmetricKeyAlgorithmTags.CAST5, // default in many gpg, pgp versions, 128 bit key + // BLOWFISH: Twofish is the successor + // SAFER: not used widely + // DES: < 128 bit security + SymmetricKeyAlgorithmTags.AES_128, + SymmetricKeyAlgorithmTags.AES_192, + SymmetricKeyAlgorithmTags.AES_256, + SymmetricKeyAlgorithmTags.TWOFISH // 128 bit + // CAMELLIA_128: not used widely + // CAMELLIA_192: not used widely + // CAMELLIA_256: not used widely + )); public static boolean isSecureSymmetricAlgorithm(int id) { return sSymmetricAlgorithmsWhitelist.contains(id); @@ -77,20 +77,19 @@ public class PgpSecurityConstants { * ((collision resistance of 112-bits)) * Implementations SHOULD NOT sign SHA-256 hashes. They MUST NOT default to signing SHA-256 hashes. */ - private static HashSet<Integer> sHashAlgorithmsWhitelist = new HashSet<>(); - static { - // MD5: broken - // SHA1: broken - // RIPEMD160: same security properties as SHA1 - // DOUBLE_SHA: not used widely - // MD2: not used widely - // TIGER_192: not used widely - // HAVAL_5_160: not used widely - sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA256); // compatibility for old Mailvelope versions - sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA384); - sHashAlgorithmsWhitelist.add(HashAlgorithmTags.SHA512); - // SHA224: Not used widely, Yahoo argues against it - } + private static HashSet<Integer> sHashAlgorithmsWhitelist = new HashSet<>(Arrays.asList( + // MD5: broken + // SHA1: broken + // RIPEMD160: same security properties as SHA1 + // DOUBLE_SHA: not used widely + // MD2: not used widely + // TIGER_192: not used widely + // HAVAL_5_160: not used widely + HashAlgorithmTags.SHA256, // compatibility for old Mailvelope versions + HashAlgorithmTags.SHA384, + HashAlgorithmTags.SHA512 + // SHA224: Not used widely, Yahoo argues against it + )); public static boolean isSecureHashAlgorithm(int id) { return sHashAlgorithmsWhitelist.contains(id); @@ -106,12 +105,11 @@ public class PgpSecurityConstants { * bitlength less than 1023 bits. * Implementations MUST NOT accept any RSA keys with bitlength less than 2047 bits after January 1, 2016. */ - private static HashSet<String> sCurveWhitelist = new HashSet<>(); - static { - sCurveWhitelist.add(NISTNamedCurves.getOID("P-256").getId()); - sCurveWhitelist.add(NISTNamedCurves.getOID("P-384").getId()); - sCurveWhitelist.add(NISTNamedCurves.getOID("P-521").getId()); - } + private static HashSet<String> sCurveWhitelist = new HashSet<>(Arrays.asList( + NISTNamedCurves.getOID("P-256").getId(), + NISTNamedCurves.getOID("P-384").getId(), + NISTNamedCurves.getOID("P-521").getId() + )); public static boolean isSecureKey(CanonicalizedPublicKey key) { switch (key.getAlgorithm()) { 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 a7baddf8b..ca98882d8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java @@ -216,17 +216,6 @@ public class UncachedKeyRing implements Serializable { } - public boolean containsSubkey(String expectedFingerprint) { - Iterator<PGPPublicKey> it = mRing.getPublicKeys(); - while (it.hasNext()) { - if (KeyFormattingUtils.convertFingerprintToHex( - it.next().getFingerprint()).equalsIgnoreCase(expectedFingerprint)) { - return true; - } - } - return false; - } - public interface IteratorWithIOThrow<E> { public boolean hasNext() throws IOException; public E next() throws IOException; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java index 26f046372..013a6bf14 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java @@ -368,4 +368,5 @@ public class UncachedPublicKey { return calendar.getTime(); } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java index 2c7f0187a..535314607 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedUserAttribute.java @@ -1,4 +1,8 @@ /* +<<<<<<< HEAD + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> +======= +>>>>>>> development * Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com> * * This program is free software: you can redistribute it and/or modify @@ -37,6 +41,7 @@ public class WrappedUserAttribute implements Serializable { public static final int UAT_NONE = 0; public static final int UAT_IMAGE = UserAttributeSubpacketTags.IMAGE_ATTRIBUTE; + public static final int UAT_URI_ATTRIBUTE = 101; private PGPUserAttributeSubpacketVector mVector; @@ -77,7 +82,7 @@ public class WrappedUserAttribute implements Serializable { public static WrappedUserAttribute fromData (byte[] data) throws IOException { UserAttributeSubpacketInputStream in = new UserAttributeSubpacketInputStream(new ByteArrayInputStream(data)); - ArrayList<UserAttributeSubpacket> list = new ArrayList<UserAttributeSubpacket>(); + ArrayList<UserAttributeSubpacket> list = new ArrayList<>(); while (in.available() > 0) { list.add(in.readPacket()); } @@ -121,6 +126,7 @@ public class WrappedUserAttribute implements Serializable { private void readObjectNoData() throws ObjectStreamException { } + @SuppressWarnings("SimplifiableIfStatement") @Override public boolean equals(Object o) { if (!WrappedUserAttribute.class.isInstance(o)) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java index 0d9a4ac16..ee28b5f36 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -51,6 +51,11 @@ public class KeychainContract { String EXPIRY = "expiry"; } + interface UpdatedKeysColumns { + String MASTER_KEY_ID = "master_key_id"; // not a database id + String LAST_UPDATED = "last_updated"; // time since epoch in seconds + } + interface UserPacketsColumns { String MASTER_KEY_ID = "master_key_id"; // foreign key to key_rings._ID String TYPE = "type"; // not a database id @@ -97,6 +102,8 @@ public class KeychainContract { public static final String BASE_KEY_RINGS = "key_rings"; + public static final String BASE_UPDATED_KEYS = "updated_keys"; + public static final String PATH_UNIFIED = "unified"; public static final String PATH_FIND = "find"; @@ -106,6 +113,7 @@ public class KeychainContract { public static final String PATH_PUBLIC = "public"; public static final String PATH_SECRET = "secret"; public static final String PATH_USER_IDS = "user_ids"; + public static final String PATH_LINKED_IDS = "linked_ids"; public static final String PATH_KEYS = "keys"; public static final String PATH_CERTS = "certs"; @@ -234,6 +242,16 @@ public class KeychainContract { } + public static class UpdatedKeys implements UpdatedKeysColumns, BaseColumns { + public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() + .appendPath(BASE_UPDATED_KEYS).build(); + + public static final String CONTENT_TYPE + = "vnd.android.cursor.dir/vnd.org.sufficientlysecure.keychain.provider.updated_keys"; + public static final String CONTENT_ITEM_TYPE + = "vnd.android.cursor.item/vnd.org.sufficientlysecure.keychain.provider.updated_keys"; + } + public static class UserPackets implements UserPacketsColumns, BaseColumns { public static final String VERIFIED = "verified"; public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon() @@ -262,6 +280,11 @@ public class KeychainContract { public static Uri buildUserIdsUri(Uri uri) { return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_USER_IDS).build(); } + + public static Uri buildLinkedIdsUri(Uri uri) { + return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_LINKED_IDS).build(); + } + } public static class ApiApps implements ApiAppsColumns, BaseColumns { @@ -350,7 +373,14 @@ public class KeychainContract { } public static Uri buildCertsUri(Uri uri) { - return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)).appendPath(PATH_CERTS).build(); + return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)) + .appendPath(PATH_CERTS).build(); + } + + public static Uri buildLinkedIdCertsUri(Uri uri, int rank) { + return CONTENT_URI.buildUpon().appendPath(uri.getPathSegments().get(1)) + .appendPath(PATH_LINKED_IDS).appendPath(Integer.toString(rank)) + .appendPath(PATH_CERTS).build(); } } 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 4d16d44c5..d7fb738fc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainDatabase.java @@ -34,6 +34,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAppsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.CertsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns; +import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeysColumns; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns; import org.sufficientlysecure.keychain.ui.ConsolidateDialogActivity; import org.sufficientlysecure.keychain.util.Log; @@ -53,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 = 11; + private static final int DATABASE_VERSION = 12; static Boolean apgHack = false; private Context mContext; @@ -61,6 +62,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { String KEY_RINGS_PUBLIC = "keyrings_public"; String KEY_RINGS_SECRET = "keyrings_secret"; String KEYS = "keys"; + String UPDATED_KEYS = "updated_keys"; String USER_PACKETS = "user_packets"; String CERTS = "certs"; String API_APPS = "api_apps"; @@ -144,6 +146,14 @@ public class KeychainDatabase extends SQLiteOpenHelper { + Tables.USER_PACKETS + "(" + UserPacketsColumns.MASTER_KEY_ID + ", " + UserPacketsColumns.RANK + ") ON DELETE CASCADE" + ")"; + private static final String CREATE_UPDATE_KEYS = + "CREATE TABLE IF NOT EXISTS " + Tables.UPDATED_KEYS + " (" + + UpdatedKeysColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY, " + + UpdatedKeysColumns.LAST_UPDATED + " INTEGER, " + + "FOREIGN KEY(" + UpdatedKeysColumns.MASTER_KEY_ID + ") REFERENCES " + + Tables.KEY_RINGS_PUBLIC + "(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE" + + ")"; + private static final String CREATE_API_APPS = "CREATE TABLE IF NOT EXISTS " + Tables.API_APPS + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " @@ -206,6 +216,7 @@ public class KeychainDatabase extends SQLiteOpenHelper { db.execSQL(CREATE_KEYS); db.execSQL(CREATE_USER_PACKETS); db.execSQL(CREATE_CERTS); + db.execSQL(CREATE_UPDATE_KEYS); db.execSQL(CREATE_API_APPS); db.execSQL(CREATE_API_APPS_ACCOUNTS); db.execSQL(CREATE_API_APPS_ALLOWED_KEYS); @@ -278,8 +289,11 @@ public class KeychainDatabase extends SQLiteOpenHelper { // fix problems in database, see #1402 for details // https://github.com/open-keychain/open-keychain/issues/1402 db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3"); + case 12: + 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 + // no consolidate if we are updating from 10, we're just here for + // the api_accounts fix and the new update keys table return; } 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 3ed9b1da9..d722fa9e7 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -31,6 +31,7 @@ import android.net.Uri; import android.text.TextUtils; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAccounts; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; @@ -38,6 +39,7 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.UpdatedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPacketsColumns; import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; @@ -62,6 +64,8 @@ public class KeychainProvider extends ContentProvider { private static final int KEY_RING_SECRET = 204; private static final int KEY_RING_CERTS = 205; private static final int KEY_RING_CERTS_SPECIFIC = 206; + private static final int KEY_RING_LINKED_IDS = 207; + private static final int KEY_RING_LINKED_ID_CERTS = 208; private static final int API_APPS = 301; private static final int API_APPS_BY_PACKAGE_NAME = 302; @@ -72,6 +76,9 @@ public class KeychainProvider extends ContentProvider { private static final int KEY_RINGS_FIND_BY_EMAIL = 400; private static final int KEY_RINGS_FIND_BY_SUBKEY = 401; + private static final int UPDATED_KEYS = 500; + private static final int UPDATED_KEYS_SPECIFIC = 501; + protected UriMatcher mUriMatcher; /** @@ -127,6 +134,9 @@ public class KeychainProvider extends ContentProvider { * key_rings/_/unified * key_rings/_/keys * key_rings/_/user_ids + * key_rings/_/linked_ids + * key_rings/_/linked_ids/_ + * key_rings/_/linked_ids/_/certs * key_rings/_/public * key_rings/_/secret * key_rings/_/certs @@ -143,6 +153,13 @@ public class KeychainProvider extends ContentProvider { + KeychainContract.PATH_USER_IDS, KEY_RING_USER_IDS); matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + + KeychainContract.PATH_LINKED_IDS, + KEY_RING_LINKED_IDS); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + + KeychainContract.PATH_LINKED_IDS + "/*/" + + KeychainContract.PATH_CERTS, + KEY_RING_LINKED_ID_CERTS); + matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" + KeychainContract.PATH_PUBLIC, KEY_RING_PUBLIC); matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/*/" @@ -179,6 +196,12 @@ public class KeychainProvider extends ContentProvider { matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/*/" + KeychainContract.PATH_ALLOWED_KEYS, API_ALLOWED_KEYS); + /** + * to access table containing last updated dates of keys + */ + matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS, UPDATED_KEYS); + matcher.addURI(authority, KeychainContract.BASE_UPDATED_KEYS + "/*", UPDATED_KEYS_SPECIFIC); + return matcher; } @@ -218,6 +241,11 @@ public class KeychainProvider extends ContentProvider { case KEY_RING_SECRET: return KeyRings.CONTENT_ITEM_TYPE; + case UPDATED_KEYS: + return UpdatedKeys.CONTENT_TYPE; + case UPDATED_KEYS_SPECIFIC: + return UpdatedKeys.CONTENT_ITEM_TYPE; + case API_APPS: return ApiApps.CONTENT_TYPE; @@ -477,7 +505,8 @@ public class KeychainProvider extends ContentProvider { } case KEY_RINGS_USER_IDS: - case KEY_RING_USER_IDS: { + case KEY_RING_USER_IDS: + case KEY_RING_LINKED_IDS: { HashMap<String, String> projectionMap = new HashMap<>(); projectionMap.put(UserPackets._ID, Tables.USER_PACKETS + ".oid AS _id"); projectionMap.put(UserPackets.MASTER_KEY_ID, Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID); @@ -502,13 +531,15 @@ public class KeychainProvider extends ContentProvider { groupBy = Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + ", " + Tables.USER_PACKETS + "." + UserPackets.RANK; - // for now, we only respect user ids here, so TYPE must be NULL - // TODO expand with KEY_RING_USER_PACKETS query type which lifts this restriction - qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL"); + if (match == KEY_RING_LINKED_IDS) { + qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " = " + + WrappedUserAttribute.UAT_URI_ATTRIBUTE); + } else { + qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL"); + } // If we are searching for a particular keyring's ids, add where - if (match == KEY_RING_USER_IDS) { - // TODO remove with the thing above + if (match == KEY_RING_USER_IDS || match == KEY_RING_LINKED_IDS) { qb.appendWhere(" AND "); qb.appendWhere(Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " = "); qb.appendWhereEscapeString(uri.getPathSegments().get(1)); @@ -559,7 +590,8 @@ public class KeychainProvider extends ContentProvider { } case KEY_RING_CERTS: - case KEY_RING_CERTS_SPECIFIC: { + case KEY_RING_CERTS_SPECIFIC: + case KEY_RING_LINKED_ID_CERTS: { HashMap<String, String> projectionMap = new HashMap<>(); projectionMap.put(Certs._ID, Tables.CERTS + ".oid AS " + Certs._ID); projectionMap.put(Certs.MASTER_KEY_ID, Tables.CERTS + "." + Certs.MASTER_KEY_ID); @@ -580,10 +612,6 @@ public class KeychainProvider extends ContentProvider { + " AND " + Tables.CERTS + "." + Certs.RANK + " = " + Tables.USER_PACKETS + "." + UserPackets.RANK - // for now, we only return user ids here, so TYPE must be NULL - // TODO at some point, we should lift this restriction - + " AND " - + Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL" + ") LEFT JOIN " + Tables.USER_PACKETS + " AS signer ON (" + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = " + "signer." + UserPackets.MASTER_KEY_ID @@ -603,6 +631,33 @@ public class KeychainProvider extends ContentProvider { qb.appendWhereEscapeString(uri.getPathSegments().get(4)); } + if (match == KEY_RING_LINKED_ID_CERTS) { + qb.appendWhere(" AND " + Tables.USER_PACKETS + "." + + UserPackets.TYPE + " IS NOT NULL"); + + qb.appendWhere(" AND " + Tables.USER_PACKETS + "." + + UserPackets.RANK + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(3)); + } else { + qb.appendWhere(" AND " + Tables.USER_PACKETS + "." + UserPackets.TYPE + " IS NULL"); + } + + break; + } + + case UPDATED_KEYS: + case UPDATED_KEYS_SPECIFIC: { + HashMap<String, String> projectionMap = new HashMap<>(); + qb.setTables(Tables.UPDATED_KEYS); + projectionMap.put(UpdatedKeys.MASTER_KEY_ID, Tables.UPDATED_KEYS + "." + + UpdatedKeys.MASTER_KEY_ID); + projectionMap.put(UpdatedKeys.LAST_UPDATED, Tables.UPDATED_KEYS + "." + + UpdatedKeys.LAST_UPDATED); + qb.setProjectionMap(projectionMap); + if (match == UPDATED_KEYS_SPECIFIC) { + qb.appendWhere(UpdatedKeys.MASTER_KEY_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(1)); + } break; } @@ -726,6 +781,12 @@ public class KeychainProvider extends ContentProvider { keyId = values.getAsLong(Certs.MASTER_KEY_ID); break; } + case UPDATED_KEYS: { + long updatedKeyId = db.replace(Tables.UPDATED_KEYS, null, values); + rowUri = UpdatedKeys.CONTENT_URI.buildUpon().appendPath("" + updatedKeyId) + .build(); + break; + } case API_APPS: { db.insertOrThrow(Tables.API_APPS, null, values); break; 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 0c37bfc2a..a6823d3ac 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -32,10 +32,15 @@ import android.support.v4.util.LongSparseArray; import org.spongycastle.bcpg.CompressionAlgorithmTags; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; +import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.operations.ImportOperation; import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; -import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult; @@ -50,7 +55,6 @@ import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; import org.sufficientlysecure.keychain.pgp.WrappedSignature; -import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiAllowedKeys; import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; @@ -59,14 +63,12 @@ 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; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.util.IterableIterator; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableFileCache; -import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize; -import org.sufficientlysecure.keychain.util.Preferences; import org.sufficientlysecure.keychain.util.ProgressFixedScaler; import org.sufficientlysecure.keychain.util.ProgressScaler; import org.sufficientlysecure.keychain.util.Utf8Util; @@ -82,6 +84,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * This class contains high level methods for database access. Despite its @@ -685,6 +688,36 @@ public class ProviderHelper { mIndent -= 1; } + // before deleting key, retrieve it's last updated time + final int INDEX_MASTER_KEY_ID = 0; + final int INDEX_LAST_UPDATED = 1; + Cursor lastUpdatedCursor = mContentResolver.query( + UpdatedKeys.CONTENT_URI, + new String[]{ + UpdatedKeys.MASTER_KEY_ID, + UpdatedKeys.LAST_UPDATED + }, + UpdatedKeys.MASTER_KEY_ID + " = ?", + new String[]{"" + masterKeyId}, + null + ); + if (lastUpdatedCursor.moveToNext()) { + // there was an entry to re-insert + // this operation must happen after the new key is inserted + ContentValues lastUpdatedEntry = new ContentValues(2); + lastUpdatedEntry.put(UpdatedKeys.MASTER_KEY_ID, + lastUpdatedCursor.getLong(INDEX_MASTER_KEY_ID)); + lastUpdatedEntry.put(UpdatedKeys.LAST_UPDATED, + lastUpdatedCursor.getLong(INDEX_LAST_UPDATED)); + operations.add( + ContentProviderOperation + .newInsert(UpdatedKeys.CONTENT_URI) + .withValues(lastUpdatedEntry) + .build() + ); + } + lastUpdatedCursor.close(); + try { // delete old version of this keyRing, which also deletes all keys and userIds on cascade int deleted = mContentResolver.delete( @@ -738,6 +771,11 @@ public class ProviderHelper { if (type != o.type) { return type == null ? -1 : 1; } + // if one is *trusted* but the other isn't, that one comes first + // this overrides the primary attribute, even! + if ( (trustedCerts.size() == 0) != (o.trustedCerts.size() == 0) ) { + return trustedCerts.size() > o.trustedCerts.size() ? -1 : 1; + } // if one key is primary but the other isn't, the primary one always comes first if (isPrimary != o.isPrimary) { return isPrimary ? -1 : 1; @@ -845,7 +883,7 @@ public class ProviderHelper { } public SaveKeyringResult savePublicKeyRing(UncachedKeyRing keyRing) { - return savePublicKeyRing(keyRing, new ProgressScaler()); + return savePublicKeyRing(keyRing, new ProgressScaler(), null); } /** @@ -854,7 +892,7 @@ public class ProviderHelper { * This is a high level method, which takes care of merging all new information into the old and * keep public and secret keyrings in sync. */ - public SaveKeyringResult savePublicKeyRing(UncachedKeyRing publicRing, Progressable progress) { + public SaveKeyringResult savePublicKeyRing(UncachedKeyRing publicRing, Progressable progress, String expectedFingerprint) { try { long masterKeyId = publicRing.getMasterKeyId(); @@ -927,6 +965,17 @@ public class ProviderHelper { canSecretRing = null; } + + // If we have an expected fingerprint, make sure it matches + if (expectedFingerprint != null) { + if (!canPublicRing.containsSubkey(expectedFingerprint)) { + log(LogType.MSG_IP_FINGERPRINT_ERROR); + return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog, null); + } else { + log(LogType.MSG_IP_FINGERPRINT_OK); + } + } + int result = saveCanonicalizedPublicKeyRing(canPublicRing, progress, canSecretRing != null); // Save the saved keyring (if any) @@ -1239,6 +1288,28 @@ public class ProviderHelper { } // 2. wipe database (IT'S DANGEROUS) + + // first, backup our list of updated key times + ArrayList<ContentValues> updatedKeysValues = new ArrayList<>(); + final int INDEX_MASTER_KEY_ID = 0; + final int INDEX_LAST_UPDATED = 1; + Cursor lastUpdatedCursor = mContentResolver.query( + UpdatedKeys.CONTENT_URI, + new String[]{ + UpdatedKeys.MASTER_KEY_ID, + UpdatedKeys.LAST_UPDATED + }, + null, null, null); + while (lastUpdatedCursor.moveToNext()) { + ContentValues values = new ContentValues(); + values.put(UpdatedKeys.MASTER_KEY_ID, + lastUpdatedCursor.getLong(INDEX_MASTER_KEY_ID)); + values.put(UpdatedKeys.LAST_UPDATED, + lastUpdatedCursor.getLong(INDEX_LAST_UPDATED)); + updatedKeysValues.add(values); + } + lastUpdatedCursor.close(); + log.add(LogType.MSG_CON_DB_CLEAR, indent); mContentResolver.delete(KeyRings.buildUnifiedKeyRingsUri(), null, null); @@ -1288,6 +1359,10 @@ public class ProviderHelper { new ProgressFixedScaler(progress, 25, 99, 100, R.string.progress_con_reimport)) .serialKeyRingImport(itPublics, numPublics, null, null); log.add(result, indent); + // re-insert our backed up list of updated key times + // TODO: can this cause issues in case a public key re-import failed? + mContentResolver.bulkInsert(UpdatedKeys.CONTENT_URI, + updatedKeysValues.toArray(new ContentValues[updatedKeysValues.size()])); } else { log.add(LogType.MSG_CON_REIMPORT_PUBLIC_SKIP, indent); } @@ -1397,6 +1472,14 @@ public class ProviderHelper { return getKeyRingAsArmoredString(data); } + public Uri renewKeyLastUpdatedTime(long masterKeyId, long time, TimeUnit timeUnit) { + ContentValues values = new ContentValues(); + values.put(UpdatedKeys.MASTER_KEY_ID, masterKeyId); + values.put(UpdatedKeys.LAST_UPDATED, timeUnit.toSeconds(time)); + + return mContentResolver.insert(UpdatedKeys.CONTENT_URI, values); + } + public ArrayList<String> getRegisteredApiApps() { Cursor cursor = mContentResolver.query(ApiApps.CONTENT_URI, null, null, null, null); 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 ea016c657..57dd068ef 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.Nullable; import android.text.TextUtils; import org.openintents.openpgp.IOpenPgpService; @@ -38,7 +39,7 @@ 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.PgpDecryptVerify; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptInputParcel; import org.sufficientlysecure.keychain.pgp.PgpSignEncryptOperation; @@ -65,8 +66,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashSet; +import java.util.List; public class OpenPgpService extends RemoteService { @@ -533,7 +536,7 @@ public class OpenPgpService extends RemoteService { byte[] detachedSignature = data.getByteArrayExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE); - PgpDecryptVerify op = new PgpDecryptVerify(this, mProviderHelper, null); + PgpDecryptVerifyOperation op = new PgpDecryptVerifyOperation(this, mProviderHelper, null); long inputLength = inputStream.available(); InputData inputData = new InputData(inputStream, inputLength); @@ -715,28 +718,40 @@ public class OpenPgpService extends RemoteService { } private Intent getSignKeyIdImpl(Intent data) { - String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID); + // if data already contains EXTRA_SIGN_KEY_ID, it has been executed again + // after user interaction. Then, we just need to return the long again! + if (data.hasExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID)) { + long signKeyId = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, + Constants.key.none); - Intent intent = new Intent(getBaseContext(), SelectSignKeyIdActivity.class); - String currentPkg = getCurrentCallingPackage(); - intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(currentPkg)); - intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId); - intent.putExtra(SelectSignKeyIdActivity.EXTRA_DATA, data); + Intent result = new Intent(); + result.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, signKeyId); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + return result; + } else { + String preferredUserId = data.getStringExtra(OpenPgpApi.EXTRA_USER_ID); - PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, - intent, - PendingIntent.FLAG_CANCEL_CURRENT); + Intent intent = new Intent(getBaseContext(), SelectSignKeyIdActivity.class); + String currentPkg = getCurrentCallingPackage(); + intent.setData(KeychainContract.ApiApps.buildByPackageNameUri(currentPkg)); + intent.putExtra(SelectSignKeyIdActivity.EXTRA_USER_ID, preferredUserId); + intent.putExtra(SelectSignKeyIdActivity.EXTRA_DATA, data); - // return PendingIntent to be executed by client - Intent result = new Intent(); - result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); - result.putExtra(OpenPgpApi.RESULT_INTENT, pi); + PendingIntent pi = PendingIntent.getActivity(getBaseContext(), 0, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + // return PendingIntent to be executed by client + Intent result = new Intent(); + result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); + result.putExtra(OpenPgpApi.RESULT_INTENT, pi); - return result; + return result; + } } private Intent getKeyIdsImpl(Intent data) { - // if data already contains key ids extra GET_KEY_IDS has been executed again + // if data already contains EXTRA_KEY_IDS, it has been executed again // after user interaction. Then, we just need to return the array again! if (data.hasExtra(OpenPgpApi.EXTRA_KEY_IDS)) { long[] keyIdsArray = data.getLongArrayExtra(OpenPgpApi.EXTRA_KEY_IDS); @@ -800,19 +815,14 @@ public class OpenPgpService extends RemoteService { } // version code is required and needs to correspond to version code of service! - // History of versions in org.openintents.openpgp.util.OpenPgpApi - // we support 3, 4, 5, 6 - if (data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 3 - && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 4 - && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 5 - && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 6 - && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 7 - && data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) != 8) { + // History of versions in openpgp-api's CHANGELOG.md + List<Integer> supportedVersions = Arrays.asList(3, 4, 5, 6, 7, 8, 9); + if (!supportedVersions.contains(data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1))) { Intent result = new Intent(); OpenPgpError error = new OpenPgpError (OpenPgpError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n" + "used API version: " + data.getIntExtra(OpenPgpApi.EXTRA_API_VERSION, -1) + "\n" - + "supported API versions: 3-8"); + + "supported API versions: " + supportedVersions.toString()); result.putExtra(OpenPgpApi.RESULT_ERROR, error); result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR); return result; @@ -830,67 +840,8 @@ public class OpenPgpService extends RemoteService { private final IOpenPgpService.Stub mBinder = new IOpenPgpService.Stub() { @Override public Intent execute(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) { - try { - Intent errorResult = checkRequirements(data); - if (errorResult != null) { - return errorResult; - } - - 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; - } - } - } finally { - // always close input and output file descriptors even in error cases - if (input != null) { - try { - input.close(); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException when closing input ParcelFileDescriptor", e); - } - } - if (output != null) { - try { - output.close(); - } catch (IOException e) { - Log.e(Constants.TAG, "IOException when closing output ParcelFileDescriptor", e); - } - } - } + Log.w(Constants.TAG, "You are using a deprecated service which may lead to truncated data on return, please use IOpenPgpService2!"); + return executeInternal(data, input, output); } }; @@ -900,4 +851,68 @@ public class OpenPgpService extends RemoteService { return mBinder; } + + protected Intent executeInternal(Intent data, ParcelFileDescriptor input, ParcelFileDescriptor output) { + try { + Intent errorResult = checkRequirements(data); + if (errorResult != null) { + return errorResult; + } + + 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; + } + } + } finally { + // always close input and output file descriptors even in error cases + if (input != null) { + try { + input.close(); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException when closing input ParcelFileDescriptor", e); + } + } + if (output != null) { + try { + output.close(); + } catch (IOException e) { + Log.e(Constants.TAG, "IOException when closing output ParcelFileDescriptor", e); + } + } + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService2.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService2.java new file mode 100644 index 000000000..110302e55 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService2.java @@ -0,0 +1,72 @@ +/* + * 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.remote; + +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; + +import org.openintents.openpgp.IOpenPgpService2; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class OpenPgpService2 extends OpenPgpService { + + private Map<Long, ParcelFileDescriptor> mOutputPipeMap = new HashMap<Long, ParcelFileDescriptor>(); + + private long createKey(int id) { + int callingPid = Binder.getCallingPid(); + return ((long) callingPid << 32) | ((long) id & 0xFFFFFFFL); + } + + private final IOpenPgpService2.Stub mBinder = new IOpenPgpService2.Stub() { + + @Override + public ParcelFileDescriptor createOutputPipe(int outputPipeId) { + try { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + mOutputPipeMap.put(createKey(outputPipeId), pipe[1]); + return pipe[0]; + } catch (IOException e) { + Log.e(Constants.TAG, "IOException in OpenPgpService2", e); + return null; + } + + } + + @Override + public Intent execute(Intent data, ParcelFileDescriptor input, int outputPipeId) { + long key = createKey(outputPipeId); + ParcelFileDescriptor output = mOutputPipeMap.get(key); + mOutputPipeMap.remove(key); + return executeInternal(data, input, output); + } + + }; + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java index 2deb33a67..a1451fb09 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/AppsListFragment.java @@ -243,7 +243,7 @@ public class AppsListFragment extends ListFragment implements null, isInstalled(packageName), 1, // registered! - R.drawable.ic_launcher // icon is retrieved later + R.mipmap.ic_launcher // icon is retrieved later }); break; } @@ -265,7 +265,7 @@ public class AppsListFragment extends ListFragment implements name, isInstalled(packageName), 1, // registered! - R.drawable.ic_launcher // icon is retrieved later + R.mipmap.ic_launcher // icon is retrieved later }); break; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java index cb9f46f7f..bed49a6f6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/ui/SelectSignKeyIdActivity.java @@ -40,14 +40,9 @@ public class SelectSignKeyIdActivity extends BaseActivity { private static final int REQUEST_CODE_CREATE_KEY = 0x00008884; - private Uri mAppUri; private String mPreferredUserId; private Intent mData; - private SelectSignKeyIdListFragment mListFragment; - private TextView mActionCreateKey; - private TextView mNone; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -62,15 +57,15 @@ public class SelectSignKeyIdActivity extends BaseActivity { } }); - mActionCreateKey = (TextView) findViewById(R.id.api_select_sign_key_create_key); - mActionCreateKey.setOnClickListener(new View.OnClickListener() { + TextView createKeyButton = (TextView) findViewById(R.id.api_select_sign_key_create_key); + createKeyButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createKey(mPreferredUserId); } }); - mNone = (TextView) findViewById(R.id.api_select_sign_key_none); - mNone.setOnClickListener(new View.OnClickListener() { + TextView noneButton = (TextView) findViewById(R.id.api_select_sign_key_none); + noneButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 0 is "none" @@ -82,16 +77,16 @@ public class SelectSignKeyIdActivity extends BaseActivity { }); Intent intent = getIntent(); - mAppUri = intent.getData(); + Uri appUri = intent.getData(); mPreferredUserId = intent.getStringExtra(EXTRA_USER_ID); mData = intent.getParcelableExtra(EXTRA_DATA); - if (mAppUri == null) { + if (appUri == null) { Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!"); finish(); return; } else { - Log.d(Constants.TAG, "uri: " + mAppUri); - startListFragments(savedInstanceState, mAppUri, mData); + Log.d(Constants.TAG, "uri: " + appUri); + startListFragments(savedInstanceState, appUri, mData); } } @@ -113,11 +108,11 @@ public class SelectSignKeyIdActivity extends BaseActivity { } // Create an instance of the fragments - mListFragment = SelectSignKeyIdListFragment.newInstance(dataUri, data); + SelectSignKeyIdListFragment listFragment = SelectSignKeyIdListFragment.newInstance(dataUri, data); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! getSupportFragmentManager().beginTransaction() - .replace(R.id.api_select_sign_key_list_fragment, mListFragment) + .replace(R.id.api_select_sign_key_list_fragment, listFragment) .commitAllowingStateLoss(); // do it immediately! getSupportFragmentManager().executePendingTransactions(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java index e76dcfb49..3cdbca633 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/CertifyActionsParcel.java @@ -23,6 +23,9 @@ import android.os.Parcelable; import java.io.Serializable; import java.util.ArrayList; +import java.util.List; +import java.util.Date; +import java.util.Map; import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; import org.sufficientlysecure.keychain.util.ParcelableProxy; @@ -86,17 +89,10 @@ public class CertifyActionsParcel implements Parcelable { final public ArrayList<String> mUserIds; final public ArrayList<WrappedUserAttribute> mUserAttributes; - public CertifyAction(long masterKeyId, ArrayList<String> userIds) { + public CertifyAction(long masterKeyId, List<String> userIds, List<WrappedUserAttribute> attributes) { mMasterKeyId = masterKeyId; - mUserIds = userIds; - mUserAttributes = null; - } - - public CertifyAction(long masterKeyId, ArrayList<String> userIds, - ArrayList<WrappedUserAttribute> attributes) { - mMasterKeyId = masterKeyId; - mUserIds = userIds; - mUserAttributes = attributes; + mUserIds = userIds == null ? null : new ArrayList<>(userIds); + mUserAttributes = attributes == null ? null : new ArrayList<>(attributes); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java index 7688b9252..b36d23775 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/ContactSyncAdapterService.java @@ -45,7 +45,7 @@ public class ContactSyncAdapterService extends Service { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, final SyncResult syncResult) { - Log.d(Constants.TAG, "Performing a sync!"); + Log.d(Constants.TAG, "Performing a contact sync!"); // TODO: Import is currently disabled for 2.8, until we implement proper origin management // importDone.set(false); // KeychainApplication.setupAccountAsNeeded(ContactSyncAdapterService.this); 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 ce4381140..d2128cd77 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainService.java @@ -41,7 +41,7 @@ import org.sufficientlysecure.keychain.operations.PromoteKeyOperation; import org.sufficientlysecure.keychain.operations.RevokeOperation; import org.sufficientlysecure.keychain.operations.SignEncryptOperation; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify; +import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation; import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel; import org.sufficientlysecure.keychain.pgp.Progressable; import org.sufficientlysecure.keychain.pgp.SignEncryptParcel; @@ -112,7 +112,7 @@ public class KeychainService extends Service implements Progressable { op = new SignEncryptOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); } else if (inputParcel instanceof PgpDecryptVerifyInputParcel) { - op = new PgpDecryptVerify(outerThis, new ProviderHelper(outerThis), outerThis); + op = new PgpDecryptVerifyOperation(outerThis, new ProviderHelper(outerThis), outerThis); } else if (inputParcel instanceof SaveKeyringParcel) { op = new EditKeyOperation(outerThis, new ProviderHelper(outerThis), outerThis, mActionCanceled); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java new file mode 100644 index 000000000..8aebae7aa --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeyserverSyncAdapterService.java @@ -0,0 +1,516 @@ +package org.sufficientlysecure.keychain.service; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SyncResult; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.PowerManager; +import android.os.SystemClock; +import android.support.v4.app.NotificationCompat; +import android.widget.Toast; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; +import org.sufficientlysecure.keychain.operations.ImportOperation; +import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.OrbotRequiredDialogActivity; +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.orbot.OrbotHelper; + +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class KeyserverSyncAdapterService extends Service { + + // how often a sync should be initiated, in s + public static final long SYNC_INTERVAL = + Constants.DEBUG_KEYSERVER_SYNC + ? TimeUnit.MINUTES.toSeconds(2) : TimeUnit.DAYS.toSeconds(3); + // 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 + 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); + + + private static final String ACTION_IGNORE_TOR = "ignore_tor"; + private static final String ACTION_UPDATE_ALL = "update_all"; + private static final String ACTION_SYNC_NOW = "sync_now"; + private static final String ACTION_DISMISS_NOTIFICATION = "cancel_sync"; + private static final String ACTION_START_ORBOT = "start_orbot"; + private static final String ACTION_CANCEL = "cancel"; + + private AtomicBoolean mCancelled = new AtomicBoolean(false); + + @Override + public int onStartCommand(final Intent intent, int flags, final int startId) { + switch (intent.getAction()) { + case ACTION_CANCEL: { + mCancelled.set(true); + break; + } + // 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 + // UPDATE_ALL, but a postponed sync is only started if screen is off + case ACTION_SYNC_NOW: { + // this checks for screen on/off before sync, and postpones the sync if on + ContentResolver.requestSync( + new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE), + Constants.PROVIDER_AUTHORITY, + new Bundle() + ); + break; + } + case ACTION_UPDATE_ALL: { + // does not check for screen on/off + asyncKeyUpdate(this, new CryptoInputParcel()); + break; + } + 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; + } + case ACTION_START_ORBOT: { + 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())); + break; + } + case OrbotRequiredDialogActivity.MESSAGE_DIALOG_CANCEL: { + // just stop service + stopSelf(); + break; + } + } + } + } + ); + startOrbot.putExtra(OrbotRequiredDialogActivity.EXTRA_MESSENGER, messenger); + startActivity(startOrbot); + break; + } + 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; + } + + private class KeyserverSyncAdapter extends AbstractThreadedSyncAdapter { + + public KeyserverSyncAdapter() { + super(KeyserverSyncAdapterService.this, true); + } + + @Override + public void onPerformSync(Account account, Bundle extras, String authority, + ContentProviderClient provider, SyncResult syncResult) { + Log.d(Constants.TAG, "Performing a keyserver sync!"); + + PowerManager pm = (PowerManager) KeyserverSyncAdapterService.this + .getSystemService(Context.POWER_SERVICE); + @SuppressWarnings("deprecation") // our min is API 15, deprecated only in 20 + boolean isScreenOn = pm.isScreenOn(); + + if (!isScreenOn) { + Intent serviceIntent = new Intent(KeyserverSyncAdapterService.this, + KeyserverSyncAdapterService.class); + serviceIntent.setAction(ACTION_UPDATE_ALL); + startService(serviceIntent); + } else { + postponeSync(); + } + } + + @Override + public void onSyncCanceled() { + super.onSyncCanceled(); + cancelUpdates(KeyserverSyncAdapterService.this); + } + } + + @Override + public IBinder onBind(Intent intent) { + return new KeyserverSyncAdapter().getSyncAdapterBinder(); + } + + private void handleUpdateResult(ImportKeyResult result) { + if (result.isPending()) { + // 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()); + } + + @Override + protected void onSilentStartDisabled() { + // show notification + NotificationManager manager = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + manager.notify(Constants.Notification.KEYSERVER_SYNC_FAIL_ORBOT, + getOrbotNoification(KeyserverSyncAdapterService.this)); + } + }.startOrbotAndListen(this, false); + } else if (isUpdateCancelled()) { + Log.d(Constants.TAG, "Keyserver sync cancelled, postponing by" + SYNC_POSTPONE_TIME + + "ms"); + postponeSync(); + } else { + Log.d(Constants.TAG, "Keyserver sync completed: Updated: " + result.mUpdatedKeys + + " Failed: " + result.mBadKeys); + stopSelf(); + } + } + + private void postponeSync() { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent serviceIntent = new Intent(this, KeyserverSyncAdapterService.class); + serviceIntent.setAction(ACTION_SYNC_NOW); + PendingIntent pi = PendingIntent.getService(this, 0, serviceIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.set( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + SYNC_POSTPONE_TIME, + pi + ); + } + + private void asyncKeyUpdate(final Context context, + final CryptoInputParcel cryptoInputParcel) { + new Thread(new Runnable() { + @Override + public void run() { + ImportKeyResult result = updateKeysFromKeyserver(context, cryptoInputParcel); + handleUpdateResult(result); + } + }).start(); + } + + private synchronized ImportKeyResult updateKeysFromKeyserver(final Context context, + final CryptoInputParcel cryptoInputParcel) { + mCancelled.set(false); + + ArrayList<ParcelableKeyRing> keyList = getKeysToUpdate(context); + + if (isUpdateCancelled()) { // if we've already been cancelled + return new ImportKeyResult(OperationResult.RESULT_CANCELLED, + new OperationResult.OperationLog()); + } + + if (cryptoInputParcel.getParcelableProxy() == null) { + // no explicit proxy, retrieve from preferences. Check if we should do a staggered sync + if (Preferences.getPreferences(context).getProxyPrefs().torEnabled) { + return staggeredUpdate(context, keyList, cryptoInputParcel); + } else { + return directUpdate(context, keyList, cryptoInputParcel); + } + } else { + return directUpdate(context, keyList, cryptoInputParcel); + } + } + + private ImportKeyResult directUpdate(Context context, ArrayList<ParcelableKeyRing> keyList, + CryptoInputParcel cryptoInputParcel) { + Log.d(Constants.TAG, "Starting normal update"); + ImportOperation importOp = new ImportOperation(context, new ProviderHelper(context), null); + return importOp.execute( + new ImportKeyringParcel(keyList, + Preferences.getPreferences(context).getPreferredKeyserver()), + cryptoInputParcel + ); + } + + + /** + * 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: + * https://github.com/open-keychain/open-keychain/issues/1337 + * + * @return result of the sync + */ + private ImportKeyResult staggeredUpdate(Context context, ArrayList<ParcelableKeyRing> keyList, + CryptoInputParcel cryptoInputParcel) { + Log.d(Constants.TAG, "Starting staggered update"); + // final int WEEK_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(7); + final int WEEK_IN_SECONDS = 0; + ImportOperation.KeyImportAccumulator accumulator + = new ImportOperation.KeyImportAccumulator(keyList.size(), null); + for (ParcelableKeyRing keyRing : keyList) { + int waitTime; + int staggeredTime = new Random().nextInt(1 + 2 * (WEEK_IN_SECONDS / keyList.size())); + if (staggeredTime >= ORBOT_CIRCUIT_TIMEOUT) { + waitTime = staggeredTime; + } else { + waitTime = ORBOT_CIRCUIT_TIMEOUT + new Random().nextInt(ORBOT_CIRCUIT_TIMEOUT); + } + Log.d(Constants.TAG, "Updating key with fingerprint " + keyRing.mExpectedFingerprint + + " with a wait time of " + waitTime + "s"); + try { + Thread.sleep(waitTime * 1000); + } catch (InterruptedException e) { + Log.e(Constants.TAG, "Exception during sleep between key updates", e); + // skip this one + continue; + } + ArrayList<ParcelableKeyRing> keyWrapper = new ArrayList<>(); + keyWrapper.add(keyRing); + if (isUpdateCancelled()) { + return new ImportKeyResult(ImportKeyResult.RESULT_CANCELLED, + new OperationResult.OperationLog()); + } + ImportKeyResult result = + new ImportOperation(context, new ProviderHelper(context), null, mCancelled) + .execute( + new ImportKeyringParcel( + keyWrapper, + Preferences.getPreferences(context) + .getPreferredKeyserver() + ), + cryptoInputParcel + ); + if (result.isPending()) { + return result; + } + accumulator.accumulateKeyImport(result); + } + return accumulator.getConsolidatedResult(); + } + + /** + * 1. Get keys which have been updated recently and therefore do not need to + * be updated now + * 2. Get list of all keys and filter out ones that don't need to be updated + * 3. Return keys to be updated + * + * @return list of keys that require update + */ + private ArrayList<ParcelableKeyRing> getKeysToUpdate(Context context) { + + // 1. Get keys which have been updated recently and don't need to updated now + final int INDEX_UPDATED_KEYS_MASTER_KEY_ID = 0; + final int INDEX_LAST_UPDATED = 1; + + // all time in seconds not milliseconds + final long CURRENT_TIME = GregorianCalendar.getInstance().getTimeInMillis() / 1000; + Cursor updatedKeysCursor = context.getContentResolver().query( + KeychainContract.UpdatedKeys.CONTENT_URI, + new String[]{ + KeychainContract.UpdatedKeys.MASTER_KEY_ID, + KeychainContract.UpdatedKeys.LAST_UPDATED + }, + "? - " + KeychainContract.UpdatedKeys.LAST_UPDATED + " < " + KEY_UPDATE_LIMIT, + new String[]{"" + CURRENT_TIME}, + null + ); + + ArrayList<Long> ignoreMasterKeyIds = new ArrayList<>(); + while (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(); + + // 2. Make a list of public keys which should be updated + final int INDEX_MASTER_KEY_ID = 0; + final int INDEX_FINGERPRINT = 1; + Cursor keyCursor = context.getContentResolver().query( + KeychainContract.KeyRings.buildUnifiedKeyRingsUri(), + new String[]{ + KeychainContract.KeyRings.MASTER_KEY_ID, + KeychainContract.KeyRings.FINGERPRINT + }, + null, + null, + null + ); + + if (keyCursor == null) { + return new ArrayList<>(); + } + + ArrayList<ParcelableKeyRing> keyList = new ArrayList<>(); + while (keyCursor.moveToNext()) { + long keyId = keyCursor.getLong(INDEX_MASTER_KEY_ID); + if (ignoreMasterKeyIds.contains(keyId)) { + continue; + } + Log.d(Constants.TAG, "Keyserver sync: Updating {" + keyId + "}"); + String fingerprint = KeyFormattingUtils + .convertFingerprintToHex(keyCursor.getBlob(INDEX_FINGERPRINT)); + String hexKeyId = KeyFormattingUtils + .convertKeyIdToHex(keyId); + // we aren't updating from keybase as of now + keyList.add(new ParcelableKeyRing(fingerprint, hexKeyId, null)); + } + keyCursor.close(); + + return keyList; + } + + private boolean isUpdateCancelled() { + return mCancelled.get(); + } + + /** + * 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 + * the default application process where the UI code runs. + * + * @param context used to send an Intent to the service requesting cancellation. + */ + public static void cancelUpdates(Context context) { + Intent intent = new Intent(context, KeyserverSyncAdapterService.class); + intent.setAction(ACTION_CANCEL); + context.startService(intent); + } + + private Notification getOrbotNoification(Context context) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(context); + builder.setSmallIcon(R.drawable.ic_stat_notify_24dp) + .setLargeIcon(getBitmap(R.mipmap.ic_launcher, context)) + .setContentTitle(context.getString(R.string.keyserver_sync_orbot_notif_title)) + .setContentText(context.getString(R.string.keyserver_sync_orbot_notif_msg)) + .setAutoCancel(true); + + // In case the user decides to not use tor + Intent ignoreTorIntent = new Intent(context, KeyserverSyncAdapterService.class); + ignoreTorIntent.setAction(ACTION_IGNORE_TOR); + PendingIntent ignoreTorPi = PendingIntent.getService( + context, + 0, // security not issue since we're giving this pending intent to Notification Manager + ignoreTorIntent, + PendingIntent.FLAG_CANCEL_CURRENT + ); + + builder.addAction(R.drawable.ic_stat_tor_off, + context.getString(R.string.keyserver_sync_orbot_notif_ignore), + ignoreTorPi); + + Intent startOrbotIntent = new Intent(context, KeyserverSyncAdapterService.class); + startOrbotIntent.setAction(ACTION_START_ORBOT); + PendingIntent startOrbotPi = PendingIntent.getService( + context, + 0, // security not issue since we're giving this pending intent to Notification Manager + startOrbotIntent, + PendingIntent.FLAG_CANCEL_CURRENT + ); + + builder.addAction(R.drawable.ic_stat_tor, + context.getString(R.string.keyserver_sync_orbot_notif_start), + startOrbotPi + ); + builder.setContentIntent(startOrbotPi); + + return builder.build(); + } + + public static void enableKeyserverSync(Context context) { + try { + AccountManager manager = AccountManager.get(context); + Account[] accounts = manager.getAccountsByType(Constants.ACCOUNT_TYPE); + + Account account = new Account(Constants.ACCOUNT_NAME, Constants.ACCOUNT_TYPE); + if (accounts.length == 0) { + if (!manager.addAccountExplicitly(account, null, null)) { + Log.e(Constants.TAG, "Adding account failed!"); + } + } + // for keyserver sync + ContentResolver.setIsSyncable(account, Constants.PROVIDER_AUTHORITY, 1); + ContentResolver.setSyncAutomatically(account, Constants.PROVIDER_AUTHORITY, + true); + ContentResolver.addPeriodicSync( + account, + Constants.PROVIDER_AUTHORITY, + new Bundle(), + SYNC_INTERVAL + ); + } catch (SecurityException e) { + Log.e(Constants.TAG, "SecurityException when adding the account", e); + Toast.makeText(context, R.string.reinstall_openkeychain, Toast.LENGTH_LONG).show(); + } + } + + // 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( + android.R.dimen.notification_large_icon_width); + int mLargeIconHeight = (int) context.getResources().getDimension( + android.R.dimen.notification_large_icon_height); + Drawable d; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // noinspection deprecation (can't help it at this api level) + d = context.getResources().getDrawable(resId); + } else { + d = context.getDrawable(resId); + } + if (d == null) { + return null; + } + Bitmap b = Bitmap.createBitmap(mLargeIconWidth, mLargeIconHeight, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + d.setBounds(0, 0, mLargeIconWidth, mLargeIconHeight); + d.draw(c); + return b; + } +} 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 2a258b7e3..5d04317b3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java @@ -101,8 +101,6 @@ public class PassphraseCacheService extends Service { private static final long DEFAULT_TTL = 15; - private static final int NOTIFICATION_ID = 1; - private static final int MSG_PASSPHRASE_CACHE_GET_OKAY = 1; private static final int MSG_PASSPHRASE_CACHE_GET_KEY_NOT_FOUND = 2; @@ -477,7 +475,7 @@ public class PassphraseCacheService extends Service { private void updateService() { if (mPassphraseCache.size() > 0) { - startForeground(NOTIFICATION_ID, getNotification()); + startForeground(Constants.Notification.PASSPHRASE_CACHE, getNotification()); } else { // stop whole service if no cached passphrases remaining Log.d(Constants.TAG, "PassphraseCacheService: No passphrases remaining in memory, stopping service!"); @@ -511,7 +509,7 @@ public class PassphraseCacheService extends Service { private Notification getNotification() { NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setSmallIcon(R.drawable.ic_stat_notify_24dp) - .setLargeIcon(getBitmap(R.drawable.ic_launcher, getBaseContext())) + .setLargeIcon(getBitmap(R.mipmap.ic_launcher, getBaseContext())) .setContentTitle(getResources().getQuantityString(R.plurals.passp_cache_notif_n_keys, mPassphraseCache.size(), mPassphraseCache.size())) .setContentText(getString(R.string.passp_cache_notif_click_to_clear)); @@ -603,4 +601,4 @@ public class PassphraseCacheService extends Service { this.passphrase = passphrase; } } -}
\ No newline at end of file +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java index 2d9ac6ee3..a3ea8ad9a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupFragment.java @@ -18,7 +18,11 @@ 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; @@ -47,7 +51,20 @@ public class BackupFragment extends Fragment { 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; + } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -80,8 +97,7 @@ public class BackupFragment extends Fragment { } if (!includeSecretKeys) { - ExportHelper exportHelper = new ExportHelper(activity); - exportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, false); + startBackup(false); return; } @@ -136,8 +152,7 @@ public class BackupFragment extends Fragment { return; } - ExportHelper exportHelper = new ExportHelper(activity); - exportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true); + startBackup(true); } }.execute(activity.getContentResolver()); @@ -167,8 +182,19 @@ public class BackupFragment extends Fragment { return; } - ExportHelper exportHelper = new ExportHelper(getActivity()); - exportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true); + startBackup(true); } } + + 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); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java index 016ab5f3c..c5528e40b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintActivity.java @@ -30,6 +30,8 @@ public class CertifyFingerprintActivity extends BaseActivity { protected Uri mDataUri; + public static final String EXTRA_ENABLE_WORD_CONFIRM = "enable_word_confirm"; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -40,6 +42,7 @@ public class CertifyFingerprintActivity extends BaseActivity { finish(); return; } + boolean enableWordConfirm = getIntent().getBooleanExtra(EXTRA_ENABLE_WORD_CONFIRM, false); setFullScreenDialogClose(new View.OnClickListener() { @Override @@ -50,7 +53,7 @@ public class CertifyFingerprintActivity extends BaseActivity { Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); - startFragment(savedInstanceState, mDataUri); + startFragment(savedInstanceState, mDataUri, enableWordConfirm); } @Override @@ -58,7 +61,7 @@ public class CertifyFingerprintActivity extends BaseActivity { setContentView(R.layout.certify_fingerprint_activity); } - private void startFragment(Bundle savedInstanceState, Uri dataUri) { + private void startFragment(Bundle savedInstanceState, Uri dataUri, boolean enableWordConfirm) { // 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. @@ -67,7 +70,7 @@ public class CertifyFingerprintActivity extends BaseActivity { } // Create an instance of the fragment - CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(dataUri); + CertifyFingerprintFragment frag = CertifyFingerprintFragment.newInstance(dataUri, enableWordConfirm); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! 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 a6b8a0e85..552fa34c0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyFingerprintFragment.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui; import android.content.Intent; import android.database.Cursor; +import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.LoaderManager; @@ -34,6 +35,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; 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; @@ -44,23 +46,24 @@ 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"; private TextView mFingerprint; + private TextView mIntro; private static final int LOADER_ID_UNIFIED = 0; private Uri mDataUri; - - private View mActionNo; - private View mActionYes; + private boolean mEnableWordConfirm; /** * Creates new instance of this fragment */ - public static CertifyFingerprintFragment newInstance(Uri dataUri) { + public static CertifyFingerprintFragment newInstance(Uri dataUri, boolean enableWordConfirm) { CertifyFingerprintFragment frag = new CertifyFingerprintFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_DATA_URI, dataUri); + args.putBoolean(ARG_ENABLE_WORD_CONFIRM, enableWordConfirm); frag.setArguments(args); @@ -72,18 +75,19 @@ public class CertifyFingerprintFragment extends LoaderFragment implements View root = super.onCreateView(inflater, superContainer, savedInstanceState); View view = inflater.inflate(R.layout.certify_fingerprint_fragment, getContainer()); - mActionNo = view.findViewById(R.id.certify_fingerprint_button_no); - mActionYes = view.findViewById(R.id.certify_fingerprint_button_yes); + View actionNo = view.findViewById(R.id.certify_fingerprint_button_no); + View actionYes = 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); - mActionNo.setOnClickListener(new View.OnClickListener() { + actionNo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { getActivity().finish(); } }); - mActionYes.setOnClickListener(new View.OnClickListener() { + actionYes.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { certify(mDataUri); @@ -103,6 +107,11 @@ public class CertifyFingerprintFragment extends LoaderFragment implements getActivity().finish(); return; } + mEnableWordConfirm = getArguments().getBoolean(ARG_ENABLE_WORD_CONFIRM); + + if (mEnableWordConfirm) { + mIntro.setText(R.string.certify_fingerprint_text_words); + } loadData(dataUri); } @@ -149,10 +158,13 @@ public class CertifyFingerprintFragment extends LoaderFragment implements switch (loader.getId()) { case LOADER_ID_UNIFIED: { if (data.moveToFirst()) { - byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); - String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); - mFingerprint.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint)); + + if (mEnableWordConfirm) { + displayWordConfirm(fingerprintBlob); + } else { + displayHexConfirm(fingerprintBlob); + } break; } @@ -162,6 +174,19 @@ public class CertifyFingerprintFragment extends LoaderFragment implements setContentShown(true); } + private void displayHexConfirm(byte[] fingerprintBlob) { + String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fingerprintBlob); + mFingerprint.setText(KeyFormattingUtils.colorizeFingerprint(fingerprint)); + } + + private void displayWordConfirm(byte[] fingerprintBlob) { + String fingerprint = ExperimentalWordConfirm.getWords(getActivity(), fingerprintBlob); + + mFingerprint.setTextSize(24); + mFingerprint.setTypeface(Typeface.DEFAULT, Typeface.BOLD); + mFingerprint.setText(fingerprint); + } + /** * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. * We need to make sure we are no longer using it. diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java index 5ca7c6bc7..357b445f0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyFragment.java @@ -83,7 +83,9 @@ public class CertifyKeyFragment }; private static final int INDEX_MASTER_KEY_ID = 1; private static final int INDEX_USER_ID = 2; + @SuppressWarnings("unused") private static final int INDEX_IS_PRIMARY = 3; + @SuppressWarnings("unused") private static final int INDEX_IS_REVOKED = 4; private MultiUserIdsAdapter mUserIdsAdapter; 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 e581e069b..881190ae2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptActivity.java @@ -29,13 +29,14 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; -import android.view.View; 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.ui.base.BaseActivity; @@ -93,7 +94,9 @@ public class DecryptActivity extends BaseActivity { } else if (intent.hasExtra(Intent.EXTRA_TEXT)) { String text = intent.getStringExtra(Intent.EXTRA_TEXT); Uri uri = readToTempFile(text); - uris.add(uri); + if (uri != null) { + uris.add(uri); + } } break; @@ -105,7 +108,9 @@ public class DecryptActivity extends BaseActivity { } else if (intent.hasExtra(Intent.EXTRA_TEXT)) { for (String text : intent.getStringArrayListExtra(Intent.EXTRA_TEXT)) { Uri uri = readToTempFile(text); - uris.add(uri); + if (uri != null) { + uris.add(uri); + } } } @@ -139,7 +144,9 @@ public class DecryptActivity extends BaseActivity { String text = clip.getItemAt(0).coerceToText(this).toString(); uri = readToTempFile(text); } - uris.add(uri); + if (uri != null) { + uris.add(uri); + } break; } @@ -170,9 +177,17 @@ public class DecryptActivity extends BaseActivity { } - public Uri readToTempFile(String text) throws IOException { + @Nullable public Uri readToTempFile(String text) throws IOException { Uri tempFile = TemporaryStorageProvider.createFile(this); OutputStream outStream = getContentResolver().openOutputStream(tempFile); + + // clean up ascii armored message, fixing newlines and stuff + String cleanedText = PgpHelper.getPgpContent(text); + if (cleanedText == null) { + return null; + } + + // if cleanup didn't work, just try the raw data outStream.write(text.getBytes()); outStream.close(); return tempFile; 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 640755ef3..3dda47ac5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java @@ -35,7 +35,6 @@ 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.Bundle; import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.LinearLayoutManager; @@ -222,18 +221,6 @@ public class DecryptListFragment } } - private void askForOutputFilename(Uri inputUri, String originalFilename, String mimeType) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - File file = new File(inputUri.getPath()); - File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR; - File targetFile = new File(parentDir, originalFilename); - FileHelper.saveFile(this, getString(R.string.title_decrypt_to_file), - getString(R.string.specify_file_to_decrypt_to), targetFile, REQUEST_CODE_OUTPUT); - } else { - FileHelper.saveDocument(this, mimeType, originalFilename, REQUEST_CODE_OUTPUT); - } - } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { @@ -388,7 +375,7 @@ public class DecryptListFragment onFileClick = new OnClickListener() { @Override public void onClick(View view) { - displayWithViewIntent(uri); + displayWithViewIntent(uri, false); } }; } @@ -413,7 +400,7 @@ public class DecryptListFragment } - public void displayWithViewIntent(final Uri uri) { + public void displayWithViewIntent(final Uri uri, boolean share) { Activity activity = getActivity(); if (activity == null || mCurrentInputUri != null) { return; @@ -432,104 +419,47 @@ public class DecryptListFragment // OpenKeychain's internal viewer if ("text/plain".equals(metadata.getMimeType())) { - parseMime(outputUri); - - // this is a significant i/o operation, use an asynctask -// new AsyncTask<Void,Void,Intent>() { -// -// @Override -// protected Intent doInBackground(Void... params) { -// -// Activity activity = getActivity(); -// if (activity == null) { -// return null; -// } -// -// Intent intent = new Intent(Intent.ACTION_VIEW); -// intent.setDataAndType(outputUri, "text/plain"); -// intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); -// return intent; -// } -// -// @Override -// protected void onPostExecute(Intent intent) { -// // for result so we can possibly get a snackbar error from internal viewer -// Activity activity = getActivity(); -// if (intent == null || activity == null) { -// return; -// } -// -// LabeledIntent internalIntent = new LabeledIntent( -// new Intent(intent) -// .setClass(activity, DisplayTextActivity.class) -// .putExtra(DisplayTextActivity.EXTRA_METADATA, result), -// BuildConfig.APPLICATION_ID, R.string.view_internal, R.drawable.ic_launcher); -// -// Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show)); -// chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, -// new Parcelable[] { internalIntent }); -// -// activity.startActivity(chooserIntent); -// } -// -// }.execute(); + if (share) { + try { + String plaintext = FileHelper.readTextFromUri(activity, outputUri, result.getCharset()); - } else { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(outputUri, metadata.getMimeType()); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(metadata.getMimeType()); + intent.putExtra(Intent.EXTRA_TEXT, plaintext); + startActivity(intent); - Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show)); - chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - activity.startActivity(chooserIntent); - } - - } - - private void parseMime(final Uri inputUri) { - - CryptoOperationHelper.Callback<MimeParsingParcel, MimeParsingResult> callback - = new CryptoOperationHelper.Callback<MimeParsingParcel, MimeParsingResult>() { - - @Override - public MimeParsingParcel createOperationInput() { - return new MimeParsingParcel(inputUri, null); - } + } catch (IOException e) { + Notify.create(activity, R.string.error_preparing_data, Style.ERROR).show(); + } - @Override - public void onCryptoOperationSuccess(MimeParsingResult result) { - handleResult(result); + return; } - @Override - public void onCryptoOperationCancelled() { + Intent intent = new Intent(activity, DisplayTextActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(outputUri, metadata.getMimeType()); + intent.putExtra(DisplayTextActivity.EXTRA_METADATA, result); + activity.startActivity(intent); - } + } else { - @Override - public void onCryptoOperationError(MimeParsingResult result) { - handleResult(result); + Intent intent; + if (share) { + intent = new Intent(Intent.ACTION_SEND); + intent.setType(metadata.getMimeType()); + intent.putExtra(Intent.EXTRA_STREAM, outputUri); + } else { + intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(outputUri, metadata.getMimeType()); } - public void handleResult(MimeParsingResult result) { - // TODO: merge with other log -// saveKeyResult.getLog().add(result, 0); - - mOutputUris = new HashMap<>(result.getTemporaryUris().size()); - for (Uri tempUri : result.getTemporaryUris()) { - // TODO: use same inputUri for all? - mOutputUris.put(inputUri, tempUri); - } - } + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - @Override - public boolean onCryptoSetProgress(String msg, int progress, int max) { - return false; - } - }; + Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show)); + chooserIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + activity.startActivity(chooserIntent); + } - CryptoOperationHelper mimeParsingHelper = new CryptoOperationHelper<>(3, this, callback, R.string.progress_uploading); - mimeParsingHelper.cryptoOperation(); } @Override @@ -576,13 +506,17 @@ public class DecryptListFragment intent.putExtra(LogDisplayFragment.EXTRA_RESULT, result); activity.startActivity(intent); return true; + case R.id.decrypt_share: + displayWithViewIntent(model.mInputUri, true); + return true; case R.id.decrypt_save: OpenPgpMetadata metadata = result.getDecryptionMetadata(); if (metadata == null) { return true; } mCurrentInputUri = model.mInputUri; - askForOutputFilename(model.mInputUri, metadata.getFilename(), metadata.getMimeType()); + FileHelper.saveDocument(this, metadata.getFilename(), model.mInputUri, metadata.getMimeType(), + R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, REQUEST_CODE_OUTPUT); return true; case R.id.decrypt_delete: deleteFile(activity, model.mInputUri); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java index b22053df1..478259133 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DeleteKeyDialogActivity.java @@ -48,7 +48,6 @@ import org.sufficientlysecure.keychain.service.RevokeKeyringParcel; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; -import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.util.Log; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java index 4769e68d8..07b0a12d3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java @@ -280,7 +280,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring case LOADER_ID_USER_IDS: { Uri baseUri = UserPackets.buildUserIdsUri(mDataUri); return new CursorLoader(getActivity(), baseUri, - UserIdsAdapter.USER_IDS_PROJECTION, null, null, null); + UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null); } case LOADER_ID_SUBKEYS: { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java index fc72a6c9f..84660ca7a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptDecryptOverviewFragment.java @@ -83,11 +83,7 @@ public class EncryptDecryptOverviewFragment extends Fragment { mDecryptFile.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - FileHelper.openDocument(EncryptDecryptOverviewFragment.this, "*/*", REQUEST_CODE_INPUT); - } else { - FileHelper.openFile(EncryptDecryptOverviewFragment.this, null, "*/*", REQUEST_CODE_INPUT); - } + FileHelper.openDocument(EncryptDecryptOverviewFragment.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 63d37f296..8572a5712 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptFilesFragment.java @@ -18,7 +18,6 @@ package org.sufficientlysecure.keychain.ui; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; @@ -196,13 +195,9 @@ public class EncryptFilesFragment } private void addInputUri() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - FileHelper.openDocument(EncryptFilesFragment.this, "*/*", true, REQUEST_CODE_INPUT); - } else { - FileHelper.openFile(EncryptFilesFragment.this, mFilesAdapter.getModelCount() == 0 ? - null : mFilesAdapter.getModelItem(mFilesAdapter.getModelCount() - 1).inputUri, - "*/*", REQUEST_CODE_INPUT); - } + FileHelper.openDocument(EncryptFilesFragment.this, mFilesAdapter.getModelCount() == 0 ? + null : mFilesAdapter.getModelItem(mFilesAdapter.getModelCount() - 1).inputUri, + "*/*", true, REQUEST_CODE_INPUT); } private void addInputUri(Uri inputUri) { @@ -230,19 +225,8 @@ public class EncryptFilesFragment (mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri)) + (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN); Uri inputUri = model.inputUri; - saveDocumentIntent(targetName, inputUri); - } - - private void saveDocumentIntent(String targetName, Uri inputUri) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - File file = new File(inputUri.getPath()); - File parentDir = file.exists() ? file.getParentFile() : Constants.Path.APP_DIR; - File targetFile = new File(parentDir, targetName); - FileHelper.saveFile(this, getString(R.string.title_encrypt_to_file), - getString(R.string.specify_file_to_encrypt_to), targetFile, REQUEST_CODE_OUTPUT); - } else { - FileHelper.saveDocument(this, "*/*", targetName, REQUEST_CODE_OUTPUT); - } + FileHelper.saveDocument(this, targetName, inputUri, + R.string.title_encrypt_to_file, R.string.specify_file_to_encrypt_to, REQUEST_CODE_OUTPUT); } public void addFile(Intent data) { 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 538fa16c7..746c75600 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java @@ -64,8 +64,8 @@ public class ImportKeysFileFragment extends Fragment { // open .asc or .gpg files // setting it to text/plain prevents Cyanogenmod's file manager from selecting asc // or gpg types! - FileHelper.openFile(ImportKeysFileFragment.this, Uri.fromFile(Constants.Path.APP_DIR), - "*/*", REQUEST_CODE_FILE); + FileHelper.openDocument(ImportKeysFileFragment.this, + Uri.fromFile(Constants.Path.APP_DIR), "*/*", false, REQUEST_CODE_FILE); } }); 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 8c46876be..ce6994ba4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -18,11 +18,6 @@ package org.sufficientlysecure.keychain.ui; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; - import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.app.Activity; @@ -70,16 +65,19 @@ import org.sufficientlysecure.keychain.service.ConsolidateInputParcel; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; 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.util.FabContainer; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; import se.emilsjolander.stickylistheaders.StickyListHeadersListView; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + /** * Public key list with sticky list headers. It does _not_ extend ListFragment because it uses * StickyListHeaders library which does not extend upon ListView. @@ -536,7 +534,7 @@ public class KeyListFragment extends LoaderFragment ); if (cursor == null) { - Notify.create(activity, R.string.error_loading_keys, Style.ERROR); + Notify.create(activity, R.string.error_loading_keys, Notify.Style.ERROR); return; } 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 1cd1a3099..43c8d2643 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/LogDisplayFragment.java @@ -18,11 +18,12 @@ package org.sufficientlysecure.keychain.ui; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Color; +import android.net.Uri; import android.os.Bundle; -import android.os.Parcel; import android.support.v4.app.ListFragment; import android.util.TypedValue; import android.view.LayoutInflater; @@ -37,19 +38,19 @@ import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; -import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; 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.ui.util.FormattingUtils; -import org.sufficientlysecure.keychain.util.FileHelper; -import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; + +import java.io.IOException; +import java.io.OutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.PrintWriter; public class LogDisplayFragment extends ListFragment implements OnItemClickListener { @@ -60,6 +61,8 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe public static final String EXTRA_RESULT = "log"; protected int mTextColor; + private Uri mLogTempFile; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -118,170 +121,40 @@ public class LogDisplayFragment extends ListFragment implements OnItemClickListe public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_log_display_export_log: - exportLog(); + shareLog(); break; } return super.onOptionsItemSelected(item); } - private void exportLog() { - showExportLogDialog(new File(Constants.Path.APP_DIR, "export.log")); - } - - private void writeToLogFile(final OperationResult.OperationLog operationLog, final File f) { - OperationResult.OperationLog currLog = new OperationResult.OperationLog(); - currLog.add(OperationResult.LogType.MSG_EXPORT_LOG, 0); - - boolean error = false; - - PrintWriter pw = null; - try { - pw = new PrintWriter(f); - pw.print(getPrintableOperationLog(operationLog, "")); - if (pw.checkError()) {//IOException - Log.e(Constants.TAG, "Log Export I/O Exception " + f.getAbsolutePath()); - currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_ERROR_WRITING, 1); - error = true; - } - } catch (FileNotFoundException e) { - Log.e(Constants.TAG, "File not found for exporting log " + f.getAbsolutePath()); - currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_ERROR_FOPEN, 1); - error = true; - } - if (pw != null) { - pw.close(); - if (!error && pw.checkError()) {//check if it is only pw.close() which generated error - currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_ERROR_WRITING, 1); - error = true; - } - } - - if (!error) { - currLog.add(OperationResult.LogType.MSG_EXPORT_LOG_EXPORT_SUCCESS, 1); - } - - int opResultCode = error ? OperationResult.RESULT_ERROR : OperationResult.RESULT_OK; - OperationResult opResult = new LogExportResult(opResultCode, currLog); - opResult.createNotify(getActivity()).show(); - } - - /** - * returns an indented String of an entire OperationLog - * - * @param opLog log to be converted to indented, printable format - * @param basePadding padding to add at the start of all log entries, made for use with SubLogs - * @return printable, indented version of passed operationLog - */ - private String getPrintableOperationLog(OperationResult.OperationLog opLog, String basePadding) { - String log = ""; - for (LogEntryParcel anOpLog : opLog) { - log += getPrintableLogEntry(anOpLog, basePadding) + "\n"; - } - log = log.substring(0, log.length() - 1);//gets rid of extra new line - return log; - } - - /** - * returns an indented String of a LogEntryParcel including any sub-logs it may contain - * - * @param entryParcel log entryParcel whose String representation is to be obtained - * @return indented version of passed log entryParcel in a readable format - */ - private String getPrintableLogEntry(OperationResult.LogEntryParcel entryParcel, - String basePadding) { - - final String indent = " ";//4 spaces = 1 Indent level - - String padding = basePadding; - for (int i = 0; i < entryParcel.mIndent; i++) { - padding += indent; - } - String logText = padding; - - switch (entryParcel.mType.mLevel) { - case DEBUG: - logText += "[DEBUG]"; - break; - case INFO: - logText += "[INFO]"; - break; - case WARN: - logText += "[WARN]"; - break; - case ERROR: - logText += "[ERROR]"; - break; - case START: - logText += "[START]"; - break; - case OK: - logText += "[OK]"; - break; - case CANCELLED: - logText += "[CANCELLED]"; - break; - } + private void shareLog() { - // special case: first parameter may be a quantity - if (entryParcel.mParameters != null && entryParcel.mParameters.length > 0 - && entryParcel.mParameters[0] instanceof Integer) { - logText += getResources().getQuantityString(entryParcel.mType.getMsgId(), - (Integer) entryParcel.mParameters[0], - entryParcel.mParameters); - } else { - logText += getResources().getString(entryParcel.mType.getMsgId(), - entryParcel.mParameters); + Activity activity = getActivity(); + if (activity == null) { + return; } - if (entryParcel instanceof SubLogEntryParcel) { - OperationResult subResult = ((SubLogEntryParcel) entryParcel).getSubResult(); - LogEntryParcel subEntry = subResult.getLog().getLast(); - if (subEntry != null) { - //the first line of log of subResult is same as entryParcel, so replace logText - logText = getPrintableOperationLog(subResult.getLog(), padding); + String log = mResult.getLog().getPrintableOperationLog(getResources(), 0); + + // if there is no log temp file yet, create one + if (mLogTempFile == null) { + mLogTempFile = TemporaryStorageProvider.createFile(getActivity(), "openkeychain_log.txt", "text/plain"); + try { + OutputStream outputStream = activity.getContentResolver().openOutputStream(mLogTempFile); + outputStream.write(log.getBytes()); + } catch (IOException e) { + Notify.create(activity, R.string.error_log_share_internal, Style.ERROR).show(); + return; } } - return logText; - } - - private void showExportLogDialog(final File exportFile) { - - String title = this.getString(R.string.title_export_log); - - String message = this.getString(R.string.specify_file_to_export_log_to); - - FileHelper.saveFile(new FileHelper.FileDialogCallback() { - @Override - public void onFileSelected(File file, boolean checked) { - writeToLogFile(mResult.getLog(), file); - } - }, this.getActivity().getSupportFragmentManager(), title, message, exportFile, null); - } - - private static class LogExportResult extends OperationResult { - - public static Creator<LogExportResult> CREATOR = new Creator<LogExportResult>() { - public LogExportResult createFromParcel(final Parcel source) { - return new LogExportResult(source); - } - - public LogExportResult[] newArray(final int size) { - return new LogExportResult[size]; - } - }; - - public LogExportResult(int result, OperationLog log) { - super(result, log); - } + 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); - /** - * trivial but necessary to implement the Parcelable protocol. - */ - public LogExportResult(Parcel source) { - super(source); - } } @Override 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 e6591595e..b811b218e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/NfcOperationActivity.java @@ -21,7 +21,6 @@ package org.sufficientlysecure.keychain.ui; import android.content.Intent; -import android.content.pm.ActivityInfo; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; @@ -50,15 +49,11 @@ import org.sufficientlysecure.keychain.util.Preferences; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.Date; /** * This class provides a communication interface to OpenPGP applications on ISO SmartCard compliant * NFC devices. - * <p/> * For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf - * NOTE: If no CryptoInputParcel is passed via EXTRA_CRYPTO_INPUT, the CryptoInputParcel is created - * internally and is NOT meant to be used by signing operations before adding signature time */ public class NfcOperationActivity extends BaseNfcActivity { @@ -84,8 +79,8 @@ public class NfcOperationActivity extends BaseNfcActivity { @Override protected void initTheme() { mThemeChanger = new ThemeChanger(this); - mThemeChanger.setThemes(R.style.Theme_Keychain_Light_Dialog_SecurityToken, - R.style.Theme_Keychain_Dark_Dialog_SecurityToken); + mThemeChanger.setThemes(R.style.Theme_Keychain_Light_Dialog, + R.style.Theme_Keychain_Dark_Dialog); mThemeChanger.changeTheme(); } @@ -101,13 +96,6 @@ public class NfcOperationActivity extends BaseNfcActivity { mInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT); - if (mInputParcel == null) { - // for compatibility when used from OpenPgpService - // (or any place other than CryptoOperationHelper) - // NOTE: This CryptoInputParcel cannot be used for signing without adding signature time - mInputParcel = new CryptoInputParcel(); - } - setTitle(R.string.nfc_text); vAnimator = (ViewAnimator) findViewById(R.id.view_animator); @@ -163,9 +151,8 @@ public class NfcOperationActivity extends BaseNfcActivity { break; } case NFC_SIGN: { - if (mInputParcel.getSignatureTime() == null) { - mInputParcel.addSignatureTime(new Date()); - } + mInputParcel.addSignatureTime(mRequiredInput.mSignatureTime); + for (int i = 0; i < mRequiredInput.mInputData.length; i++) { byte[] hash = mRequiredInput.mInputData[i]; int algo = mRequiredInput.mSignAlgos[i]; @@ -240,7 +227,7 @@ public class NfcOperationActivity extends BaseNfcActivity { throw new IOException("Inappropriate key flags for smart card key."); } - // TODO: Is this really needed? + // TODO: Is this really used anywhere? mInputParcel.addCryptoData(subkeyBytes, cardSerialNumber); } @@ -261,15 +248,13 @@ public class NfcOperationActivity extends BaseNfcActivity { protected void onNfcPostExecute() throws IOException { if (mServiceIntent != null) { // if we're triggered by OpenPgpService + // save updated cryptoInputParcel in cache CryptoInputParcelCacheService.addCryptoInputParcel(this, mServiceIntent, mInputParcel); - mServiceIntent.putExtra(EXTRA_CRYPTO_INPUT, - getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT)); setResult(RESULT_OK, mServiceIntent); } else { Intent result = new Intent(); + // send back the CryptoInputParcel we received result.putExtra(RESULT_CRYPTO_INPUT, mInputParcel); - // send back the CryptoInputParcel we receive, to conform with the pattern in - // CryptoOperationHelper setResult(RESULT_OK, result); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java index d1c247a76..0e70cda14 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/OrbotRequiredDialogActivity.java @@ -18,12 +18,22 @@ package org.sufficientlysecure.keychain.ui; +import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; import android.support.v4.app.FragmentActivity; +import android.view.ContextThemeWrapper; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ParcelableProxy; import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; @@ -34,8 +44,14 @@ import org.sufficientlysecure.keychain.util.orbot.OrbotHelper; public class OrbotRequiredDialogActivity extends FragmentActivity implements OrbotHelper.DialogActions { + public static final int MESSAGE_ORBOT_STARTED = 1; + public static final int MESSAGE_ORBOT_IGNORE = 2; + public static final int MESSAGE_DIALOG_CANCEL = 3; + // if suppplied and true will start Orbot directly without showing dialog public static final String EXTRA_START_ORBOT = "start_orbot"; + // used for communicating results when triggered from a service + public static final String EXTRA_MESSENGER = "messenger"; // to provide any previous crypto input into which proxy preference is merged public static final String EXTRA_CRYPTO_INPUT = "extra_crypto_input"; @@ -43,6 +59,9 @@ public class OrbotRequiredDialogActivity extends FragmentActivity public static final String RESULT_CRYPTO_INPUT = "result_crypto_input"; private CryptoInputParcel mCryptoInputParcel; + private Messenger mMessenger; + + private ProgressDialog mShowOrbotProgressDialog; @Override protected void onCreate(Bundle savedInstanceState) { @@ -54,9 +73,16 @@ public class OrbotRequiredDialogActivity extends FragmentActivity mCryptoInputParcel = new CryptoInputParcel(); } + mMessenger = getIntent().getParcelableExtra(EXTRA_MESSENGER); + boolean startOrbotDirect = getIntent().getBooleanExtra(EXTRA_START_ORBOT, false); if (startOrbotDirect) { - OrbotHelper.bestPossibleOrbotStart(this, this); + ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(this); + mShowOrbotProgressDialog = new ProgressDialog(theme); + mShowOrbotProgressDialog.setTitle(R.string.progress_starting_orbot); + mShowOrbotProgressDialog.setCancelable(false); + mShowOrbotProgressDialog.show(); + OrbotHelper.bestPossibleOrbotStart(this, this, false); } else { showDialog(); } @@ -84,13 +110,32 @@ public class OrbotRequiredDialogActivity extends FragmentActivity super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case OrbotHelper.START_TOR_RESULT: { - onOrbotStarted(); // assumption that orbot was started, no way to tell for sure + dismissOrbotProgressDialog(); + // unfortunately, this result is returned immediately and not when Orbot is started + // 10s is approximately the longest time Orbot has taken to start + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + onOrbotStarted(); // assumption that orbot was started + } + }, 10000); } } } + /** + * for when Orbot is started without showing the dialog by the EXTRA_START_ORBOT intent extra + */ + private void dismissOrbotProgressDialog() { + if (mShowOrbotProgressDialog != null) { + mShowOrbotProgressDialog.dismiss(); + } + } + @Override public void onOrbotStarted() { + dismissOrbotProgressDialog(); + sendMessage(MESSAGE_ORBOT_STARTED); Intent intent = new Intent(); // send back unmodified CryptoInputParcel for a retry intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel); @@ -100,6 +145,7 @@ public class OrbotRequiredDialogActivity extends FragmentActivity @Override public void onNeutralButton() { + sendMessage(MESSAGE_ORBOT_IGNORE); Intent intent = new Intent(); mCryptoInputParcel.addParcelableProxy(ParcelableProxy.getForNoProxy()); intent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel); @@ -109,6 +155,19 @@ public class OrbotRequiredDialogActivity extends FragmentActivity @Override public void onCancel() { + sendMessage(MESSAGE_DIALOG_CANCEL); finish(); } + + private void sendMessage(int what) { + if (mMessenger != null) { + Message msg = Message.obtain(); + msg.what = what; + try { + mMessenger.send(msg); + } catch (RemoteException e) { + Log.e(Constants.TAG, "Could not deliver message", e); + } + } + } }
\ No newline at end of file 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 d7224bd04..e71349880 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java @@ -71,6 +71,7 @@ import org.sufficientlysecure.keychain.util.Preferences; * internally and is NOT meant to be used by signing operations before adding a signature time */ public class PassphraseDialogActivity extends FragmentActivity { + public static final String RESULT_CRYPTO_INPUT = "result_data"; public static final String EXTRA_REQUIRED_INPUT = "required_input"; @@ -261,6 +262,9 @@ public class PassphraseDialogActivity extends FragmentActivity { case DIVERT_TO_CARD: message = getString(R.string.yubikey_pin_for, userId); break; + // special case: empty passphrase just returns the empty passphrase + case PASSPHRASE_EMPTY: + finishCaching(new Passphrase("")); default: throw new AssertionError("Unhandled SecretKeyType (should not happen)"); } @@ -280,51 +284,42 @@ public class PassphraseDialogActivity extends FragmentActivity { mPassphraseText.setText(message); - if (keyType == CanonicalizedSecretKey.SecretKeyType.PATTERN) { - // start pattern dialog and show progress circle here... -// Intent patternActivity = new Intent(getActivity(), LockPatternActivity.class); -// patternActivity.putExtra(LockPatternActivity.EXTRA_PATTERN, "123"); -// startActivityForResult(patternActivity, REQUEST_CODE_ENTER_PATTERN); - mInput.setVisibility(View.INVISIBLE); - mProgress.setVisibility(View.VISIBLE); - } else { - // Hack to open keyboard. - // This is the only method that I found to work across all Android versions - // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/ - // Notes: * onCreateView can't be used because we want to add buttons to the dialog - // * opening in onActivityCreated does not work on Android 4.4 - mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - mPassphraseEditText.post(new Runnable() { - @Override - public void run() { - if (getActivity() == null || mPassphraseEditText == null) { - return; - } - InputMethodManager imm = (InputMethodManager) getActivity() - .getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT); + // Hack to open keyboard. + // This is the only method that I found to work across all Android versions + // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/ + // Notes: * onCreateView can't be used because we want to add buttons to the dialog + // * opening in onActivityCreated does not work on Android 4.4 + mPassphraseEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + mPassphraseEditText.post(new Runnable() { + @Override + public void run() { + if (getActivity() == null || mPassphraseEditText == null) { + return; } - }); - } - }); - mPassphraseEditText.requestFocus(); - - mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE); - mPassphraseEditText.setOnEditorActionListener(this); - - if ((keyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD && Preferences.getPreferences(activity).useNumKeypadForYubiKeyPin()) - || keyType == CanonicalizedSecretKey.SecretKeyType.PIN) { - mPassphraseEditText.setInputType(InputType.TYPE_CLASS_NUMBER); - mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); - } else { - mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + InputMethodManager imm = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT); + } + }); } + }); + mPassphraseEditText.requestFocus(); + mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE); + mPassphraseEditText.setOnEditorActionListener(this); + + if ((keyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD && Preferences.getPreferences(activity).useNumKeypadForYubiKeyPin()) + || keyType == CanonicalizedSecretKey.SecretKeyType.PIN) { + mPassphraseEditText.setInputType(InputType.TYPE_CLASS_NUMBER); mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + } else { + mPassphraseEditText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } + mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + AlertDialog dialog = alert.create(); dialog.setButton(DialogInterface.BUTTON_POSITIVE, activity.getString(R.string.btn_unlock), (DialogInterface.OnClickListener) null); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java deleted file mode 100644 index e55494145..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseWizardActivity.java +++ /dev/null @@ -1,577 +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.ui; - -import android.annotation.TargetApi; -import android.support.v7.app.AlertDialog; -import android.app.PendingIntent; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.nfc.FormatException; -import android.nfc.NdefMessage; -import android.nfc.NdefRecord; -import android.nfc.NfcAdapter; -import android.nfc.Tag; -import android.nfc.tech.Ndef; -import android.os.Build; -import android.os.Bundle; -import android.provider.Settings; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.FragmentTransaction; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.util.Log; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.List; - -@TargetApi(Build.VERSION_CODES.HONEYCOMB) -public class PassphraseWizardActivity extends FragmentActivity { -//public class PassphraseWizardActivity extends FragmentActivity implements LockPatternView.OnPatternListener { - //create or authenticate - public String selectedAction; - //for lockpattern - public static char[] pattern; - private static String passphrase = ""; - //nfc string - private static byte[] output = new byte[8]; - - public static final String CREATE_METHOD = "create"; - public static final String AUTHENTICATION = "authenticate"; - - NfcAdapter adapter; - PendingIntent pendingIntent; - IntentFilter writeTagFilters[]; - boolean writeMode; - Tag myTag; - boolean writeNFC = false; - boolean readNFC = false; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getActionBar() != null) { - getActionBar().setTitle(R.string.unlock_method); - } - - selectedAction = getIntent().getAction(); - if (savedInstanceState == null) { - SelectMethods selectMethods = new SelectMethods(); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.add(R.id.fragmentContainer, selectMethods).commit(); - } - setContentView(R.layout.passphrase_wizard); - - adapter = NfcAdapter.getDefaultAdapter(this); - if (adapter != null) { - pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, PassphraseWizardActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); - IntentFilter tagDetected = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED); - tagDetected.addCategory(Intent.CATEGORY_DEFAULT); - writeTagFilters = new IntentFilter[]{tagDetected}; - } - } - - public void noPassphrase(View view) { - passphrase = ""; - Toast.makeText(this, R.string.no_passphrase_set, Toast.LENGTH_SHORT).show(); - this.finish(); - } - - public void passphrase(View view) { - Passphrase passphrase = new Passphrase(); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.fragmentContainer, passphrase).addToBackStack(null).commit(); - } - - public void startLockpattern(View view) { - if (getActionBar() != null) { - getActionBar().setTitle(R.string.draw_lockpattern); - } -// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction); -// LockPatternFragment lpf = LockPatternFragment.newInstance("asd"); - -// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); -// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit(); - } - - public void cancel(View view) { - this.finish(); - } - - public void savePassphrase(View view) { - EditText passphrase = (EditText) findViewById(R.id.passphrase); - passphrase.setError(null); - String pw = passphrase.getText().toString(); - //check and save passphrase - if (selectedAction.equals(CREATE_METHOD)) { - EditText passphraseAgain = (EditText) findViewById(R.id.passphraseAgain); - passphraseAgain.setError(null); - String pwAgain = passphraseAgain.getText().toString(); - - if (!TextUtils.isEmpty(pw)) { - if (!TextUtils.isEmpty(pwAgain)) { - if (pw.equals(pwAgain)) { - PassphraseWizardActivity.passphrase = pw; - Toast.makeText(this, getString(R.string.passphrase_saved), Toast.LENGTH_SHORT).show(); - this.finish(); - } else { - passphrase.setError(getString(R.string.passphrase_invalid)); - passphrase.requestFocus(); - } - } else { - passphraseAgain.setError(getString(R.string.missing_passphrase)); - passphraseAgain.requestFocus(); - } - } else { - passphrase.setError(getString(R.string.missing_passphrase)); - passphrase.requestFocus(); - } - } - //check for right passphrase - if (selectedAction.equals(AUTHENTICATION)) { - if (pw.equals(PassphraseWizardActivity.passphrase)) { - Toast.makeText(this, getString(R.string.unlocked), Toast.LENGTH_SHORT).show(); - this.finish(); - } else { - passphrase.setError(getString(R.string.passphrase_invalid)); - passphrase.requestFocus(); - } - } - } - - public void NFC(View view) { - if (adapter != null) { - if (getActionBar() != null) { - getActionBar().setTitle(R.string.nfc_title); - } - NFCFragment nfc = new NFCFragment(); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.fragmentContainer, nfc).addToBackStack(null).commit(); - - //if you want to create a new method or just authenticate - if (CREATE_METHOD.equals(selectedAction)) { - writeNFC = true; - } else if (AUTHENTICATION.equals(selectedAction)) { - readNFC = true; - } - - if (!adapter.isEnabled()) { - showAlertDialog(getString(R.string.enable_nfc), true); - } - } else { - showAlertDialog(getString(R.string.no_nfc_support), false); - } - } - - @Override - protected void onNewIntent(Intent intent) { - if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) { - myTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); - - if (writeNFC && CREATE_METHOD.equals(selectedAction)) { - //write new password on NFC tag - try { - if (myTag != null) { - write(myTag); - writeNFC = false; //just write once - Toast.makeText(this, R.string.nfc_write_succesful, Toast.LENGTH_SHORT).show(); - //advance to lockpattern -// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction); -// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); -// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit(); - } - } catch (IOException | FormatException e) { - Log.e(Constants.TAG, "Failed to write on NFC tag", e); - } - - } else if (readNFC && AUTHENTICATION.equals(selectedAction)) { - //read pw from NFC tag - try { - if (myTag != null) { - //if tag detected, read tag - String pwtag = read(myTag); - if (output != null && pwtag.equals(output.toString())) { - - //passwort matches, go to next view - Toast.makeText(this, R.string.passphrases_match + "!", Toast.LENGTH_SHORT).show(); - -// LockPatternFragmentOld lpf = LockPatternFragmentOld.newInstance(selectedAction); -// FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); -// transaction.replace(R.id.fragmentContainer, lpf).addToBackStack(null).commit(); - readNFC = false; //just once - } else { - //passwort doesnt match - TextView nfc = (TextView) findViewById(R.id.nfcText); - nfc.setText(R.string.nfc_wrong_tag); - } - } - } catch (IOException | FormatException e) { - Log.e(Constants.TAG, "Failed to read NFC tag", e); - } - } - } - } - - private void write(Tag tag) throws IOException, FormatException { - //generate new random key and write them on the tag - SecureRandom sr = new SecureRandom(); - sr.nextBytes(output); - NdefRecord[] records = {createRecord(output.toString())}; - NdefMessage message = new NdefMessage(records); - Ndef ndef = Ndef.get(tag); - ndef.connect(); - ndef.writeNdefMessage(message); - ndef.close(); - } - - private String read(Tag tag) throws IOException, FormatException { - //read string from tag - String password = null; - Ndef ndef = Ndef.get(tag); - ndef.connect(); - NdefMessage ndefMessage = ndef.getCachedNdefMessage(); - - NdefRecord[] records = ndefMessage.getRecords(); - for (NdefRecord ndefRecord : records) { - if (ndefRecord.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) { - try { - password = readText(ndefRecord); - } catch (UnsupportedEncodingException e) { - Log.e(Constants.TAG, "Failed to read password from tag", e); - } - } - } - ndef.close(); - return password; - } - - private String readText(NdefRecord record) throws UnsupportedEncodingException { - //low-level method for reading nfc - byte[] payload = record.getPayload(); - String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16"; - int languageCodeLength = payload[0] & 0063; - return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding); - } - - private NdefRecord createRecord(String text) throws UnsupportedEncodingException { - //low-level method for writing nfc - String lang = "en"; - byte[] textBytes = text.getBytes(); - byte[] langBytes = lang.getBytes("US-ASCII"); - int langLength = langBytes.length; - int textLength = textBytes.length; - byte[] payload = new byte[1 + langLength + textLength]; - - // set status byte (see NDEF spec for actual bits) - payload[0] = (byte) langLength; - // copy langbytes and textbytes into payload - System.arraycopy(langBytes, 0, payload, 1, langLength); - System.arraycopy(textBytes, 0, payload, 1 + langLength, textLength); - return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], payload); - } - - public void showAlertDialog(String message, boolean nfc) { - //This method shows an AlertDialog - AlertDialog.Builder alert = new AlertDialog.Builder(this); - alert.setTitle("Information").setMessage(message).setPositiveButton("Ok", - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - } - } - ); - if (nfc) { - - alert.setNeutralButton(R.string.nfc_settings, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialogInterface, int i) { - startActivity(new Intent(Settings.ACTION_NFC_SETTINGS)); - } - } - ); - } - alert.show(); - } - - @Override - public void onPause() { - //pause this app and free nfc intent - super.onPause(); - if (adapter != null) { - WriteModeOff(); - } - } - - @Override - public void onResume() { - //resume this app and get nfc intent - super.onResume(); - if (adapter != null) { - WriteModeOn(); - } - } - - private void WriteModeOn() { - //enable nfc for this view - writeMode = true; - adapter.enableForegroundDispatch(this, pendingIntent, writeTagFilters, null); - } - - private void WriteModeOff() { - //disable nfc for this view - writeMode = false; - adapter.disableForegroundDispatch(this); - } - - public static class SelectMethods extends Fragment { -// private OnFragmentInteractionListener mListener; - - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - */ - public SelectMethods() { - - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public void onResume() { - super.onResume(); - if (getActivity().getActionBar() != null) { - getActivity().getActionBar().setTitle(R.string.unlock_method); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.passphrase_wizard_fragment_select_methods, container, false); - } - -// @Override -// public void onAttach(Activity activity) { -// super.onAttach(activity); -// try { -// mListener = (OnFragmentInteractionListener) activity; -// } catch (ClassCastException e) { -// throw new ClassCastException(activity.toString() -// + " must implement OnFragmentInteractionListener"); -// } -// } -// -// @Override -// public void onDetach() { -// super.onDetach(); -// mListener = null; -// } - - /** - * This interface must be implemented by activities that contain this - * fragment to allow an interaction in this fragment to be communicated - * to the activity and potentially other fragments contained in that - * activity. - * <p/> - * See the Android Training lesson <a href= - * "http://developer.android.com/training/basics/fragments/communicating.html" - * >Communicating with Other Fragments</a> for more information. - */ -// public static interface OnFragmentInteractionListener { -// public void onFragmentInteraction(Uri uri); -// } - - } - - - // /** -// * A simple {@link android.support.v4.app.Fragment} subclass. -// * Activities that contain this fragment must implement the -// * {@link com.haibison.android.lockpattern.Passphrase.OnFragmentInteractionListener} interface -// * to handle interaction events. -// */ - public static class Passphrase extends Fragment { - -// private OnFragmentInteractionListener mListener; - - public Passphrase() { - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflate the layout for this fragment - View view = inflater.inflate(R.layout.passphrase_wizard_fragment_passphrase, container, false); - EditText passphraseAgain = (EditText) view.findViewById(R.id.passphraseAgain); - TextView passphraseText = (TextView) view.findViewById(R.id.passphraseText); - TextView passphraseTextAgain = (TextView) view.findViewById(R.id.passphraseTextAgain); - String selectedAction = getActivity().getIntent().getAction(); - if (selectedAction.equals(AUTHENTICATION)) { - passphraseAgain.setVisibility(View.GONE); - passphraseTextAgain.setVisibility(View.GONE); - passphraseText.setText(R.string.enter_passphrase); -// getActivity().getActionBar().setTitle(R.string.enter_passphrase); - } else if (selectedAction.equals(CREATE_METHOD)) { - passphraseAgain.setVisibility(View.VISIBLE); - passphraseTextAgain.setVisibility(View.VISIBLE); - passphraseText.setText(R.string.passphrase); -// getActivity().getActionBar().setTitle(R.string.set_passphrase); - } - return view; - } - -// @Override -// public void onAttach(Activity activity) { -// super.onAttach(activity); -// try { -// mListener = (OnFragmentInteractionListener) activity; -// } catch (ClassCastException e) { -// throw new ClassCastException(activity.toString() -// + " must implement OnFragmentInteractionListener"); -// } -// } -// -// @Override -// public void onDetach() { -// super.onDetach(); -// mListener = null; -// } - -// /** -// * This interface must be implemented by activities that contain this -// * fragment to allow an interaction in this fragment to be communicated -// * to the activity and potentially other fragments contained in that -// * activity. -// * <p/> -// * See the Android Training lesson <a href= -// * "http://developer.android.com/training/basics/fragments/communicating.html" -// * >Communicating with Other Fragments</a> for more information. -// */ -// public interface OnFragmentInteractionListener { -// public void onFragmentInteraction(Uri uri); -// } - } - - - /** - * A simple {@link android.support.v4.app.Fragment} subclass. - * Activities that contain this fragment must implement the - * interface - * to handle interaction events. - * Use the method to - * create an instance of this fragment. - */ - public static class NFCFragment extends Fragment { - // TODO: Rename parameter arguments, choose names that match - // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER - private static final String ARG_PARAM1 = "param1"; - private static final String ARG_PARAM2 = "param2"; - - // TODO: Rename and change types of parameters - private String mParam1; - private String mParam2; - -// private OnFragmentInteractionListener mListener; - - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment SelectMethods. - */ - // TODO: Rename and change types and number of parameters - public static NFCFragment newInstance(String param1, String param2) { - NFCFragment fragment = new NFCFragment(); - Bundle args = new Bundle(); - args.putString(ARG_PARAM1, param1); - args.putString(ARG_PARAM2, param2); - fragment.setArguments(args); - return fragment; - } - - public NFCFragment() { - // Required empty public constructor - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getArguments() != null) { - mParam1 = getArguments().getString(ARG_PARAM1); - mParam2 = getArguments().getString(ARG_PARAM2); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.passphrase_wizard_fragment_nfc, container, false); - } - -// // TODO: Rename method, update argument and hook method into UI event -// public void onButtonPressed(Uri uri) { -// if (mListener != null) { -// mListener.onFragmentInteraction(uri); -// } -// } - -// @Override -// public void onAttach(Activity activity) { -// super.onAttach(activity); -// try { -// mListener = (OnFragmentInteractionListener) activity; -// } catch (ClassCastException e) { -// throw new ClassCastException(activity.toString() -// + " must implement OnFragmentInteractionListener"); -// } -// } - - -// @Override -// public void onDetach() { -// super.onDetach(); -// mListener = null; -// } - } - -} 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 c18156428..eb9ee05af 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java @@ -18,11 +18,12 @@ package org.sufficientlysecure.keychain.ui; -import android.annotation.TargetApi; +import android.accounts.Account; +import android.accounts.AccountManager; import android.app.Activity; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; @@ -31,6 +32,8 @@ import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.ContactsContract; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.View; @@ -74,7 +77,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity { String action = getIntent().getAction(); - if (action != null && action.equals(ACTION_PREFS_CLOUD)) { + if (ACTION_PREFS_CLOUD.equals(action)) { addPreferencesFromResource(R.xml.cloud_search_prefs); mKeyServerPreference = (PreferenceScreen) findPreference(Constants.Pref.KEY_SERVERS); @@ -91,14 +94,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity { } }); initializeSearchKeyserver( - (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) + (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) ); initializeSearchKeybase( - (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) + (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) ); - } else if (action != null && action.equals(ACTION_PREFS_ADV)) { - addPreferencesFromResource(R.xml.adv_preferences); + } else if (ACTION_PREFS_ADV.equals(action)) { + addPreferencesFromResource(R.xml.passphrase_preferences); initializePassphraseCacheSubs( (CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS)); @@ -112,7 +115,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity { initializeUseNumKeypadForYubiKeyPin( (CheckBoxPreference) findPreference(Constants.Pref.USE_NUMKEYPAD_FOR_YUBIKEY_PIN)); - } else if (action != null && action.equals(ACTION_PREFS_GUI)) { + } else if (ACTION_PREFS_GUI.equals(action)) { addPreferencesFromResource(R.xml.gui_preferences); initializeTheme((ListPreference) findPreference(Constants.Pref.THEME)); @@ -192,10 +195,10 @@ public class SettingsActivity extends AppCompatPreferenceActivity { } }); initializeSearchKeyserver( - (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) + (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYSERVER) ); initializeSearchKeybase( - (CheckBoxPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) + (SwitchPreference) findPreference(Constants.Pref.SEARCH_KEYBASE) ); } @@ -219,14 +222,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity { /** * This fragment shows the PIN/password preferences */ - public static class AdvancedPrefsFragment extends PreferenceFragment { + public static class PassphrasePrefsFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Load the preferences from an XML resource - addPreferencesFromResource(R.xml.adv_preferences); + addPreferencesFromResource(R.xml.passphrase_preferences); initializePassphraseCacheSubs( (CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS)); @@ -253,8 +256,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity { } public static class Initializer { - private CheckBoxPreference mUseTor; - private CheckBoxPreference mUseNormalProxy; + private SwitchPreference mUseTor; + private SwitchPreference mUseNormalProxy; private EditTextPreference mProxyHost; private EditTextPreference mProxyPort; private ListPreference mProxyType; @@ -290,8 +293,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity { mActivity.addPreferencesFromResource(R.xml.proxy_prefs); } - mUseTor = (CheckBoxPreference) automaticallyFindPreference(Constants.Pref.USE_TOR_PROXY); - mUseNormalProxy = (CheckBoxPreference) automaticallyFindPreference(Constants.Pref.USE_NORMAL_PROXY); + mUseTor = (SwitchPreference) automaticallyFindPreference(Constants.Pref.USE_TOR_PROXY); + mUseNormalProxy = (SwitchPreference) automaticallyFindPreference(Constants.Pref.USE_NORMAL_PROXY); mProxyHost = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_HOST); mProxyPort = (EditTextPreference) automaticallyFindPreference(Constants.Pref.PROXY_PORT); mProxyType = (ListPreference) automaticallyFindPreference(Constants.Pref.PROXY_TYPE); @@ -467,11 +470,121 @@ public class SettingsActivity extends AppCompatPreferenceActivity { } } + /** + * This fragment shows the keyserver/contacts sync preferences + */ + public static class SyncPrefsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.sync_preferences); + } + + @Override + public void onResume() { + super.onResume(); + // this needs to be done in onResume since the user can change sync values from Android + // settings and we need to reflect that change when the user navigates back + AccountManager manager = AccountManager.get(getActivity()); + final Account account = manager.getAccountsByType(Constants.ACCOUNT_TYPE)[0]; + // for keyserver sync + initializeSyncCheckBox( + (SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER), + account, + Constants.PROVIDER_AUTHORITY + ); + // for contacts sync + initializeSyncCheckBox( + (SwitchPreference) findPreference(Constants.Pref.SYNC_CONTACTS), + account, + ContactsContract.AUTHORITY + ); + } + + private void initializeSyncCheckBox(final SwitchPreference syncCheckBox, + final Account account, + final String authority) { + boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority); + syncCheckBox.setChecked(syncEnabled); + setSummary(syncCheckBox, authority, syncEnabled); + + syncCheckBox.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean syncEnabled = (Boolean) newValue; + if (syncEnabled) { + ContentResolver.setSyncAutomatically(account, authority, true); + } else { + // disable syncs + ContentResolver.setSyncAutomatically(account, authority, false); + // cancel any ongoing/pending syncs + ContentResolver.cancelSync(account, authority); + } + setSummary(syncCheckBox, authority, syncEnabled); + return true; + } + }); + } + + private void setSummary(SwitchPreference syncCheckBox, String authority, + boolean checked) { + switch (authority) { + case Constants.PROVIDER_AUTHORITY: { + if (checked) { + syncCheckBox.setSummary(R.string.label_sync_settings_keyserver_summary_on); + } else { + syncCheckBox.setSummary(R.string.label_sync_settings_keyserver_summary_off); + } + break; + } + case ContactsContract.AUTHORITY: { + if (checked) { + syncCheckBox.setSummary(R.string.label_sync_settings_contacts_summary_on); + } else { + syncCheckBox.setSummary(R.string.label_sync_settings_contacts_summary_off); + } + break; + } + } + } + } + + /** + * This fragment shows experimental features + */ + public static class ExperimentalPrefsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.experimental_preferences); + + initializeExperimentalEnableWordConfirm( + (SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM)); + + initializeExperimentalEnableLinkedIdentities( + (SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES)); + + initializeExperimentalEnableKeybase( + (SwitchPreference) findPreference(Constants.Pref.EXPERIMENTAL_ENABLE_KEYBASE)); + + initializeTheme((ListPreference) findPreference(Constants.Pref.THEME)); + + } + } + protected boolean isValidFragment(String fragmentName) { - return AdvancedPrefsFragment.class.getName().equals(fragmentName) + 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); } @@ -502,11 +615,13 @@ public class SettingsActivity extends AppCompatPreferenceActivity { private static void initializeTheme(final ListPreference mTheme) { mTheme.setValue(sPreferences.getTheme()); - mTheme.setSummary(mTheme.getEntry()); + mTheme.setSummary(mTheme.getEntry() + "\n" + + mTheme.getContext().getString(R.string.label_experimental_settings_theme_summary)); mTheme.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { public boolean onPreferenceChange(Preference preference, Object newValue) { mTheme.setValue((String) newValue); - mTheme.setSummary(mTheme.getEntry()); + mTheme.setSummary(mTheme.getEntry() + "\n" + + mTheme.getContext().getString(R.string.label_experimental_settings_theme_summary)); sPreferences.setTheme((String) newValue); ((SettingsActivity) mTheme.getContext()).recreate(); @@ -516,7 +631,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity { }); } - private static void initializeSearchKeyserver(final CheckBoxPreference mSearchKeyserver) { + private static void initializeSearchKeyserver(final SwitchPreference mSearchKeyserver) { Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs(); mSearchKeyserver.setChecked(prefs.searchKeyserver); mSearchKeyserver.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @@ -529,7 +644,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity { }); } - private static void initializeSearchKeybase(final CheckBoxPreference mSearchKeybase) { + private static void initializeSearchKeybase(final SwitchPreference mSearchKeybase) { Preferences.CloudSearchPrefs prefs = sPreferences.getCloudSearchPrefs(); mSearchKeybase.setChecked(prefs.searchKeybase); mSearchKeybase.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @@ -571,4 +686,37 @@ public class SettingsActivity extends AppCompatPreferenceActivity { } }); } + + private static void initializeExperimentalEnableWordConfirm(final SwitchPreference mExperimentalEnableWordConfirm) { + mExperimentalEnableWordConfirm.setChecked(sPreferences.getExperimentalEnableWordConfirm()); + mExperimentalEnableWordConfirm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mExperimentalEnableWordConfirm.setChecked((Boolean) newValue); + sPreferences.setExperimentalEnableWordConfirm((Boolean) newValue); + return false; + } + }); + } + + private static void initializeExperimentalEnableLinkedIdentities(final SwitchPreference mExperimentalEnableLinkedIdentities) { + mExperimentalEnableLinkedIdentities.setChecked(sPreferences.getExperimentalEnableLinkedIdentities()); + mExperimentalEnableLinkedIdentities.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mExperimentalEnableLinkedIdentities.setChecked((Boolean) newValue); + sPreferences.setExperimentalEnableLinkedIdentities((Boolean) newValue); + return false; + } + }); + } + + private static void initializeExperimentalEnableKeybase(final SwitchPreference mExperimentalKeybase) { + mExperimentalKeybase.setChecked(sPreferences.getExperimentalEnableKeybase()); + mExperimentalKeybase.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + mExperimentalKeybase.setChecked((Boolean) newValue); + sPreferences.setExperimentalEnableKeybase((Boolean) newValue); + return false; + } + }); + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java index f61ada84f..7dd92c45f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsKeyServerActivity.java @@ -19,6 +19,7 @@ package org.sufficientlysecure.keychain.ui; import android.content.Intent; import android.os.Bundle; +import android.view.MenuItem; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.base.BaseActivity; @@ -34,6 +35,19 @@ public class SettingsKeyServerActivity extends BaseActivity { Intent intent = getIntent(); String servers[] = intent.getStringArrayExtra(EXTRA_KEY_SERVERS); loadFragment(savedInstanceState, servers); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + } + return super.onOptionsItemSelected(item); } @Override 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 1d0e085da..930c1fc26 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -18,6 +18,11 @@ package org.sufficientlysecure.keychain.ui; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; @@ -32,6 +37,9 @@ import android.os.Build; import android.os.Bundle; 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.FloatingActionButton; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentManager; import android.support.v4.app.LoaderManager; @@ -45,14 +53,13 @@ import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; -import com.getbase.floatingactionbutton.FloatingActionButton; - import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; @@ -65,8 +72,10 @@ import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; +import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType; import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; +import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard; import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; @@ -80,8 +89,6 @@ import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.NfcHelper; import org.sufficientlysecure.keychain.util.Preferences; -import java.io.IOException; -import java.util.ArrayList; public class ViewKeyActivity extends BaseNfcActivity implements LoaderManager.LoaderCallbacks<Cursor>, @@ -97,6 +104,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements static final int REQUEST_DELETE = 4; public static final String EXTRA_DISPLAY_RESULT = "display_result"; + public static final String EXTRA_LINKED_TRANSITION = "linked_transition"; ProviderHelper mProviderHelper; @@ -107,16 +115,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements private ArrayList<ParcelableKeyRing> mKeyList; private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mOperationHelper; - private TextView mName; private TextView mStatusText; private ImageView mStatusImage; - private RelativeLayout mBigToolbar; + private AppBarLayout mAppBarLayout; + private CollapsingToolbarLayout mCollapsingToolbarLayout; private ImageButton mActionEncryptFile; private ImageButton mActionEncryptText; private ImageButton mActionNfc; private FloatingActionButton mFab; private ImageView mPhoto; + private FrameLayout mPhotoLayout; private ImageView mQrCode; private CardView mQrCodeLayout; @@ -156,16 +165,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements setTitle(null); - mName = (TextView) findViewById(R.id.view_key_name); mStatusText = (TextView) findViewById(R.id.view_key_status); mStatusImage = (ImageView) findViewById(R.id.view_key_status_image); - mBigToolbar = (RelativeLayout) findViewById(R.id.toolbar_big); + mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout); + mCollapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); mActionEncryptFile = (ImageButton) findViewById(R.id.view_key_action_encrypt_files); mActionEncryptText = (ImageButton) findViewById(R.id.view_key_action_encrypt_text); mActionNfc = (ImageButton) findViewById(R.id.view_key_action_nfc); mFab = (FloatingActionButton) findViewById(R.id.fab); mPhoto = (ImageView) findViewById(R.id.view_key_photo); + mPhotoLayout = (FrameLayout) findViewById(R.id.view_key_photo_layout); mQrCode = (ImageView) findViewById(R.id.view_key_qr_code); mQrCodeLayout = (CardView) findViewById(R.id.view_key_qr_code_layout); @@ -287,13 +297,26 @@ public class ViewKeyActivity extends BaseNfcActivity implements return; } + boolean linkedTransition = getIntent().getBooleanExtra(EXTRA_LINKED_TRANSITION, false); + if (linkedTransition && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + postponeEnterTransition(); + } + FragmentManager manager = getSupportFragmentManager(); // Create an instance of the fragment - final ViewKeyFragment frag = ViewKeyFragment.newInstance(mDataUri); + final ViewKeyFragment frag = ViewKeyFragment.newInstance(mDataUri, + linkedTransition ? PostponeType.LINKED : PostponeType.NONE); manager.beginTransaction() .replace(R.id.view_key_fragment, frag) .commit(); + if (Preferences.getPreferences(this).getExperimentalEnableKeybase()) { + final ViewKeyKeybaseFragment keybaseFrag = ViewKeyKeybaseFragment.newInstance(mDataUri); + manager.beginTransaction() + .replace(R.id.view_key_keybase_fragment, keybaseFrag) + .commit(); + } + // need to postpone loading of the yubikey fragment until after mMasterKeyId // is available, but we mark here that this should be done mShowYubikeyAfterCreation = true; @@ -344,12 +367,23 @@ public class ViewKeyActivity extends BaseNfcActivity implements } return true; } + case R.id.menu_key_view_add_linked_identity: { + Intent intent = new Intent(this, LinkedIdWizard.class); + intent.setData(mDataUri); + startActivity(intent); + finish(); + return true; + } case R.id.menu_key_view_edit: { editKey(mDataUri); return true; } case R.id.menu_key_view_certify_fingerprint: { - certifyFingeprint(mDataUri); + certifyFingeprint(mDataUri, false); + return true; + } + case R.id.menu_key_view_certify_fingerprint_word: { + certifyFingeprint(mDataUri, true); return true; } } @@ -360,10 +394,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements public boolean onPrepareOptionsMenu(Menu menu) { 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 addLinked = menu.findItem(R.id.menu_key_view_add_linked_identity); + addLinked.setVisible(mIsSecret + && Preferences.getPreferences(this).getExperimentalEnableLinkedIdentities()); + MenuItem certifyFingerprint = menu.findItem(R.id.menu_key_view_certify_fingerprint); certifyFingerprint.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked); + MenuItem certifyFingerprintWord = menu.findItem(R.id.menu_key_view_certify_fingerprint_word); + certifyFingerprintWord.setVisible(!mIsSecret && !mIsVerified && !mIsExpired && !mIsRevoked + && Preferences.getPreferences(this).getExperimentalEnableWordConfirm()); return true; } @@ -375,16 +418,17 @@ public class ViewKeyActivity extends BaseNfcActivity implements startActivityForResult(scanQrCode, REQUEST_QR_FINGERPRINT); } - private void certifyFingeprint(Uri dataUri) { + private void certifyFingeprint(Uri dataUri, boolean enableWordConfirm) { Intent intent = new Intent(this, CertifyFingerprintActivity.class); intent.setData(dataUri); + intent.putExtra(CertifyFingerprintActivity.EXTRA_ENABLE_WORD_CONFIRM, enableWordConfirm); startActivityForResult(intent, REQUEST_CERTIFY); } private void certifyImmediate() { Intent intent = new Intent(this, CertifyKeyActivity.class); - intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[]{mMasterKeyId}); + intent.putExtra(CertifyKeyActivity.EXTRA_KEY_IDS, new long[] { mMasterKeyId }); startActivityForResult(intent, REQUEST_CERTIFY); } @@ -413,7 +457,8 @@ public class ViewKeyActivity extends BaseNfcActivity implements private void backupToFile() { new ExportHelper(this).showExportKeysDialog( - mMasterKeyId, Constants.Path.APP_DIR_FILE, true); + mMasterKeyId, new File(Constants.Path.APP_DIR, + KeyFormattingUtils.convertKeyIdToHex(mMasterKeyId) + ".sec.asc"), true); } private void deleteKey() { @@ -437,13 +482,13 @@ public class ViewKeyActivity extends BaseNfcActivity implements return; } - if (resultCode != Activity.RESULT_OK) { - return; - } - switch (requestCode) { case REQUEST_QR_FINGERPRINT: { + if (resultCode != Activity.RESULT_OK) { + return; + } + // If there is an EXTRA_RESULT, that's an error. Just show it. if (data.hasExtra(OperationResult.EXTRA_RESULT)) { OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); @@ -465,11 +510,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements } case REQUEST_BACKUP: { + if (resultCode != Activity.RESULT_OK) { + return; + } + backupToFile(); return; } case REQUEST_CERTIFY: { + if (resultCode != Activity.RESULT_OK) { + return; + } + if (data.hasExtra(OperationResult.EXTRA_RESULT)) { OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); result.createNotify(this).show(); @@ -478,6 +531,10 @@ public class ViewKeyActivity extends BaseNfcActivity implements } case REQUEST_DELETE: { + if (resultCode != Activity.RESULT_OK) { + return; + } + setResult(RESULT_OK, data); finish(); return; @@ -530,7 +587,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements finish(); } }, R.string.snack_yubikey_view).show(); - // and if it's not found, offer import } catch (PgpKeyNotFoundException e) { Notify.create(this, R.string.snack_yubi_other, Notify.LENGTH_LONG, @@ -728,9 +784,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements // get name, email, and comment from USER_ID KeyRing.UserId mainUserId = KeyRing.splitUserId(data.getString(INDEX_USER_ID)); if (mainUserId.name != null) { - mName.setText(mainUserId.name); + mCollapsingToolbarLayout.setTitle(mainUserId.name); } else { - mName.setText(R.string.user_id_no_name); + mCollapsingToolbarLayout.setTitle(getString(R.string.user_id_no_name)); } mMasterKeyId = data.getLong(INDEX_MASTER_KEY_ID); @@ -767,8 +823,12 @@ public class ViewKeyActivity extends BaseNfcActivity implements } protected void onPostExecute(Bitmap photo) { + if (photo == null) { + return; + } + mPhoto.setImageBitmap(photo); - mPhoto.setVisibility(View.VISIBLE); + mPhotoLayout.setVisibility(View.VISIBLE); } }; @@ -781,9 +841,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements State.REVOKED, R.color.icons, true); color = getResources().getColor(R.color.key_flag_red); - mActionEncryptFile.setVisibility(View.GONE); - mActionEncryptText.setVisibility(View.GONE); - mActionNfc.setVisibility(View.GONE); + mActionEncryptFile.setVisibility(View.INVISIBLE); + mActionEncryptText.setVisibility(View.INVISIBLE); + mActionNfc.setVisibility(View.INVISIBLE); mFab.setVisibility(View.GONE); mQrCodeLayout.setVisibility(View.GONE); } else if (mIsExpired) { @@ -797,9 +857,9 @@ public class ViewKeyActivity extends BaseNfcActivity implements State.EXPIRED, R.color.icons, true); color = getResources().getColor(R.color.key_flag_red); - mActionEncryptFile.setVisibility(View.GONE); - mActionEncryptText.setVisibility(View.GONE); - mActionNfc.setVisibility(View.GONE); + mActionEncryptFile.setVisibility(View.INVISIBLE); + mActionEncryptText.setVisibility(View.INVISIBLE); + mActionNfc.setVisibility(View.INVISIBLE); mFab.setVisibility(View.GONE); mQrCodeLayout.setVisibility(View.GONE); } else if (mIsSecret) { @@ -814,15 +874,15 @@ public class ViewKeyActivity extends BaseNfcActivity implements mQrCodeLayout.setVisibility(View.VISIBLE); // and place leftOf qr code - RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams) - mName.getLayoutParams(); - // remove right margin - nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - nameParams.setMarginEnd(0); - } - nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); - mName.setLayoutParams(nameParams); +// RelativeLayout.LayoutParams nameParams = (RelativeLayout.LayoutParams) +// mName.getLayoutParams(); +// // remove right margin +// nameParams.setMargins(FormattingUtils.dpToPx(this, 48), 0, 0, 0); +// if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { +// nameParams.setMarginEnd(0); +// } +// nameParams.addRule(RelativeLayout.LEFT_OF, R.id.view_key_qr_code_layout); +// mName.setLayoutParams(nameParams); RelativeLayout.LayoutParams statusParams = (RelativeLayout.LayoutParams) mStatusText.getLayoutParams(); @@ -844,7 +904,7 @@ public class ViewKeyActivity extends BaseNfcActivity implements } mFab.setVisibility(View.VISIBLE); // noinspection deprecation (no getDrawable with theme at current minApi level 15!) - mFab.setIconDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp)); + mFab.setImageDrawable(getResources().getDrawable(R.drawable.ic_repeat_white_24dp)); } else { mActionEncryptFile.setVisibility(View.VISIBLE); mActionEncryptText.setVisibility(View.VISIBLE); @@ -872,22 +932,19 @@ public class ViewKeyActivity extends BaseNfcActivity implements } if (mPreviousColor == 0 || mPreviousColor == color) { - mStatusBar.setBackgroundColor(getStatusBarBackgroundColor(color)); - mBigToolbar.setBackgroundColor(color); + mAppBarLayout.setBackgroundColor(color); + mCollapsingToolbarLayout.setContentScrimColor(color); + mCollapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color)); mPreviousColor = color; } else { - ObjectAnimator colorFade1 = - ObjectAnimator.ofObject(mStatusBar, "backgroundColor", - new ArgbEvaluator(), mPreviousColor, - getStatusBarBackgroundColor(color)); - ObjectAnimator colorFade2 = - ObjectAnimator.ofObject(mBigToolbar, "backgroundColor", + ObjectAnimator colorFade = + ObjectAnimator.ofObject(mAppBarLayout, "backgroundColor", new ArgbEvaluator(), mPreviousColor, color); + mCollapsingToolbarLayout.setContentScrimColor(color); + mCollapsingToolbarLayout.setStatusBarScrimColor(getStatusBarBackgroundColor(color)); - colorFade1.setDuration(1200); - colorFade2.setDuration(1200); - colorFade1.start(); - colorFade2.start(); + colorFade.setDuration(1200); + colorFade.start(); mPreviousColor = color; } @@ -963,4 +1020,6 @@ public class ViewKeyActivity extends BaseNfcActivity implements public boolean onCryptoSetProgress(String msg, int progress, int max) { return true; } + } + 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 673092e61..edd9feec9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvActivity.java @@ -57,7 +57,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements public static final int TAB_IDENTITIES = 1; public static final int TAB_SUBKEYS = 2; public static final int TAB_CERTS = 3; - public static final int TAB_KEYBASE = 4; // view private ViewPager mViewPager; @@ -140,11 +139,6 @@ public class ViewKeyAdvActivity extends BaseActivity implements adapter.addTab(ViewKeyAdvCertsFragment.class, certsBundle, getString(R.string.key_view_tab_certs)); - Bundle trustBundle = new Bundle(); - trustBundle.putParcelable(ViewKeyTrustFragment.ARG_DATA_URI, dataUri); - adapter.addTab(ViewKeyTrustFragment.class, - trustBundle, getString(R.string.key_view_tab_keybase)); - // update layout after operations mSlidingTabLayout.setViewPager(mViewPager); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java index 7bfebaf62..ad437f924 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyAdvUserIdsFragment.java @@ -133,7 +133,7 @@ public class ViewKeyAdvUserIdsFragment extends LoaderFragment implements case LOADER_ID_USER_IDS: { Uri baseUri = UserPackets.buildUserIdsUri(mDataUri); return new CursorLoader(getActivity(), baseUri, - UserIdsAdapter.USER_IDS_PROJECTION, null, null, null); + UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null); } default: diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java index a929d52f0..7be695de0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyFragment.java @@ -18,52 +18,73 @@ package org.sufficientlysecure.keychain.ui; + +import java.io.IOException; +import java.util.List; + +import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; +import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Bundle; +import android.os.Handler; import android.provider.ContactsContract; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v7.widget.CardView; +import android.transition.Fade; +import android.transition.Transition; +import android.transition.TransitionInflater; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.*; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnPreDrawListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround; import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter; import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; import org.sufficientlysecure.keychain.ui.dialog.UserIdInfoDialogFragment; +import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment; +import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.OnIdentityLoadedListener; import org.sufficientlysecure.keychain.util.ContactHelper; import org.sufficientlysecure.keychain.util.Log; - -import java.util.List; +import org.sufficientlysecure.keychain.util.Preferences; public class ViewKeyFragment extends LoaderFragment implements LoaderManager.LoaderCallbacks<Cursor> { public static final String ARG_DATA_URI = "uri"; + public static final String ARG_POSTPONE_TYPE = "postpone_type"; private ListView mUserIds; //private ListView mLinkedSystemContact; - boolean mIsSecret = false; + enum PostponeType { + NONE, LINKED; + } - CardView mSystemContactCard; - LinearLayout mSystemContactLayout; - ImageView mSystemContactPicture; - TextView mSystemContactName; + boolean mIsSecret = false; private static final int LOADER_ID_UNIFIED = 0; private static final int LOADER_ID_USER_IDS = 1; private static final int LOADER_ID_LINKED_CONTACT = 2; + private static final int LOADER_ID_LINKED_IDS = 3; private static final String LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID = "loader_linked_contact_master_key_id"; @@ -71,16 +92,29 @@ public class ViewKeyFragment extends LoaderFragment implements = "loader_linked_contact_is_secret"; private UserIdsAdapter mUserIdsAdapter; + private LinkedIdsAdapter mLinkedIdsAdapter; private Uri mDataUri; + private PostponeType mPostponeType; + + private CardView mSystemContactCard; + private LinearLayout mSystemContactLayout; + private ImageView mSystemContactPicture; + private TextView mSystemContactName; + + private ListView mLinkedIds; + private CardView mLinkedIdsCard; + private byte[] mFingerprint; + private TextView mLinkedIdsExpander; /** * Creates new instance of this fragment */ - public static ViewKeyFragment newInstance(Uri dataUri) { + public static ViewKeyFragment newInstance(Uri dataUri, PostponeType postponeType) { ViewKeyFragment frag = new ViewKeyFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_DATA_URI, dataUri); + args.putInt(ARG_POSTPONE_TYPE, postponeType.ordinal()); frag.setArguments(args); @@ -93,6 +127,11 @@ public class ViewKeyFragment extends LoaderFragment implements View view = inflater.inflate(R.layout.view_key_fragment, getContainer()); mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids); + mLinkedIdsCard = (CardView) view.findViewById(R.id.card_linked_ids); + + mLinkedIds = (ListView) view.findViewById(R.id.view_key_linked_ids); + + mLinkedIdsExpander = (TextView) view.findViewById(R.id.view_key_linked_ids_expander); mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override @@ -100,6 +139,12 @@ public class ViewKeyFragment extends LoaderFragment implements showUserIdInfo(position); } }); + mLinkedIds.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + showLinkedId(position); + } + }); mSystemContactCard = (CardView) view.findViewById(R.id.linked_system_contact_card); mSystemContactLayout = (LinearLayout) view.findViewById(R.id.system_contact_layout); @@ -109,6 +154,47 @@ public class ViewKeyFragment extends LoaderFragment implements return root; } + private void showLinkedId(final int position) { + final LinkedIdViewFragment frag; + try { + frag = mLinkedIdsAdapter.getLinkedIdFragment(mDataUri, position, mFingerprint); + } catch (IOException e) { + e.printStackTrace(); + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Transition trans = TransitionInflater.from(getActivity()) + .inflateTransition(R.transition.linked_id_card_trans); + // setSharedElementReturnTransition(trans); + setExitTransition(new Fade()); + frag.setSharedElementEnterTransition(trans); + } + + getFragmentManager().beginTransaction() + .add(R.id.view_key_fragment, frag) + .hide(frag) + .commit(); + + frag.setOnIdentityLoadedListener(new OnIdentityLoadedListener() { + @Override + public void onIdentityLoaded() { + new Handler().post(new Runnable() { + @Override + public void run() { + getFragmentManager().beginTransaction() + .show(frag) + .addSharedElement(mLinkedIdsCard, "card_linked_ids") + .remove(ViewKeyFragment.this) + .addToBackStack("linked_id") + .commit(); + } + }); + } + }); + + } + private void showUserIdInfo(final int position) { if (!mIsSecret) { final boolean isRevoked = mUserIdsAdapter.getIsRevoked(position); @@ -129,8 +215,6 @@ public class ViewKeyFragment extends LoaderFragment implements * Hides card if no linked system contact exists. Sets name, picture * and onClickListener for the linked system contact's layout. * In the case of a secret key, "me" (own profile) contact details are loaded. - * - * @param contactId */ private void loadLinkedSystemContact(final long contactId) { // contact doesn't exist, stop @@ -188,7 +272,6 @@ public class ViewKeyFragment extends LoaderFragment implements * ContactsContract.Contact table) * * @param contactId _ID for row in ContactsContract.Contacts table - * @param context */ private void launchContactActivity(final long contactId, Context context) { Intent intent = new Intent(Intent.ACTION_VIEW); @@ -202,6 +285,7 @@ public class ViewKeyFragment extends LoaderFragment implements super.onActivityCreated(savedInstanceState); Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); + mPostponeType = PostponeType.values()[getArguments().getInt(ARG_POSTPONE_TYPE, 0)]; if (dataUri == null) { Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); getActivity().finish(); @@ -225,12 +309,17 @@ public class ViewKeyFragment extends LoaderFragment implements }; static final int INDEX_MASTER_KEY_ID = 1; + @SuppressWarnings("unused") static final int INDEX_USER_ID = 2; + @SuppressWarnings("unused") static final int INDEX_IS_REVOKED = 3; + @SuppressWarnings("unused") static final int INDEX_IS_EXPIRED = 4; + @SuppressWarnings("unused") static final int INDEX_VERIFIED = 5; static final int INDEX_HAS_ANY_SECRET = 6; static final int INDEX_FINGERPRINT = 7; + @SuppressWarnings("unused") static final int INDEX_HAS_ENCRYPT = 8; private static final String[] RAWCONTACT_PROJECTION = { @@ -246,21 +335,26 @@ public class ViewKeyFragment extends LoaderFragment implements // Prepare the loaders. Either re-connect with an existing ones, // or start new ones. - // TODO Is this loader the same as the one in the activity? getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { - setContentShown(false); switch (id) { case LOADER_ID_UNIFIED: { + setContentShown(false, false); Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); } - case LOADER_ID_USER_IDS: + + case LOADER_ID_USER_IDS: { return UserIdsAdapter.createLoader(getActivity(), mDataUri); + } + + case LOADER_ID_LINKED_IDS: { + return LinkedIdsAdapter.createLoader(getActivity(), mDataUri); + } //we need a separate loader for linked contact to ensure refreshing on verification case LOADER_ID_LINKED_CONTACT: { @@ -310,19 +404,21 @@ public class ViewKeyFragment extends LoaderFragment implements if (data.moveToFirst()) { mIsSecret = data.getInt(INDEX_HAS_ANY_SECRET) != 0; + mFingerprint = data.getBlob(INDEX_FINGERPRINT); + long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); // load user ids after we know if it's a secret key mUserIdsAdapter = new UserIdsAdapter(getActivity(), null, 0, !mIsSecret, null); mUserIds.setAdapter(mUserIdsAdapter); getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); - long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID); - // we need to load linked contact here to prevent lag introduced by loader - // for the linked contact - long contactId = ContactHelper.findContactId( - getActivity().getContentResolver(), - masterKeyId); - loadLinkedSystemContact(contactId); + if (Preferences.getPreferences(getActivity()).getExperimentalEnableLinkedIdentities()) { + mLinkedIdsAdapter = + new LinkedIdsAdapter(getActivity(), null, 0, mIsSecret, mLinkedIdsExpander); + mLinkedIds.setAdapter(mLinkedIdsAdapter); + getLoaderManager().initLoader(LOADER_ID_LINKED_IDS, null, this); + } + Bundle linkedContactData = new Bundle(); linkedContactData.putLong(LOADER_EXTRA_LINKED_CONTACT_MASTER_KEY_ID, masterKeyId); @@ -336,10 +432,28 @@ public class ViewKeyFragment extends LoaderFragment implements } case LOADER_ID_USER_IDS: { + setContentShown(true, false); mUserIdsAdapter.swapCursor(data); break; } + case LOADER_ID_LINKED_IDS: { + mLinkedIdsAdapter.swapCursor(data); + mLinkedIdsCard.setVisibility(mLinkedIdsAdapter.getCount() > 0 ? View.VISIBLE : View.GONE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mPostponeType == PostponeType.LINKED) { + mLinkedIdsCard.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { + @TargetApi(VERSION_CODES.LOLLIPOP) + @Override + public boolean onPreDraw() { + mLinkedIdsCard.getViewTreeObserver().removeOnPreDrawListener(this); + getActivity().startPostponedEnterTransition(); + return true; + } + }); + } + break; + } + case LOADER_ID_LINKED_CONTACT: { if (data.moveToFirst()) {// if we have a linked contact long contactId = data.getLong(INDEX_CONTACT_ID); @@ -349,7 +463,6 @@ public class ViewKeyFragment extends LoaderFragment implements } } - setContentShown(true); } /** @@ -363,6 +476,11 @@ public class ViewKeyFragment extends LoaderFragment implements mUserIdsAdapter.swapCursor(null); break; } + case LOADER_ID_LINKED_IDS: { + mLinkedIdsCard.setVisibility(View.GONE); + mLinkedIdsAdapter.swapCursor(null); + break; + } } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java index 150acdc90..266633061 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyTrustFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeybaseFragment.java @@ -59,14 +59,12 @@ import java.util.ArrayList; import java.util.Hashtable; import java.util.List; -public class ViewKeyTrustFragment extends LoaderFragment implements +public class ViewKeyKeybaseFragment extends LoaderFragment implements LoaderManager.LoaderCallbacks<Cursor>, CryptoOperationHelper.Callback<KeybaseVerificationParcel, KeybaseVerificationResult> { public static final String ARG_DATA_URI = "uri"; - private View mStartSearch; - private TextView mTrustReadout; private TextView mReportHeader; private TableLayout mProofListing; private LayoutInflater mInflater; @@ -86,15 +84,25 @@ public class ViewKeyTrustFragment extends LoaderFragment implements private CryptoOperationHelper<KeybaseVerificationParcel, KeybaseVerificationResult> mKeybaseOpHelper; + /** + * Creates new instance of this fragment + */ + public static ViewKeyKeybaseFragment newInstance(Uri dataUri) { + ViewKeyKeybaseFragment frag = new ViewKeyKeybaseFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_DATA_URI, dataUri); + + frag.setArguments(args); + + return frag; + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { View root = super.onCreateView(inflater, superContainer, savedInstanceState); View view = inflater.inflate(R.layout.view_key_adv_keybase_fragment, getContainer()); mInflater = inflater; - mTrustReadout = (TextView) view.findViewById(R.id.view_key_trust_readout); - mStartSearch = view.findViewById(R.id.view_key_trust_search_cloud); - mStartSearch.setEnabled(false); mReportHeader = (TextView) view.findViewById(R.id.view_key_trust_cloud_narrative); mProofListing = (TableLayout) view.findViewById(R.id.view_key_proof_list); mProofVerifyHeader = view.findViewById(R.id.view_key_proof_verify_header); @@ -157,83 +165,45 @@ public class ViewKeyTrustFragment extends LoaderFragment implements } boolean nothingSpecial = true; - StringBuilder message = new StringBuilder(); // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) if (data.moveToFirst()) { - if (data.getInt(INDEX_UNIFIED_HAS_ANY_SECRET) != 0) { - message.append(getString(R.string.key_trust_it_is_yours)).append("\n"); - nothingSpecial = false; - } else if (data.getInt(INDEX_VERIFIED) != 0) { - message.append(getString(R.string.key_trust_already_verified)).append("\n"); - nothingSpecial = false; - } + final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT); + final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp); - // If this key is revoked, don’t trust it! - if (data.getInt(INDEX_TRUST_IS_REVOKED) != 0) { - message.append(getString(R.string.key_trust_revoked)). - append(getString(R.string.key_trust_old_keys)); + startSearch(fingerprint); + } - nothingSpecial = false; - } else { - if (data.getInt(INDEX_TRUST_IS_EXPIRED) != 0) { + setContentShown(true); + } - // if expired, don’t trust it! - message.append(getString(R.string.key_trust_expired)). - append(getString(R.string.key_trust_old_keys)); + private void startSearch(final String fingerprint) { + final Preferences.ProxyPrefs proxyPrefs = + Preferences.getPreferences(getActivity()).getProxyPrefs(); - nothingSpecial = false; - } + OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() { + @Override + public void onOrbotStarted() { + new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint); } - if (nothingSpecial) { - message.append(getString(R.string.key_trust_maybe)); + @Override + public void onNeutralButton() { + new DescribeKey(ParcelableProxy.getForNoProxy()) + .execute(fingerprint); } - final byte[] fp = data.getBlob(INDEX_TRUST_FINGERPRINT); - final String fingerprint = KeyFormattingUtils.convertFingerprintToHex(fp); - if (fingerprint != null) { + @Override + public void onCancel() { - mStartSearch.setEnabled(true); - mStartSearch.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - final Preferences.ProxyPrefs proxyPrefs = - Preferences.getPreferences(getActivity()).getProxyPrefs(); - - OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() { - @Override - public void onOrbotStarted() { - mStartSearch.setEnabled(false); - new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint); - } - - @Override - public void onNeutralButton() { - mStartSearch.setEnabled(false); - new DescribeKey(ParcelableProxy.getForNoProxy()) - .execute(fingerprint); - } - - @Override - public void onCancel() { - - } - }; - - if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) { - mStartSearch.setEnabled(false); - new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint); - } - } - }); } - } + }; - mTrustReadout.setText(message); - setContentShown(true); + if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) { + new DescribeKey(proxyPrefs.parcelableProxy).execute(fingerprint); + } } /** @@ -299,17 +269,16 @@ public class ViewKeyTrustFragment extends LoaderFragment implements return new ResultPage(getString(R.string.key_trust_results_prefix), proofList); } - private SpannableStringBuilder formatSpannableString(SpannableStringBuilder proofLinks,String proofType){ + private SpannableStringBuilder formatSpannableString(SpannableStringBuilder proofLinks, String proofType) { //Formatting SpannableStringBuilder with String.format() causes the links to stop working. //This method is to insert the links while reserving the links SpannableStringBuilder ssb = new SpannableStringBuilder(); ssb.append(proofType); - if(proofType.contains("%s")){ + if (proofType.contains("%s")) { int i = proofType.indexOf("%s"); - ssb.replace(i,i+2,proofLinks); - } - else ssb.append(proofLinks); + ssb.replace(i, i + 2, proofLinks); + } else ssb.append(proofLinks); return ssb; } @@ -343,7 +312,6 @@ public class ViewKeyTrustFragment extends LoaderFragment implements result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence); } - mStartSearch.setVisibility(View.GONE); mReportHeader.setVisibility(View.VISIBLE); mProofListing.setVisibility(View.VISIBLE); mReportHeader.setText(result.mHeader); @@ -358,22 +326,35 @@ public class ViewKeyTrustFragment extends LoaderFragment implements text.setMovementMethod(LinkMovementMethod.getInstance()); mProofListing.addView(row); } - - // mSearchReport.loadDataWithBaseURL("file:///android_res/drawable/", s, "text/html", "UTF-8", null); } } private String getProofNarrative(int proofType) { int stringIndex; switch (proofType) { - case Proof.PROOF_TYPE_TWITTER: stringIndex = R.string.keybase_narrative_twitter; break; - case Proof.PROOF_TYPE_GITHUB: stringIndex = R.string.keybase_narrative_github; break; - case Proof.PROOF_TYPE_DNS: stringIndex = R.string.keybase_narrative_dns; break; - case Proof.PROOF_TYPE_WEB_SITE: stringIndex = R.string.keybase_narrative_web_site; break; - case Proof.PROOF_TYPE_HACKERNEWS: stringIndex = R.string.keybase_narrative_hackernews; break; - case Proof.PROOF_TYPE_COINBASE: stringIndex = R.string.keybase_narrative_coinbase; break; - case Proof.PROOF_TYPE_REDDIT: stringIndex = R.string.keybase_narrative_reddit; break; - default: stringIndex = R.string.keybase_narrative_unknown; + case Proof.PROOF_TYPE_TWITTER: + stringIndex = R.string.keybase_narrative_twitter; + break; + case Proof.PROOF_TYPE_GITHUB: + stringIndex = R.string.keybase_narrative_github; + break; + case Proof.PROOF_TYPE_DNS: + stringIndex = R.string.keybase_narrative_dns; + break; + case Proof.PROOF_TYPE_WEB_SITE: + stringIndex = R.string.keybase_narrative_web_site; + break; + case Proof.PROOF_TYPE_HACKERNEWS: + stringIndex = R.string.keybase_narrative_hackernews; + break; + case Proof.PROOF_TYPE_COINBASE: + stringIndex = R.string.keybase_narrative_coinbase; + break; + case Proof.PROOF_TYPE_REDDIT: + stringIndex = R.string.keybase_narrative_reddit; + break; + default: + stringIndex = R.string.keybase_narrative_unknown; } return getActivity().getString(stringIndex); } @@ -390,14 +371,22 @@ public class ViewKeyTrustFragment extends LoaderFragment implements // which proofs do we have working verifiers for? private boolean haveProofFor(int proofType) { switch (proofType) { - case Proof.PROOF_TYPE_TWITTER: return true; - case Proof.PROOF_TYPE_GITHUB: return true; - case Proof.PROOF_TYPE_DNS: return true; - case Proof.PROOF_TYPE_WEB_SITE: return true; - case Proof.PROOF_TYPE_HACKERNEWS: return true; - case Proof.PROOF_TYPE_COINBASE: return true; - case Proof.PROOF_TYPE_REDDIT: return true; - default: return false; + case Proof.PROOF_TYPE_TWITTER: + return true; + case Proof.PROOF_TYPE_GITHUB: + return true; + case Proof.PROOF_TYPE_DNS: + return true; + case Proof.PROOF_TYPE_WEB_SITE: + return true; + case Proof.PROOF_TYPE_HACKERNEWS: + return true; + case Proof.PROOF_TYPE_COINBASE: + return true; + case Proof.PROOF_TYPE_REDDIT: + return true; + default: + return false; } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java index 59d772d63..56d273c7c 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Date; import android.content.Context; import android.database.Cursor; @@ -193,9 +194,9 @@ public class KeyAdapter extends CursorAdapter { String dateTime = DateUtils.formatDateTime(context, item.mCreation.getTime(), DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); - mCreationDate.setText(context.getString(R.string.label_key_created, dateTime)); mCreationDate.setTextColor(textColor); 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 new file mode 100644 index 000000000..5cf0e6e08 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsAdapter.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2014-2015 Dominik Schürmann <dominik@dominikschuermann.de> + * 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.adapter; + +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.support.v4.content.CursorLoader; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.linked.LinkedAttribute; +import org.sufficientlysecure.keychain.linked.UriAttribute; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; +import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker; +import org.sufficientlysecure.keychain.util.FilterCursorWrapper; + +import java.io.IOException; +import java.util.WeakHashMap; + +public class LinkedIdsAdapter extends UserAttributesAdapter { + private final boolean mIsSecret; + protected LayoutInflater mInflater; + WeakHashMap<Integer,UriAttribute> mLinkedIdentityCache = new WeakHashMap<>(); + + private Cursor mUnfilteredCursor; + + private TextView mExpander; + + public LinkedIdsAdapter(Context context, Cursor c, int flags, + boolean isSecret, TextView expander) { + super(context, c, flags); + mInflater = LayoutInflater.from(context); + mIsSecret = isSecret; + + if (expander != null) { + expander.setVisibility(View.GONE); + /* don't show an expander (maybe in some sort of advanced view?) + mExpander = expander; + mExpander.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + showUnfiltered(); + } + }); + */ + } + } + + @Override + public Cursor swapCursor(Cursor cursor) { + if (cursor == null) { + mUnfilteredCursor = null; + return super.swapCursor(null); + } + mUnfilteredCursor = cursor; + FilterCursorWrapper filteredCursor = new FilterCursorWrapper(cursor) { + @Override + public boolean isVisible(Cursor cursor) { + UriAttribute id = getItemAtPosition(cursor); + return id instanceof LinkedAttribute; + } + }; + + if (mExpander != null) { + int hidden = filteredCursor.getHiddenCount(); + if (hidden == 0) { + mExpander.setVisibility(View.GONE); + } else { + mExpander.setVisibility(View.VISIBLE); + mExpander.setText(mContext.getResources().getQuantityString( + R.plurals.linked_id_expand, hidden)); + } + } + + return super.swapCursor(filteredCursor); + } + + private void showUnfiltered() { + mExpander.setVisibility(View.GONE); + super.swapCursor(mUnfilteredCursor); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + + ViewHolder holder = (ViewHolder) view.getTag(); + + if (!mIsSecret) { + int isVerified = cursor.getInt(INDEX_VERIFIED); + switch (isVerified) { + case Certs.VERIFIED_SECRET: + KeyFormattingUtils.setStatusImage(mContext, holder.vVerified, + null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR); + break; + case Certs.VERIFIED_SELF: + KeyFormattingUtils.setStatusImage(mContext, holder.vVerified, + null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR); + break; + default: + KeyFormattingUtils.setStatusImage(mContext, holder.vVerified, + null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR); + break; + } + } + + UriAttribute id = getItemAtPosition(cursor); + holder.setData(mContext, id); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setTransitionName(id.mUri.toString()); + } + + } + + public UriAttribute getItemAtPosition(Cursor cursor) { + int rank = cursor.getInt(INDEX_RANK); + Log.d(Constants.TAG, "requested rank: " + rank); + + UriAttribute ret = mLinkedIdentityCache.get(rank); + if (ret != null) { + Log.d(Constants.TAG, "cached!"); + return ret; + } + Log.d(Constants.TAG, "not cached!"); + + try { + byte[] data = cursor.getBlob(INDEX_ATTRIBUTE_DATA); + ret = LinkedAttribute.fromAttributeData(data); + mLinkedIdentityCache.put(rank, ret); + return ret; + } catch (IOException e) { + Log.e(Constants.TAG, "could not read linked identity subpacket data", e); + return null; + } + } + + @Override + public UriAttribute getItem(int position) { + Cursor cursor = getCursor(); + cursor.moveToPosition(position); + return getItemAtPosition(cursor); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View v = mInflater.inflate(R.layout.linked_id_item, null); + ViewHolder holder = new ViewHolder(v); + v.setTag(holder); + return v; + } + + // don't show revoked user ids, irrelevant for average users + public static final String LINKED_IDS_WHERE = UserPackets.IS_REVOKED + " = 0"; + + public static CursorLoader createLoader(Activity activity, Uri dataUri) { + Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri); + return new CursorLoader(activity, baseUri, + UserIdsAdapter.USER_PACKETS_PROJECTION, LINKED_IDS_WHERE, null, null); + } + + public LinkedIdViewFragment getLinkedIdFragment(Uri baseUri, + int position, byte[] fingerprint) throws IOException { + Cursor c = getCursor(); + c.moveToPosition(position); + int rank = c.getInt(UserIdsAdapter.INDEX_RANK); + + Uri dataUri = UserPackets.buildLinkedIdsUri(baseUri); + return LinkedIdViewFragment.newInstance(dataUri, rank, mIsSecret, fingerprint); + } + + public static class ViewHolder { + final public ImageView vVerified; + final public ImageView vIcon; + final public TextView vTitle; + final public TextView vComment; + + public ViewHolder(View view) { + vVerified = (ImageView) view.findViewById(R.id.linked_id_certified_icon); + vIcon = (ImageView) view.findViewById(R.id.linked_id_type_icon); + vTitle = (TextView) view.findViewById(R.id.linked_id_title); + vComment = (TextView) view.findViewById(R.id.linked_id_comment); + } + + public void setData(Context context, UriAttribute id) { + + vTitle.setText(id.getDisplayTitle(context)); + + String comment = id.getDisplayComment(context); + if (comment != null) { + vComment.setVisibility(View.VISIBLE); + vComment.setText(comment); + } else { + vComment.setVisibility(View.GONE); + } + + vIcon.setImageResource(id.getDisplayIcon()); + + } + + public void seekAttention() { + ObjectAnimator anim = SubtleAttentionSeeker.tintText(vComment, 1000); + anim.setStartDelay(200); + anim.start(); + } + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java new file mode 100644 index 000000000..5ecd9f408 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/LinkedIdsCertAdapter.java @@ -0,0 +1,57 @@ +package org.sufficientlysecure.keychain.ui.adapter; + + +import android.app.Activity; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.support.v4.content.CursorLoader; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; + + +public class LinkedIdsCertAdapter extends CursorAdapter { + + public static final String[] USER_CERTS_PROJECTION = new String[]{ + UserPackets._ID, + UserPackets.TYPE, + UserPackets.USER_ID, + UserPackets.ATTRIBUTE_DATA, + UserPackets.RANK, + UserPackets.VERIFIED, + UserPackets.IS_PRIMARY, + UserPackets.IS_REVOKED + }; + protected static final int INDEX_ID = 0; + protected static final int INDEX_TYPE = 1; + protected static final int INDEX_USER_ID = 2; + protected static final int INDEX_ATTRIBUTE_DATA = 3; + protected static final int INDEX_RANK = 4; + protected static final int INDEX_VERIFIED = 5; + protected static final int INDEX_IS_PRIMARY = 6; + protected static final int INDEX_IS_REVOKED = 7; + + public LinkedIdsCertAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return null; + } + + public static CursorLoader createLoader(Activity activity, Uri dataUri) { + Uri baseUri = UserPackets.buildLinkedIdsUri(dataUri); + return new CursorLoader(activity, baseUri, + UserIdsAdapter.USER_PACKETS_PROJECTION, null, null, null); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java index 6b16e8445..b91abf076 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/MultiUserIdsAdapter.java @@ -171,7 +171,7 @@ public class MultiUserIdsAdapter extends CursorAdapter { CertifyAction action = actions.get(keyId); if (actions.get(keyId) == null) { - actions.put(keyId, new CertifyAction(keyId, uids)); + actions.put(keyId, new CertifyAction(keyId, uids, null)); } else { action.mUserIds.addAll(uids); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java index 4ea651bb5..f01f25200 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java @@ -137,9 +137,9 @@ abstract public class SelectKeyCursorAdapter extends CursorAdapter { String dateTime = DateUtils.formatDateTime(context, cursor.getLong(mIndexCreation) * 1000, DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_ABBREV_MONTH); - h.creation.setText(context.getString(R.string.label_key_created, dateTime)); h.creation.setVisibility(View.VISIBLE); } else { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java index 457083770..e0abaf4b0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserAttributesAdapter.java @@ -8,22 +8,24 @@ import android.view.View; import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; public abstract class UserAttributesAdapter extends CursorAdapter { - public static final String[] USER_IDS_PROJECTION = new String[]{ + public static final String[] USER_PACKETS_PROJECTION = new String[]{ UserPackets._ID, UserPackets.TYPE, UserPackets.USER_ID, + UserPackets.ATTRIBUTE_DATA, UserPackets.RANK, UserPackets.VERIFIED, UserPackets.IS_PRIMARY, UserPackets.IS_REVOKED }; - protected static final int INDEX_ID = 0; - protected static final int INDEX_TYPE = 1; - protected static final int INDEX_USER_ID = 2; - protected static final int INDEX_RANK = 3; - protected static final int INDEX_VERIFIED = 4; - protected static final int INDEX_IS_PRIMARY = 5; - protected static final int INDEX_IS_REVOKED = 6; + public static final int INDEX_ID = 0; + public static final int INDEX_TYPE = 1; + public static final int INDEX_USER_ID = 2; + public static final int INDEX_ATTRIBUTE_DATA = 3; + public static final int INDEX_RANK = 4; + public static final int INDEX_VERIFIED = 5; + public static final int INDEX_IS_PRIMARY = 6; + public static final int INDEX_IS_REVOKED = 7; public UserAttributesAdapter(Context context, Cursor c, int flags) { super(context, c, flags); @@ -46,4 +48,5 @@ public abstract class UserAttributesAdapter extends CursorAdapter { mCursor.moveToPosition(position); return mCursor.getInt(INDEX_VERIFIED); } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java index e2c6b0928..0f4312dad 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java @@ -188,7 +188,7 @@ public class UserIdsAdapter extends UserAttributesAdapter { public static CursorLoader createLoader(Activity activity, Uri dataUri) { Uri baseUri = UserPackets.buildUserIdsUri(dataUri); return new CursorLoader(activity, baseUri, - UserIdsAdapter.USER_IDS_PROJECTION, USER_IDS_WHERE, null, null); + UserIdsAdapter.USER_PACKETS_PROJECTION, USER_IDS_WHERE, null, null); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java index fcf5dc11e..aa4e7d840 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/BaseActivity.java @@ -30,6 +30,7 @@ import android.view.ViewGroup; import android.widget.TextView; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; /** @@ -51,6 +52,7 @@ public abstract class BaseActivity extends AppCompatActivity { @Override protected void onResume() { super.onResume(); + KeyserverSyncAdapterService.cancelUpdates(this); if (mThemeChanger.changeTheme()) { Intent intent = getIntent(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java index 52507f3e9..de90d48fd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationFragment.java @@ -18,9 +18,9 @@ package org.sufficientlysecure.keychain.ui.base; + import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; @@ -50,17 +50,21 @@ import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; * @see KeychainService * */ -abstract class CryptoOperationFragment<T extends Parcelable, S extends OperationResult> +public abstract class CryptoOperationFragment<T extends Parcelable, S extends OperationResult> extends Fragment implements CryptoOperationHelper.Callback<T, S> { final private CryptoOperationHelper<T, S> mOperationHelper; + public CryptoOperationFragment() { + mOperationHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_processing); + } + public CryptoOperationFragment(Integer initialProgressMsg) { mOperationHelper = new CryptoOperationHelper<>(1, this, this, initialProgressMsg); } - public CryptoOperationFragment() { - mOperationHelper = new CryptoOperationHelper<>(1, this, this, R.string.progress_processing); + public CryptoOperationFragment(int id, Integer initialProgressMsg) { + mOperationHelper = new CryptoOperationHelper<>(id, this, this, initialProgressMsg); } @Override 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 b33128978..52c6797d5 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 @@ -19,6 +19,8 @@ package org.sufficientlysecure.keychain.ui.base; +import java.util.Date; + import android.app.Activity; import android.app.ProgressDialog; import android.content.Intent; @@ -70,9 +72,11 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu // particular helper. a request code looks as follows: // (id << 9) + (1<<8) + REQUEST_CODE_X // that is, starting from LSB, there are 8 bits request code, 1 - // fixed bit set, then 7 bit operator-id code. the first two - // summands are stored in the mId for easy operation. - private final int mId; + // fixed bit set, then 7 bit helper-id code. the first two + // summands are stored in the mHelperId for easy operation. + private final int mHelperId; + // bitmask for helperId is everything except the least 8 bits + public static final int HELPER_ID_BITMASK = ~0xff; public static final int REQUEST_CODE_PASSPHRASE = 1; public static final int REQUEST_CODE_NFC = 2; @@ -92,7 +96,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu */ public CryptoOperationHelper(int id, FragmentActivity activity, Callback<T, S> callback, Integer progressMessageString) { - mId = (id << 9) + (1<<8); + mHelperId = (id << 9) + (1<<8); mActivity = activity; mUseFragment = false; mCallback = callback; @@ -103,7 +107,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu * if OperationHelper is being integrated into a fragment */ public CryptoOperationHelper(int id, Fragment fragment, Callback<T, S> callback, Integer progressMessageString) { - mId = (id << 9) + (1<<8); + mHelperId = (id << 9) + (1<<8); mFragment = fragment; mUseFragment = true; mProgressMessageResource = progressMessageString; @@ -162,9 +166,9 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu protected void startActivityForResult(Intent intent, int requestCode) { if (mUseFragment) { - mFragment.startActivityForResult(intent, mId + requestCode); + mFragment.startActivityForResult(intent, mHelperId + requestCode); } else { - mActivity.startActivityForResult(intent, mId + requestCode); + mActivity.startActivityForResult(intent, mHelperId + requestCode); } } @@ -176,13 +180,13 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { Log.d(Constants.TAG, "received activity result in OperationHelper"); - if ((requestCode & mId) != mId) { + if ((requestCode & HELPER_ID_BITMASK) != mHelperId) { // this wasn't meant for us to handle return false; } Log.d(Constants.TAG, "handling activity result in OperationHelper"); - // filter out mId from requestCode - requestCode ^= mId; + // filter out mHelperId from requestCode + requestCode ^= mHelperId; if (resultCode == Activity.RESULT_CANCELED) { mCallback.onCryptoOperationCancelled(); @@ -313,7 +317,7 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu } public void cryptoOperation() { - cryptoOperation(new CryptoInputParcel()); + cryptoOperation(new CryptoInputParcel(new Date())); } public void onHandleResult(OperationResult result) { 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 index 5ef8618ce..84774ae5e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java @@ -23,6 +23,7 @@ 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; @@ -119,14 +120,19 @@ public class FileDialogFragment extends DialogFragment { mFilename = (EditText) view.findViewById(R.id.input); mFilename.setText(mFile.getName()); mBrowse = (ImageButton) view.findViewById(R.id.btn_browse); - mBrowse.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - // only .asc or .gpg files - // setting it to text/plain prevents Cynaogenmod's file manager from selecting asc - // or gpg types! - FileHelper.openFile(FileDialogFragment.this, Uri.fromFile(mFile), "*/*", REQUEST_CODE); - } - }); + 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) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java new file mode 100644 index 000000000..e09b1e755 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateFinalFragment.java @@ -0,0 +1,225 @@ +package org.sufficientlysecure.keychain.ui.linked; + + +import android.graphics.PorterDuff; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.linked.LinkedAttribute; +import org.sufficientlysecure.keychain.linked.LinkedTokenResource; +import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.service.input.CryptoInputParcel; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.util.Notify; + + +public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragment { + + protected LinkedIdWizard mLinkedIdWizard; + + private ImageView mVerifyImage; + private TextView mVerifyStatus; + private ViewAnimator mVerifyAnimator; + + // This is a resource, set AFTER it has been verified + LinkedTokenResource mVerifiedResource = null; + private ViewAnimator mVerifyButtonAnimator; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mLinkedIdWizard = (LinkedIdWizard) getActivity(); + } + + protected abstract View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState); + + @Override @NonNull + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = newView(inflater, container, savedInstanceState); + + View nextButton = view.findViewById(R.id.next_button); + if (nextButton != null) { + nextButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + cryptoOperation(); + } + }); + } + + view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT); + } + }); + + mVerifyAnimator = (ViewAnimator) view.findViewById(R.id.verify_progress); + mVerifyImage = (ImageView) view.findViewById(R.id.verify_image); + mVerifyStatus = (TextView) view.findViewById(R.id.verify_status); + mVerifyButtonAnimator = (ViewAnimator) view.findViewById(R.id.verify_buttons); + + view.findViewById(R.id.button_verify).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + proofVerify(); + } + }); + + view.findViewById(R.id.button_retry).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + proofVerify(); + } + }); + + setVerifyProgress(false, null); + mVerifyStatus.setText(R.string.linked_verify_pending); + + return view; + } + + abstract LinkedTokenResource getResource(OperationLog log); + + private void setVerifyProgress(boolean on, Boolean success) { + if (success == null) { + mVerifyStatus.setText(R.string.linked_verifying); + displayButton(on ? 2 : 0); + } else if (success) { + mVerifyStatus.setText(R.string.linked_verify_success); + mVerifyImage.setImageResource(R.drawable.status_signature_verified_cutout_24dp); + mVerifyImage.setColorFilter(getResources().getColor(R.color.android_green_dark), + PorterDuff.Mode.SRC_IN); + displayButton(2); + } else { + mVerifyStatus.setText(R.string.linked_verify_error); + mVerifyImage.setImageResource(R.drawable.status_signature_unknown_cutout_24dp); + mVerifyImage.setColorFilter(getResources().getColor(R.color.android_red_dark), + PorterDuff.Mode.SRC_IN); + displayButton(1); + } + mVerifyAnimator.setDisplayedChild(on ? 1 : 0); + } + + public void displayButton(int button) { + if (mVerifyButtonAnimator.getDisplayedChild() == button) { + return; + } + mVerifyButtonAnimator.setDisplayedChild(button); + } + + protected void proofVerify() { + setVerifyProgress(true, null); + + new AsyncTask<Void,Void,LinkedVerifyResult>() { + + @Override + protected LinkedVerifyResult doInBackground(Void... params) { + long timer = System.currentTimeMillis(); + + OperationLog log = new OperationLog(); + LinkedTokenResource resource = getResource(log); + if (resource == null) { + return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log); + } + + LinkedVerifyResult result = resource.verify(getActivity(), mLinkedIdWizard.mFingerprint); + + // ux flow: this operation should take at last a second + timer = System.currentTimeMillis() -timer; + if (timer < 1000) try { + Thread.sleep(1000 -timer); + } catch (InterruptedException e) { + // never mind + } + + if (result.success()) { + mVerifiedResource = resource; + } + return result; + } + + @Override + protected void onPostExecute(LinkedVerifyResult result) { + super.onPostExecute(result); + if (result.success()) { + setVerifyProgress(false, true); + } else { + setVerifyProgress(false, false); + // on error, show error message + result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this); + } + } + }.execute(); + + } + + @Override + protected void cryptoOperation() { + if (mVerifiedResource == null) { + Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR) + .show(LinkedIdCreateFinalFragment.this); + return; + } + + super.cryptoOperation(); + } + + @Override + protected void cryptoOperation(CryptoInputParcel cryptoInput) { + if (mVerifiedResource == null) { + Notify.create(getActivity(), R.string.linked_need_verify, Notify.Style.ERROR) + .show(LinkedIdCreateFinalFragment.this); + return; + } + + super.cryptoOperation(cryptoInput); + } + + @Nullable + @Override + public Parcelable createOperationInput() { + SaveKeyringParcel skp = + new SaveKeyringParcel(mLinkedIdWizard.mMasterKeyId, mLinkedIdWizard.mFingerprint); + + WrappedUserAttribute ua = + LinkedAttribute.fromResource(mVerifiedResource).toUserAttribute(); + + skp.mAddUserAttribute.add(ua); + + return skp; + } + + @Override + public void onCryptoOperationSuccess(OperationResult result) { + // if bad -> display here! + if (!result.success()) { + result.createNotify(getActivity()).show(LinkedIdCreateFinalFragment.this); + return; + } + + getActivity().finish(); + } + + @Override + public void onCryptoOperationError(OperationResult result) { + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java new file mode 100644 index 000000000..ccb20a764 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateGithubFragment.java @@ -0,0 +1,706 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui.linked; + + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URL; +import java.util.Random; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.ActivityOptionsCompat; +import android.support.v4.app.FragmentActivity; +import android.util.Base64; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.webkit.CookieManager; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import javax.net.ssl.HttpsURLConnection; +import org.json.JSONException; +import org.json.JSONObject; +import org.spongycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.BuildConfig; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.linked.LinkedAttribute; +import org.sufficientlysecure.keychain.linked.resources.GithubResource; +import org.sufficientlysecure.keychain.operations.results.EditKeyResult; +import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; +import org.sufficientlysecure.keychain.ui.ViewKeyActivity; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.ui.widget.StatusIndicator; +import org.sufficientlysecure.keychain.ui.widget.StatusIndicator.Status; +import org.sufficientlysecure.keychain.util.Log; + + +public class LinkedIdCreateGithubFragment extends CryptoOperationFragment<SaveKeyringParcel,EditKeyResult> { + + public static final String ARG_GITHUB_COOKIE = "github_cookie"; + private Button mRetryButton; + + enum State { + IDLE, AUTH_PROCESS, AUTH_ERROR, POST_PROCESS, POST_ERROR, LID_PROCESS, LID_ERROR, DONE + } + + ViewAnimator mButtonContainer; + + StatusIndicator mStatus1, mStatus2, mStatus3; + + byte[] mFingerprint; + long mMasterKeyId; + private SaveKeyringParcel mSaveKeyringParcel; + private TextView mLinkedIdTitle, mLinkedIdComment; + private boolean mFinishOnStop; + + public static LinkedIdCreateGithubFragment newInstance() { + return new LinkedIdCreateGithubFragment(); + } + + public LinkedIdCreateGithubFragment() { + super(null); + } + + @Override @NonNull + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.linked_create_github_fragment, container, false); + + mButtonContainer = (ViewAnimator) view.findViewById(R.id.button_container); + + mStatus1 = (StatusIndicator) view.findViewById(R.id.linked_status_step1); + mStatus2 = (StatusIndicator) view.findViewById(R.id.linked_status_step2); + mStatus3 = (StatusIndicator) view.findViewById(R.id.linked_status_step3); + + mRetryButton = (Button) view.findViewById(R.id.button_retry); + + ((ImageView) view.findViewById(R.id.linked_id_type_icon)).setImageResource(R.drawable.linked_github); + ((ImageView) view.findViewById(R.id.linked_id_certified_icon)).setImageResource(R.drawable.octo_link_24dp); + mLinkedIdTitle = (TextView) view.findViewById(R.id.linked_id_title); + mLinkedIdComment = (TextView) view.findViewById(R.id.linked_id_comment); + + view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LinkedIdWizard activity = (LinkedIdWizard) getActivity(); + if (activity == null) { + return; + } + activity.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT); + } + }); + + view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + step1GetOAuthCode(); + // for animation testing + // onCryptoOperationSuccess(null); + } + }); + + return view; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + LinkedIdWizard wizard = (LinkedIdWizard) getActivity(); + mFingerprint = wizard.mFingerprint; + mMasterKeyId = wizard.mMasterKeyId; + } + + private void step1GetOAuthCode() { + + setState(State.AUTH_PROCESS); + + mButtonContainer.setDisplayedChild(1); + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + oAuthRequest("github.com/login/oauth/authorize", BuildConfig.GITHUB_CLIENT_ID, "gist"); + } + }, 300); + + } + + private void showRetryForOAuth() { + + mRetryButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + v.setOnClickListener(null); + step1GetOAuthCode(); + } + }); + mButtonContainer.setDisplayedChild(3); + + } + + private void step1GetOAuthToken() { + + if (mOAuthCode == null) { + setState(State.AUTH_ERROR); + showRetryForOAuth(); + return; + } + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + final String gistText = GithubResource.generate(activity, mFingerprint); + + new AsyncTask<Void,Void,JSONObject>() { + + Exception mException; + + @Override + protected JSONObject doInBackground(Void... dummy) { + try { + + JSONObject params = new JSONObject(); + params.put("client_id", BuildConfig.GITHUB_CLIENT_ID); + params.put("client_secret", BuildConfig.GITHUB_CLIENT_SECRET); + params.put("code", mOAuthCode); + params.put("state", mOAuthState); + + return jsonHttpRequest("https://github.com/login/oauth/access_token", params, null); + + } catch (IOException | HttpResultException e) { + mException = e; + } catch (JSONException e) { + throw new AssertionError("json error, this is a bug!"); + } + return null; + } + + @Override + protected void onPostExecute(JSONObject result) { + super.onPostExecute(result); + + Activity activity = getActivity(); + if (activity == null) { + // we couldn't show an error anyways + return; + } + + Log.d(Constants.TAG, "response: " + result); + + if (result == null || result.optString("access_token", null) == null) { + setState(State.AUTH_ERROR); + showRetryForOAuth(); + + if (result != null) { + Notify.create(activity, R.string.linked_error_auth_failed, Style.ERROR).show(); + return; + } + + if (mException instanceof SocketTimeoutException) { + Notify.create(activity, R.string.linked_error_timeout, Style.ERROR).show(); + } else if (mException instanceof HttpResultException) { + Notify.create(activity, activity.getString(R.string.linked_error_http, + ((HttpResultException) mException).mResponse), + Style.ERROR).show(); + } else if (mException instanceof IOException) { + Notify.create(activity, R.string.linked_error_network, Style.ERROR).show(); + } + + return; + } + + step2PostGist(result.optString("access_token"), gistText); + + } + }.execute(); + + } + + private void step2PostGist(final String accessToken, final String gistText) { + + setState(State.POST_PROCESS); + + new AsyncTask<Void,Void,JSONObject>() { + + Exception mException; + + @Override + protected JSONObject doInBackground(Void... dummy) { + try { + + long timer = System.currentTimeMillis(); + + JSONObject file = new JSONObject(); + file.put("content", gistText); + + JSONObject files = new JSONObject(); + files.put("openpgp.txt", file); + + JSONObject params = new JSONObject(); + params.put("public", true); + params.put("description", getString(R.string.linked_gist_description)); + params.put("files", files); + + JSONObject result = jsonHttpRequest("https://api.github.com/gists", params, accessToken); + + // ux flow: this operation should take at last a second + timer = System.currentTimeMillis() -timer; + if (timer < 1000) try { + Thread.sleep(1000 -timer); + } catch (InterruptedException e) { + // never mind + } + + return result; + + } catch (IOException | HttpResultException e) { + mException = e; + } catch (JSONException e) { + throw new AssertionError("json error, this is a bug!"); + } + return null; + } + + @Override + protected void onPostExecute(JSONObject result) { + super.onPostExecute(result); + + Log.d(Constants.TAG, "response: " + result); + + Activity activity = getActivity(); + if (activity == null) { + // we couldn't show an error anyways + return; + } + + if (result == null) { + setState(State.POST_ERROR); + showRetryForOAuth(); + + if (mException instanceof SocketTimeoutException) { + Notify.create(activity, R.string.linked_error_timeout, Style.ERROR).show(); + } else if (mException instanceof HttpResultException) { + Notify.create(activity, activity.getString(R.string.linked_error_http, + ((HttpResultException) mException).mResponse), + Style.ERROR).show(); + } else if (mException instanceof IOException) { + Notify.create(activity, R.string.linked_error_network, Style.ERROR).show(); + } + + return; + } + + GithubResource resource; + + try { + String gistId = result.getString("id"); + JSONObject owner = result.getJSONObject("owner"); + String gistLogin = owner.getString("login"); + + URI uri = URI.create("https://gist.github.com/" + gistLogin + "/" + gistId); + resource = GithubResource.create(uri); + } catch (JSONException e) { + setState(State.POST_ERROR); + return; + } + + View linkedItem = mButtonContainer.getChildAt(2); + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + linkedItem.setTransitionName(resource.toUri().toString()); + } + + // we only need authorization for this one operation, drop it afterwards + revokeToken(accessToken); + + step3EditKey(resource); + } + + }.execute(); + + } + + private void revokeToken(final String token) { + + new AsyncTask<Void,Void,Void>() { + @Override + protected Void doInBackground(Void... dummy) { + try { + HttpsURLConnection nection = (HttpsURLConnection) new URL( + "https://api.github.com/applications/" + BuildConfig.GITHUB_CLIENT_ID + "/tokens/" + token) + .openConnection(); + nection.setRequestMethod("DELETE"); + String encoded = Base64.encodeToString( + (BuildConfig.GITHUB_CLIENT_ID + ":" + BuildConfig.GITHUB_CLIENT_SECRET).getBytes(), Base64.DEFAULT); + nection.setRequestProperty("Authorization", "Basic " + encoded); + nection.connect(); + } catch (IOException e) { + // nvm + } + return null; + } + }.execute(); + + } + + private void step3EditKey(final GithubResource resource) { + + // set item data while we're there + { + Context context = getActivity(); + mLinkedIdTitle.setText(resource.getDisplayTitle(context)); + mLinkedIdComment.setText(resource.getDisplayComment(context)); + } + + setState(State.LID_PROCESS); + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + + WrappedUserAttribute ua = LinkedAttribute.fromResource(resource).toUserAttribute(); + mSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint); + mSaveKeyringParcel.mAddUserAttribute.add(ua); + + cryptoOperation(); + + } + }, 250); + + } + + @Nullable + @Override + public SaveKeyringParcel createOperationInput() { + // if this is null, the cryptoOperation silently aborts - which is what we want in that case + return mSaveKeyringParcel; + } + + @Override + public void onCryptoOperationSuccess(EditKeyResult result) { + + setState(State.DONE); + + mButtonContainer.getInAnimation().setDuration(750); + mButtonContainer.setDisplayedChild(2); + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + FragmentActivity activity = getActivity(); + Intent intent = new Intent(activity, ViewKeyActivity.class); + intent.setData(KeyRings.buildGenericKeyRingUri(mMasterKeyId)); + // intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + intent.putExtra(ViewKeyActivity.EXTRA_LINKED_TRANSITION, true); + View linkedItem = mButtonContainer.getChildAt(2); + + Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation( + activity, linkedItem, linkedItem.getTransitionName()).toBundle(); + activity.startActivity(intent, options); + mFinishOnStop = true; + } else { + activity.startActivity(intent); + activity.finish(); + } + } + }, 1000); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + // cookies are automatically saved, we don't want that + CookieManager cookieManager = CookieManager.getInstance(); + String cookie = cookieManager.getCookie("https://github.com/"); + outState.putString(ARG_GITHUB_COOKIE, cookie); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (savedInstanceState != null) { + String cookie = savedInstanceState.getString(ARG_GITHUB_COOKIE); + CookieManager cookieManager = CookieManager.getInstance(); + cookieManager.setCookie("https://github.com/", cookie); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + try { + // cookies are automatically saved, we don't want that + CookieManager cookieManager = CookieManager.getInstance(); + // noinspection deprecation (replacement is api lvl 21) + cookieManager.removeAllCookie(); + } catch (Exception e) { + // no biggie if this fails + } + } + + @Override + public void onStop() { + super.onStop(); + + if (mFinishOnStop) { + Activity activity = getActivity(); + activity.setResult(Activity.RESULT_OK); + activity.finish(); + } + } + + @Override + public void onCryptoOperationError(EditKeyResult result) { + result.createNotify(getActivity()).show(this); + setState(State.LID_ERROR); + } + + @Override + public void onCryptoOperationCancelled() { + mRetryButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + v.setOnClickListener(null); + mButtonContainer.setDisplayedChild(1); + setState(State.LID_PROCESS); + cryptoOperation(); + } + }); + mButtonContainer.setDisplayedChild(3); + setState(State.LID_ERROR); + } + + private String mOAuthCode, mOAuthState; + + public void oAuthRequest(String hostAndPath, String clientId, String scope) { + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + byte[] buf = new byte[16]; + new Random().nextBytes(buf); + mOAuthState = new String(Hex.encode(buf)); + mOAuthCode = null; + + final Dialog auth_dialog = new Dialog(activity); + auth_dialog.setContentView(R.layout.oauth_webview); + WebView web = (WebView) auth_dialog.findViewById(R.id.web_view); + web.getSettings().setSaveFormData(false); + web.getSettings().setUserAgentString("OpenKeychain " + BuildConfig.VERSION_NAME); + web.setWebViewClient(new WebViewClient() { + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + Uri uri = Uri.parse(url); + if ("oauth-openkeychain".equals(uri.getScheme())) { + + if (mOAuthCode != null) { + return true; + } + + if (uri.getQueryParameter("error") != null) { + Log.i(Constants.TAG, "got oauth error: " + uri.getQueryParameter("error")); + auth_dialog.dismiss(); + return true; + } + + // check if mOAuthState == queryParam[state] + mOAuthCode = uri.getQueryParameter("code"); + + auth_dialog.dismiss(); + return true; + } + // don't surf away from github! + if (!"github.com".equals(uri.getHost())) { + auth_dialog.dismiss(); + return true; + } + return false; + } + + }); + + auth_dialog.setTitle(R.string.linked_webview_title_github); + auth_dialog.setCancelable(true); + auth_dialog.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + step1GetOAuthToken(); + } + }); + auth_dialog.show(); + + web.loadUrl("https://" + hostAndPath + + "?client_id=" + clientId + + "&scope=" + scope + + "&redirect_uri=oauth-openkeychain://linked/" + + "&state=" + mOAuthState); + + } + + public void setState(State state) { + switch (state) { + case IDLE: + mStatus1.setDisplayedChild(Status.IDLE); + mStatus2.setDisplayedChild(Status.IDLE); + mStatus3.setDisplayedChild(Status.IDLE); + break; + case AUTH_PROCESS: + mStatus1.setDisplayedChild(Status.PROGRESS); + mStatus2.setDisplayedChild(Status.IDLE); + mStatus3.setDisplayedChild(Status.IDLE); + break; + case AUTH_ERROR: + mStatus1.setDisplayedChild(Status.ERROR); + mStatus2.setDisplayedChild(Status.IDLE); + mStatus3.setDisplayedChild(Status.IDLE); + break; + case POST_PROCESS: + mStatus1.setDisplayedChild(Status.OK); + mStatus2.setDisplayedChild(Status.PROGRESS); + mStatus3.setDisplayedChild(Status.IDLE); + break; + case POST_ERROR: + mStatus1.setDisplayedChild(Status.OK); + mStatus2.setDisplayedChild(Status.ERROR); + mStatus3.setDisplayedChild(Status.IDLE); + break; + case LID_PROCESS: + mStatus1.setDisplayedChild(Status.OK); + mStatus2.setDisplayedChild(Status.OK); + mStatus3.setDisplayedChild(Status.PROGRESS); + break; + case LID_ERROR: + mStatus1.setDisplayedChild(Status.OK); + mStatus2.setDisplayedChild(Status.OK); + mStatus3.setDisplayedChild(Status.ERROR); + break; + case DONE: + mStatus1.setDisplayedChild(Status.OK); + mStatus2.setDisplayedChild(Status.OK); + mStatus3.setDisplayedChild(Status.OK); + } + } + + private static JSONObject jsonHttpRequest(String url, JSONObject params, String accessToken) + throws IOException, HttpResultException { + + HttpsURLConnection nection = (HttpsURLConnection) new URL(url).openConnection(); + nection.setDoInput(true); + nection.setDoOutput(true); + nection.setConnectTimeout(2000); + nection.setReadTimeout(1000); + nection.setRequestProperty("Content-Type", "application/json"); + nection.setRequestProperty("Accept", "application/json"); + nection.setRequestProperty("User-Agent", "OpenKeychain " + BuildConfig.VERSION_NAME); + if (accessToken != null) { + nection.setRequestProperty("Authorization", "token " + accessToken); + } + + OutputStream os = nection.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); + writer.write(params.toString()); + writer.flush(); + writer.close(); + os.close(); + + try { + + nection.connect(); + + int code = nection.getResponseCode(); + if (code != HttpsURLConnection.HTTP_CREATED && code != HttpsURLConnection.HTTP_OK) { + throw new HttpResultException(nection.getResponseCode(), nection.getResponseMessage()); + } + + InputStream in = new BufferedInputStream(nection.getInputStream()); + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + StringBuilder response = new StringBuilder(); + while (true) { + String line = reader.readLine(); + if (line == null) { + break; + } + response.append(line); + } + + try { + return new JSONObject(response.toString()); + } catch (JSONException e) { + throw new IOException(e); + } + + } finally { + nection.disconnect(); + } + + } + + static class HttpResultException extends Exception { + final int mCode; + final String mResponse; + + HttpResultException(int code, String response) { + mCode = code; + mResponse = response; + } + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java new file mode 100644 index 000000000..8a05c35db --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep1Fragment.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui.linked; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Patterns; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.EditText; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource; + +public class LinkedIdCreateHttpsStep1Fragment extends Fragment { + + LinkedIdWizard mLinkedIdWizard; + + EditText mEditUri; + + public static LinkedIdCreateHttpsStep1Fragment newInstance() { + LinkedIdCreateHttpsStep1Fragment frag = new LinkedIdCreateHttpsStep1Fragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mLinkedIdWizard = (LinkedIdWizard) getActivity(); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.linked_create_https_fragment_step1, container, false); + + view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + + String uri = "https://" + mEditUri.getText(); + + if (!checkUri(uri)) { + return; + } + + String proofText = GenericHttpsResource.generateText(getActivity(), + mLinkedIdWizard.mFingerprint); + + LinkedIdCreateHttpsStep2Fragment frag = + LinkedIdCreateHttpsStep2Fragment.newInstance(uri, proofText); + + mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + + } + }); + + view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT); + } + }); + + mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri); + + mEditUri.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { + } + + @Override + public void afterTextChanged(Editable editable) { + String uri = "https://" + editable; + if (uri.length() > 0) { + if (checkUri(uri)) { + mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, + R.drawable.ic_stat_retyped_ok, 0); + } else { + mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, + R.drawable.ic_stat_retyped_bad, 0); + } + } else { + // remove drawable if email is empty + mEditUri.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); + } + } + }); + + // mEditUri.setText("mugenguild.com/pgpkey.txt"); + + return view; + } + + private static boolean checkUri(String uri) { + return Patterns.WEB_URL.matcher(uri).matches(); + } + +} 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 new file mode 100644 index 000000000..22a201ba3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateHttpsStep2Fragment.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui.linked; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.view.LayoutInflater; +import android.view.View; +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; +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; +import java.net.URISyntaxException; + +public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragment { + + private static final int REQUEST_CODE_OUTPUT = 0x00007007; + + public static final String ARG_URI = "uri", ARG_TEXT = "text"; + + EditText mEditUri; + + URI mResourceUri; + String mResourceString; + + public static LinkedIdCreateHttpsStep2Fragment newInstance + (String uri, String proofText) { + + LinkedIdCreateHttpsStep2Fragment frag = new LinkedIdCreateHttpsStep2Fragment(); + + Bundle args = new Bundle(); + args.putString(ARG_URI, uri); + args.putString(ARG_TEXT, proofText); + frag.setArguments(args); + + return frag; + } + + @Override + GenericHttpsResource getResource(OperationLog log) { + return GenericHttpsResource.createNew(mResourceUri); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + try { + mResourceUri = new URI(getArguments().getString(ARG_URI)); + } catch (URISyntaxException e) { + e.printStackTrace(); + getActivity().finish(); + } + + mResourceString = getArguments().getString(ARG_TEXT); + + } + + protected View newView(LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.linked_create_https_fragment_step2, container, false); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + + if (view != null) { + + view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + proofSend(); + } + }); + + view.findViewById(R.id.button_save).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + proofSave(); + } + }); + + mEditUri = (EditText) view.findViewById(R.id.linked_create_https_uri); + mEditUri.setText(mResourceUri.toString()); + } + + return view; + } + + private void proofSend () { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString); + sendIntent.setType("text/plain"); + startActivity(sendIntent); + } + + private void proofSave () { + String state = Environment.getExternalStorageState(); + if (!Environment.MEDIA_MOUNTED.equals(state)) { + Notify.create(getActivity(), "External storage not available!", Style.ERROR); + return; + } + + 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); + } + + private void saveFile(Uri uri) { + try { + PrintWriter out = + new PrintWriter(getActivity().getContentResolver().openOutputStream(uri)); + out.print(mResourceString); + if (out.checkError()) { + Notify.create(getActivity(), "Error writing file!", Style.ERROR).show(); + } + } catch (FileNotFoundException e) { + Notify.create(getActivity(), "File could not be opened for writing!", Style.ERROR).show(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + // For saving a file + case REQUEST_CODE_OUTPUT: + if (data == null) { + return; + } + Uri uri = data.getData(); + saveFile(uri); + break; + default: + super.onActivityResult(requestCode, resultCode, data); + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java new file mode 100644 index 000000000..c25f775b0 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep1Fragment.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui.linked; + +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.EditText; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.Notify; + +public class LinkedIdCreateTwitterStep1Fragment extends Fragment { + + LinkedIdWizard mLinkedIdWizard; + + EditText mEditHandle; + + public static LinkedIdCreateTwitterStep1Fragment newInstance() { + LinkedIdCreateTwitterStep1Fragment frag = new LinkedIdCreateTwitterStep1Fragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mLinkedIdWizard = (LinkedIdWizard) getActivity(); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.linked_create_twitter_fragment_step1, container, false); + + view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + + final String handle = mEditHandle.getText().toString(); + + if ("".equals(handle)) { + mEditHandle.setError("Please input a Twitter handle!"); + return; + } + + new AsyncTask<Void,Void,Boolean>() { + + @Override + protected Boolean doInBackground(Void... params) { + return true; + // return checkHandle(handle); + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + + if (result == null) { + Notify.create(getActivity(), + "Connection error while checking username!", + Notify.Style.ERROR).show(LinkedIdCreateTwitterStep1Fragment.this); + return; + } + + if (!result) { + Notify.create(getActivity(), + "This handle does not exist on Twitter!", + Notify.Style.ERROR).show(LinkedIdCreateTwitterStep1Fragment.this); + return; + } + + LinkedIdCreateTwitterStep2Fragment frag = + LinkedIdCreateTwitterStep2Fragment.newInstance(handle); + + mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + } + }.execute(); + + } + }); + + view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mLinkedIdWizard.loadFragment(null, null, LinkedIdWizard.FRAG_ACTION_TO_LEFT); + } + }); + + mEditHandle = (EditText) view.findViewById(R.id.linked_create_twitter_handle); + + return view; + } + + /* not used at this point, too many problems + private static Boolean checkHandle(String handle) { + try { + HttpURLConnection nection = + (HttpURLConnection) new URL("https://twitter.com/" + handle).openConnection(); + nection.setRequestMethod("HEAD"); + nection.setRequestProperty("User-Agent", "OpenKeychain"); + return nection.getResponseCode() == 200; + } catch (IOException e) { + return null; + } + } + */ + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java new file mode 100644 index 000000000..362798bc8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdCreateTwitterStep2Fragment.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui.linked; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.linked.LinkedTokenResource; +import org.sufficientlysecure.keychain.linked.resources.TwitterResource; + +public class LinkedIdCreateTwitterStep2Fragment extends LinkedIdCreateFinalFragment { + + public static final String ARG_HANDLE = "handle"; + + String mResourceHandle; + String mResourceString; + + public static LinkedIdCreateTwitterStep2Fragment newInstance + (String handle) { + + LinkedIdCreateTwitterStep2Fragment frag = new LinkedIdCreateTwitterStep2Fragment(); + + Bundle args = new Bundle(); + args.putString(ARG_HANDLE, handle); + frag.setArguments(args); + + return frag; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mResourceString = + TwitterResource.generate(mLinkedIdWizard.mFingerprint); + + mResourceHandle = getArguments().getString(ARG_HANDLE); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + + if (view != null) { + view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + proofSend(); + } + }); + + view.findViewById(R.id.button_share).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + proofShare(); + } + }); + + ((TextView) view.findViewById(R.id.linked_tweet_published)).setText( + Html.fromHtml(getString(R.string.linked_create_twitter_2_3, mResourceHandle)) + ); + } + + return view; + } + + @Override + LinkedTokenResource getResource(OperationLog log) { + return TwitterResource.searchInTwitterStream(getActivity(), + mResourceHandle, mResourceString, log); + } + + @Override + protected View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.linked_create_twitter_fragment_step2, container, false); + } + + private void proofShare() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, mResourceString); + sendIntent.setType("text/plain"); + startActivity(sendIntent); + } + + private void proofSend() { + + Uri.Builder builder = Uri.parse("https://twitter.com/intent/tweet").buildUpon(); + builder.appendQueryParameter("text", mResourceString); + Uri uri = builder.build(); + + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + getActivity().startActivity(intent); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java new file mode 100644 index 000000000..a17a97013 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdSelectFragment.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui.linked; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.sufficientlysecure.keychain.R; + +public class LinkedIdSelectFragment extends Fragment { + + LinkedIdWizard mLinkedIdWizard; + + /** + * Creates new instance of this fragment + */ + public static LinkedIdSelectFragment newInstance() { + LinkedIdSelectFragment frag = new LinkedIdSelectFragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.linked_select_fragment, container, false); + + view.findViewById(R.id.linked_create_https_button) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LinkedIdCreateHttpsStep1Fragment frag = + LinkedIdCreateHttpsStep1Fragment.newInstance(); + + mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + } + }); + + /* + view.findViewById(R.id.linked_create_dns_button) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LinkedIdCreateDnsStep1Fragment frag = + LinkedIdCreateDnsStep1Fragment.newInstance(); + + mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + } + }); + */ + + view.findViewById(R.id.linked_create_twitter_button) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LinkedIdCreateTwitterStep1Fragment frag = + LinkedIdCreateTwitterStep1Fragment.newInstance(); + + mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + } + }); + + view.findViewById(R.id.linked_create_github_button) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LinkedIdCreateGithubFragment frag = + LinkedIdCreateGithubFragment.newInstance(); + + mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT); + } + }); + + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mLinkedIdWizard = (LinkedIdWizard) getActivity(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java new file mode 100644 index 000000000..5630932b4 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdViewFragment.java @@ -0,0 +1,560 @@ +package org.sufficientlysecure.keychain.ui.linked; + +import java.io.IOException; +import java.util.Collections; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentManager.OnBackStackChangedListener; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextSwitcher; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Constants.key; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult; +import org.sufficientlysecure.keychain.linked.LinkedTokenResource; +import org.sufficientlysecure.keychain.linked.LinkedAttribute; +import org.sufficientlysecure.keychain.linked.LinkedResource; +import org.sufficientlysecure.keychain.linked.UriAttribute; +import org.sufficientlysecure.keychain.operations.results.OperationResult; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets; +import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables; +import org.sufficientlysecure.keychain.service.CertifyActionsParcel; +import org.sufficientlysecure.keychain.service.CertifyActionsParcel.CertifyAction; +import org.sufficientlysecure.keychain.ui.adapter.LinkedIdsAdapter; +import org.sufficientlysecure.keychain.ui.adapter.UserIdsAdapter; +import org.sufficientlysecure.keychain.ui.base.CryptoOperationFragment; +import org.sufficientlysecure.keychain.ui.linked.LinkedIdViewFragment.ViewHolder.VerifyState; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.ui.util.SubtleAttentionSeeker; +import org.sufficientlysecure.keychain.ui.widget.CertListWidget; +import org.sufficientlysecure.keychain.ui.widget.CertifyKeySpinner; +import org.sufficientlysecure.keychain.util.Log; + +public class LinkedIdViewFragment extends CryptoOperationFragment implements + LoaderManager.LoaderCallbacks<Cursor>, OnBackStackChangedListener { + + private static final String ARG_DATA_URI = "data_uri"; + private static final String ARG_LID_RANK = "rank"; + private static final String ARG_IS_SECRET = "verified"; + private static final String ARG_FINGERPRINT = "fingerprint"; + private static final int LOADER_ID_LINKED_ID = 1; + + private UriAttribute mLinkedId; + private LinkedTokenResource mLinkedResource; + private boolean mIsSecret; + + private Context mContext; + private byte[] mFingerprint; + + private AsyncTask mInProgress; + + private Uri mDataUri; + private ViewHolder mViewHolder; + private int mLidRank; + private OnIdentityLoadedListener mIdLoadedListener; + private long mCertifyKeyId; + + public static LinkedIdViewFragment newInstance(Uri dataUri, int rank, + boolean isSecret, byte[] fingerprint) throws IOException { + LinkedIdViewFragment frag = new LinkedIdViewFragment(); + + Bundle args = new Bundle(); + args.putParcelable(ARG_DATA_URI, dataUri); + args.putInt(ARG_LID_RANK, rank); + args.putBoolean(ARG_IS_SECRET, isSecret); + args.putByteArray(ARG_FINGERPRINT, fingerprint); + frag.setArguments(args); + + return frag; + } + + public LinkedIdViewFragment() { + // IMPORTANT: the id must be unique in the ViewKeyActivity CryptoOperationHelper id namespace! + // no initial progress message -> we handle progress ourselves! + super(5, null); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + mDataUri = args.getParcelable(ARG_DATA_URI); + mLidRank = args.getInt(ARG_LID_RANK); + + mIsSecret = args.getBoolean(ARG_IS_SECRET); + mFingerprint = args.getByteArray(ARG_FINGERPRINT); + + mContext = getActivity(); + + getLoaderManager().initLoader(LOADER_ID_LINKED_ID, null, this); + + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_ID_LINKED_ID: + return new CursorLoader(getActivity(), mDataUri, + UserIdsAdapter.USER_PACKETS_PROJECTION, + Tables.USER_PACKETS + "." + UserPackets.RANK + + " = " + Integer.toString(mLidRank), null, null); + default: + return null; + } + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { + switch (loader.getId()) { + case LOADER_ID_LINKED_ID: + + // Nothing to load means break if we are *expected* to load + if (!cursor.moveToFirst()) { + if (mIdLoadedListener != null) { + Notify.create(getActivity(), "Error loading identity!", + Notify.LENGTH_LONG, Style.ERROR).show(); + finishFragment(); + } + // Or just ignore, this is probably some intermediate state during certify + break; + } + + try { + int certStatus = cursor.getInt(UserIdsAdapter.INDEX_VERIFIED); + + byte[] data = cursor.getBlob(UserIdsAdapter.INDEX_ATTRIBUTE_DATA); + UriAttribute linkedId = LinkedAttribute.fromAttributeData(data); + + loadIdentity(linkedId, certStatus); + + if (mIdLoadedListener != null) { + mIdLoadedListener.onIdentityLoaded(); + mIdLoadedListener = null; + } + + } catch (IOException e) { + Log.e(Constants.TAG, "error parsing identity", e); + Notify.create(getActivity(), "Error parsing identity!", + Notify.LENGTH_LONG, Style.ERROR).show(); + finishFragment(); + } + + break; + } + } + + public void finishFragment() { + new Handler().post(new Runnable() { + @Override + public void run() { + FragmentManager manager = getFragmentManager(); + manager.removeOnBackStackChangedListener(LinkedIdViewFragment.this); + manager.popBackStack("linked_id", FragmentManager.POP_BACK_STACK_INCLUSIVE); + } + }); + } + + public interface OnIdentityLoadedListener { + void onIdentityLoaded(); + } + + public void setOnIdentityLoadedListener(OnIdentityLoadedListener listener) { + mIdLoadedListener = listener; + } + + private void loadIdentity(UriAttribute linkedId, int certStatus) { + mLinkedId = linkedId; + + if (mLinkedId instanceof LinkedAttribute) { + LinkedResource res = ((LinkedAttribute) mLinkedId).mResource; + mLinkedResource = (LinkedTokenResource) res; + } + + if (!mIsSecret) { + switch (certStatus) { + case Certs.VERIFIED_SECRET: + KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified, + null, State.VERIFIED, KeyFormattingUtils.DEFAULT_COLOR); + break; + case Certs.VERIFIED_SELF: + KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified, + null, State.UNVERIFIED, KeyFormattingUtils.DEFAULT_COLOR); + break; + default: + KeyFormattingUtils.setStatusImage(mContext, mViewHolder.mLinkedIdHolder.vVerified, + null, State.INVALID, KeyFormattingUtils.DEFAULT_COLOR); + break; + } + } else { + mViewHolder.mLinkedIdHolder.vVerified.setImageResource(R.drawable.octo_link_24dp); + } + + mViewHolder.mLinkedIdHolder.setData(mContext, mLinkedId); + + setShowVerifying(false); + + // no resource, nothing further we can do… + if (mLinkedResource == null) { + mViewHolder.vButtonView.setVisibility(View.GONE); + mViewHolder.vButtonVerify.setVisibility(View.GONE); + return; + } + + if (mLinkedResource.isViewable()) { + mViewHolder.vButtonView.setVisibility(View.VISIBLE); + mViewHolder.vButtonView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = mLinkedResource.getViewIntent(); + if (intent == null) { + return; + } + getActivity().startActivity(intent); + } + }); + } else { + mViewHolder.vButtonView.setVisibility(View.GONE); + } + + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + + } + + static class ViewHolder { + private final View vButtonView; + private final ViewAnimator vVerifyingContainer; + private final ViewAnimator vItemCertified; + private final View vKeySpinnerContainer; + LinkedIdsAdapter.ViewHolder mLinkedIdHolder; + + private ViewAnimator vButtonSwitcher; + private CertListWidget vLinkedCerts; + private CertifyKeySpinner vKeySpinner; + private final View vButtonVerify; + private final View vButtonRetry; + private final View vButtonConfirm; + + private final ViewAnimator vProgress; + private final TextSwitcher vText; + + ViewHolder(View root) { + vLinkedCerts = (CertListWidget) root.findViewById(R.id.linked_id_certs); + vKeySpinner = (CertifyKeySpinner) root.findViewById(R.id.cert_key_spinner); + vKeySpinnerContainer = root.findViewById(R.id.cert_key_spincontainer); + vButtonSwitcher = (ViewAnimator) root.findViewById(R.id.button_animator); + + mLinkedIdHolder = new LinkedIdsAdapter.ViewHolder(root); + + vButtonVerify = root.findViewById(R.id.button_verify); + vButtonRetry = root.findViewById(R.id.button_retry); + vButtonConfirm = root.findViewById(R.id.button_confirm); + vButtonView = root.findViewById(R.id.button_view); + + vVerifyingContainer = (ViewAnimator) root.findViewById(R.id.linked_verify_container); + vItemCertified = (ViewAnimator) root.findViewById(R.id.linked_id_certified); + + vProgress = (ViewAnimator) root.findViewById(R.id.linked_cert_progress); + vText = (TextSwitcher) root.findViewById(R.id.linked_cert_text); + } + + enum VerifyState { + VERIFYING, VERIFY_OK, VERIFY_ERROR, CERTIFYING + } + + void setVerifyingState(Context context, VerifyState state, boolean isSecret) { + switch (state) { + case VERIFYING: + vProgress.setDisplayedChild(0); + vText.setText(context.getString(R.string.linked_text_verifying)); + vKeySpinnerContainer.setVisibility(View.GONE); + break; + + case VERIFY_OK: + vProgress.setDisplayedChild(1); + if (!isSecret) { + showButton(2); + if (!vKeySpinner.isSingleEntry()) { + vKeySpinnerContainer.setVisibility(View.VISIBLE); + } + } else { + showButton(1); + vKeySpinnerContainer.setVisibility(View.GONE); + } + break; + + case VERIFY_ERROR: + showButton(1); + vProgress.setDisplayedChild(2); + vText.setText(context.getString(R.string.linked_text_error)); + vKeySpinnerContainer.setVisibility(View.GONE); + break; + + case CERTIFYING: + vProgress.setDisplayedChild(0); + vText.setText(context.getString(R.string.linked_text_confirming)); + vKeySpinnerContainer.setVisibility(View.GONE); + break; + } + } + + void showVerifyingContainer(Context context, boolean show, boolean isSecret) { + if (vVerifyingContainer.getDisplayedChild() == (show ? 1 : 0)) { + return; + } + + vVerifyingContainer.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down); + vVerifyingContainer.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down); + vVerifyingContainer.setDisplayedChild(show ? 1 : 0); + + vItemCertified.setInAnimation(context, show ? R.anim.fade_in_up : R.anim.fade_in_down); + vItemCertified.setOutAnimation(context, show ? R.anim.fade_out_up : R.anim.fade_out_down); + vItemCertified.setDisplayedChild(show || isSecret ? 1 : 0); + } + + void showButton(int which) { + if (vButtonSwitcher.getDisplayedChild() == which) { + return; + } + vButtonSwitcher.setDisplayedChild(which); + } + + } + + private boolean mVerificationState = false; + /** Switches between the 'verifying' ui bit and certificate status. This method + * must behave correctly in all states, showing or hiding the appropriate views + * and cancelling pending operations where necessary. + * + * This method also handles back button functionality in combination with + * onBackStateChanged. + */ + void setShowVerifying(boolean show) { + if (!show) { + if (mInProgress != null) { + mInProgress.cancel(false); + mInProgress = null; + } + getFragmentManager().removeOnBackStackChangedListener(this); + new Handler().post(new Runnable() { + @Override + public void run() { + getFragmentManager().popBackStack("verification", + FragmentManager.POP_BACK_STACK_INCLUSIVE); + } + }); + + if (!mVerificationState) { + return; + } + mVerificationState = false; + + mViewHolder.showButton(0); + mViewHolder.vKeySpinnerContainer.setVisibility(View.GONE); + mViewHolder.showVerifyingContainer(mContext, false, mIsSecret); + return; + } + + if (mVerificationState) { + return; + } + mVerificationState = true; + + FragmentManager manager = getFragmentManager(); + manager.beginTransaction().addToBackStack("verification").commit(); + manager.executePendingTransactions(); + manager.addOnBackStackChangedListener(this); + mViewHolder.showVerifyingContainer(mContext, true, mIsSecret); + + } + + @Override + public void onBackStackChanged() { + setShowVerifying(false); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.linked_id_view_fragment, null); + + mViewHolder = new ViewHolder(root); + root.setTag(mViewHolder); + + ((ImageView) root.findViewById(R.id.status_icon_verified)) + .setColorFilter(mContext.getResources().getColor(R.color.android_green_light), + PorterDuff.Mode.SRC_IN); + ((ImageView) root.findViewById(R.id.status_icon_invalid)) + .setColorFilter(mContext.getResources().getColor(R.color.android_red_light), + PorterDuff.Mode.SRC_IN); + + mViewHolder.vButtonVerify.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + verifyResource(); + } + }); + mViewHolder.vButtonRetry.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + verifyResource(); + } + }); + mViewHolder.vButtonConfirm.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + initiateCertifying(); + } + }); + + { + Bundle args = new Bundle(); + args.putParcelable(CertListWidget.ARG_URI, Certs.buildLinkedIdCertsUri(mDataUri, mLidRank)); + args.putBoolean(CertListWidget.ARG_IS_SECRET, mIsSecret); + getLoaderManager().initLoader(CertListWidget.LOADER_ID_LINKED_CERTS, + args, mViewHolder.vLinkedCerts); + } + + return root; + } + + void verifyResource() { + + // only one at a time (no sync needed, mInProgress is only touched in ui thread) + if (mInProgress != null) { + return; + } + + setShowVerifying(true); + + mViewHolder.vKeySpinnerContainer.setVisibility(View.GONE); + mViewHolder.setVerifyingState(mContext, VerifyState.VERIFYING, mIsSecret); + + mInProgress = new AsyncTask<Void,Void,LinkedVerifyResult>() { + @Override + protected LinkedVerifyResult doInBackground(Void... params) { + long timer = System.currentTimeMillis(); + LinkedVerifyResult result = mLinkedResource.verify(getActivity(), mFingerprint); + + // ux flow: this operation should take at last a second + timer = System.currentTimeMillis() -timer; + if (timer < 1000) try { + Thread.sleep(1000 -timer); + } catch (InterruptedException e) { + // never mind + } + + return result; + } + + @Override + protected void onPostExecute(LinkedVerifyResult result) { + if (isCancelled()) { + return; + } + if (result.success()) { + mViewHolder.vText.setText(getString(mLinkedResource.getVerifiedText(mIsSecret))); + // hack to preserve bold text + ((TextView) mViewHolder.vText.getCurrentView()).setText( + mLinkedResource.getVerifiedText(mIsSecret)); + mViewHolder.setVerifyingState(mContext, VerifyState.VERIFY_OK, mIsSecret); + mViewHolder.mLinkedIdHolder.seekAttention(); + } else { + mViewHolder.setVerifyingState(mContext, VerifyState.VERIFY_ERROR, mIsSecret); + result.createNotify(getActivity()).show(); + } + mInProgress = null; + } + }.execute(); + + } + + private void initiateCertifying() { + + if (mIsSecret) { + return; + } + + // get the user's passphrase for this key (if required) + mCertifyKeyId = mViewHolder.vKeySpinner.getSelectedKeyId(); + if (mCertifyKeyId == key.none || mCertifyKeyId == key.symmetric) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + SubtleAttentionSeeker.tintBackground(mViewHolder.vKeySpinnerContainer, 600).start(); + } else { + Notify.create(getActivity(), R.string.select_key_to_certify, Style.ERROR).show(); + } + return; + } + + mViewHolder.setVerifyingState(mContext, VerifyState.CERTIFYING, false); + cryptoOperation(); + + } + + @Override + public void onCryptoOperationCancelled() { + super.onCryptoOperationCancelled(); + + // go back to 'verified ok' + setShowVerifying(false); + + } + + @Nullable + @Override + public Parcelable createOperationInput() { + long masterKeyId = KeyFormattingUtils.convertFingerprintToKeyId(mFingerprint); + CertifyAction action = new CertifyAction(masterKeyId, null, + Collections.singletonList(mLinkedId.toUserAttribute())); + + // fill values for this action + CertifyActionsParcel parcel = new CertifyActionsParcel(mCertifyKeyId); + parcel.mCertifyActions.addAll(Collections.singletonList(action)); + + return parcel; + } + + @Override + public void onCryptoOperationSuccess(OperationResult result) { + result.createNotify(getActivity()).show(); + // no need to do anything else, we will get a loader refresh! + } + + @Override + public void onCryptoOperationError(OperationResult result) { + result.createNotify(getActivity()).show(); + } + + @Override + public boolean onCryptoSetProgress(String msg, int progress, int max) { + return true; + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java new file mode 100644 index 000000000..8c677199d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/linked/LinkedIdWizard.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui.linked; + + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.NavUtils; +import android.support.v4.app.TaskStackBuilder; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; +import org.sufficientlysecure.keychain.util.Log; + +public class LinkedIdWizard extends BaseActivity { + + public static final int FRAG_ACTION_START = 0; + public static final int FRAG_ACTION_TO_RIGHT = 1; + public static final int FRAG_ACTION_TO_LEFT = 2; + + long mMasterKeyId; + byte[] mFingerprint; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setTitle(getString(R.string.title_linked_id_create)); + + try { + Uri uri = getIntent().getData(); + uri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(uri); + CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(uri); + if (!ring.hasAnySecret()) { + Log.e(Constants.TAG, "Linked Identities can only be added to secret keys!"); + finish(); + return; + } + + mMasterKeyId = ring.extractOrGetMasterKeyId(); + mFingerprint = ring.getFingerprint(); + } catch (PgpKeyNotFoundException e) { + Log.e(Constants.TAG, "Invalid uri given, key does not exist!"); + finish(); + return; + } + + // pass extras into fragment + LinkedIdSelectFragment frag = LinkedIdSelectFragment.newInstance(); + loadFragment(null, frag, FRAG_ACTION_START); + } + + @Override + protected void initLayout() { + setContentView(R.layout.create_key_activity); + } + + public void loadFragment(Bundle savedInstanceState, Fragment fragment, int action) { + // However, if we're being restored from a previous state, + // then we don't need to do anything and should return or else + // we could end up with overlapping fragments. + if (savedInstanceState != null) { + return; + } + + hideKeyboard(); + + // Add the fragment to the 'fragment_container' FrameLayout + // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + + switch (action) { + case FRAG_ACTION_START: + transaction.setCustomAnimations(0, 0); + transaction.replace(R.id.create_key_fragment_container, fragment) + .commitAllowingStateLoss(); + break; + case FRAG_ACTION_TO_LEFT: + getSupportFragmentManager().popBackStackImmediate(); + break; + case FRAG_ACTION_TO_RIGHT: + transaction.setCustomAnimations(R.anim.frag_slide_in_from_right, R.anim.frag_slide_out_to_left, + R.anim.frag_slide_in_from_left, R.anim.frag_slide_out_to_right); + transaction.addToBackStack(null); + transaction.replace(R.id.create_key_fragment_container, fragment) + .commitAllowingStateLoss(); + break; + + } + // do it immediately! + getSupportFragmentManager().executePendingTransactions(); + } + + private void hideKeyboard() { + InputMethodManager inputManager = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + + // check if no view has focus + View v = getCurrentFocus(); + if (v == null) + return; + + inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + + @Override + public void onBackPressed() { + if (!getFragmentManager().popBackStackImmediate()) { + navigateBack(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + // Respond to the action bar's Up/Home button + case android.R.id.home: + navigateBack(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void navigateBack() { + Intent upIntent = NavUtils.getParentActivityIntent(this); + upIntent.setData(KeyRings.buildGenericKeyRingUri(mMasterKeyId)); + // This activity is NOT part of this app's task, so create a new task + // when navigating up, with a synthesized back stack. + TaskStackBuilder.create(this) + // Add all of this activity's parents to the back stack + .addNextIntentWithParentStack(upIntent) + // Navigate up to the closest parent + .startActivities(); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java new file mode 100644 index 000000000..43ccac24f --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ExperimentalWordConfirm.java @@ -0,0 +1,126 @@ +/* + * 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.util; + +import android.content.Context; + +import org.spongycastle.util.Arrays; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.BitSet; + +public class ExperimentalWordConfirm { + + public static String getWords(Context context, byte[] fingerprintBlob) { + ArrayList<String> words = new ArrayList<>(); + + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader( + context.getAssets().open("word_confirm_list.txt"), + "UTF-8" + )); + + String line = reader.readLine(); + while (line != null) { + words.add(line); + + line = reader.readLine(); + } + } catch (IOException e) { + throw new RuntimeException("IOException", e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) { + } + } + } + + String fingerprint = ""; + + // NOTE: 160 bit SHA-1 truncated to 156 bit + byte[] fingerprintBlobTruncated = Arrays.copyOfRange(fingerprintBlob, 0, 156 / 8); + + // TODO: implement key stretching to minimize fp length? + + // BitSet bits = BitSet.valueOf(fingerprintBlob); // min API 19 and little endian! + BitSet bits = bitSetToByteArray(fingerprintBlobTruncated); + Log.d(Constants.TAG, "bits: " + bits.toString()); + + final int CHUNK_SIZE = 13; + final int LAST_CHUNK_INDEX = fingerprintBlobTruncated.length * 8 / CHUNK_SIZE; // 12 + Log.d(Constants.TAG, "LAST_CHUNK_INDEX: " + LAST_CHUNK_INDEX); + + int from = 0; + int to = CHUNK_SIZE; + for (int i = 0; i < (LAST_CHUNK_INDEX + 1); i++) { + Log.d(Constants.TAG, "from: " + from + " to: " + to); + + BitSet setIndex = bits.get(from, to); + int wordIndex = (int) bitSetToLong(setIndex); + // int wordIndex = (int) setIndex.toLongArray()[0]; // min API 19 + + fingerprint += words.get(wordIndex); + + if (i != LAST_CHUNK_INDEX) { + // line break every 3 words + if (to % (CHUNK_SIZE * 3) == 0) { + fingerprint += "\n"; + } else { + fingerprint += " "; + } + } + + from = to; + to += CHUNK_SIZE; + } + + return fingerprint; + } + + /** + * Returns a BitSet containing the values in bytes. + * BIG ENDIAN! + */ + private static BitSet bitSetToByteArray(byte[] bytes) { + int arrayLength = bytes.length * 8; + BitSet bits = new BitSet(); + + for (int i = 0; i < arrayLength; i++) { + if ((bytes[bytes.length - i / 8 - 1] & (1 << (i % 8))) > 0) { + bits.set(i); + } + } + return bits; + } + + private static long bitSetToLong(BitSet bits) { + long value = 0L; + for (int i = 0; i < bits.length(); ++i) { + value += bits.get(i) ? (1L << i) : 0L; + } + return value; + } +} 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 9984c245e..284c17e7a 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 @@ -269,6 +269,10 @@ public class KeyFormattingUtils { return hexString; } + public static long convertFingerprintToKeyId(byte[] fingerprint) { + return ByteBuffer.wrap(fingerprint, 12, 8).getLong(); + } + /** * Makes a human-readable version of a key ID, which is usually 64 bits: lower-case, no * leading 0x, space-separated quartets (for keys whose length in hex is divisible by 4) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/SubtleAttentionSeeker.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/SubtleAttentionSeeker.java index 87444c226..4549e8993 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/SubtleAttentionSeeker.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/SubtleAttentionSeeker.java @@ -17,13 +17,19 @@ package org.sufficientlysecure.keychain.ui.util; +import android.animation.AnimatorInflater; +import android.animation.ArgbEvaluator; import android.animation.Keyframe; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.annotation.TargetApi; +import android.content.Context; import android.os.Build.VERSION_CODES; import android.view.View; +import org.sufficientlysecure.keychain.R; + + @TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH) /** Simple animation helper for subtle attention seeker stuff. * @@ -36,6 +42,10 @@ public class SubtleAttentionSeeker { } public static ObjectAnimator tada(View view, float shakeFactor) { + return tada(view, shakeFactor, 1400); + } + + public static ObjectAnimator tada(View view, float shakeFactor, int duration) { PropertyValuesHolder pvhScaleX = PropertyValuesHolder.ofKeyframe(View.SCALE_X, Keyframe.ofFloat(0f, 1f), @@ -80,7 +90,19 @@ public class SubtleAttentionSeeker { ); return ObjectAnimator.ofPropertyValuesHolder(view, pvhScaleX, pvhScaleY, pvhRotate). - setDuration(1400); + setDuration(duration); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + public static ObjectAnimator tintBackground(View view, int duration) { + return ObjectAnimator.ofArgb(view, "backgroundColor", + 0x00FF0000, 0x33FF0000, 0x00FF0000).setDuration(duration); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + public static ObjectAnimator tintText(View view, int duration) { + return ObjectAnimator.ofArgb(view, "backgroundColor", + 0x00FF7F00, 0x33FF7F00, 0x00FF7F00).setDuration(duration); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java index 75a0d1ea5..375483d89 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/ThemeChanger.java @@ -40,9 +40,9 @@ public class ThemeChanger { // hack to get holo design (which is not automatically applied due to activity's // Theme.NoDisplay) if (Constants.Pref.Theme.DARK.equals(preferences.getTheme())) { - return new ContextThemeWrapper(context, R.style.Theme_AppCompat_Dialog); + return new ContextThemeWrapper(context, R.style.Theme_Keychain_Dark); } else { - return new ContextThemeWrapper(context, R.style.Theme_AppCompat_Light_Dialog); + return new ContextThemeWrapper(context, R.style.Theme_Keychain_Light); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java new file mode 100644 index 000000000..c413a00be --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertListWidget.java @@ -0,0 +1,143 @@ +package org.sufficientlysecure.keychain.ui.widget; + +import java.util.Date; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.ViewAnimator; + +import org.ocpsoft.prettytime.PrettyTime; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.Certs; + +public class CertListWidget extends ViewAnimator + implements LoaderManager.LoaderCallbacks<Cursor> { + + public static final int LOADER_ID_LINKED_CERTS = 38572; + + public static final String ARG_URI = "uri"; + public static final String ARG_IS_SECRET = "is_secret"; + + + // These are the rows that we will retrieve. + static final String[] CERTS_PROJECTION = new String[]{ + KeychainContract.Certs._ID, + KeychainContract.Certs.MASTER_KEY_ID, + KeychainContract.Certs.VERIFIED, + KeychainContract.Certs.TYPE, + KeychainContract.Certs.RANK, + KeychainContract.Certs.KEY_ID_CERTIFIER, + KeychainContract.Certs.USER_ID, + KeychainContract.Certs.SIGNER_UID, + KeychainContract.Certs.CREATION + }; + public static final int INDEX_MASTER_KEY_ID = 1; + public static final int INDEX_VERIFIED = 2; + public static final int INDEX_TYPE = 3; + public static final int INDEX_RANK = 4; + public static final int INDEX_KEY_ID_CERTIFIER = 5; + public static final int INDEX_USER_ID = 6; + public static final int INDEX_SIGNER_UID = 7; + public static final int INDEX_CREATION = 8; + + private TextView vCollapsed; + private ListView vExpanded; + private View vExpandButton; + private boolean mIsSecret; + + public CertListWidget(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + View root = getRootView(); + vCollapsed = (TextView) root.findViewById(R.id.cert_collapsed_list); + vExpanded = (ListView) root.findViewById(R.id.cert_expanded_list); + vExpandButton = root.findViewById(R.id.cert_expand_button); + + // for now + vExpandButton.setVisibility(View.GONE); + vExpandButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + toggleExpanded(); + } + }); + + // vExpanded.setAdapter(null); + + } + + void toggleExpanded() { + setDisplayedChild(getDisplayedChild() == 1 ? 0 : 1); + } + + void setExpanded(boolean expanded) { + setDisplayedChild(expanded ? 1 : 0); + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + Uri uri = args.getParcelable(ARG_URI); + mIsSecret = args.getBoolean(ARG_IS_SECRET, false); + return new CursorLoader(getContext(), uri, + CERTS_PROJECTION, null, null, null); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + + if (data == null || !data.moveToFirst()) { + return; + } + + // TODO support external certificates + Date userCert = null; + while (!data.isAfterLast()) { + + int verified = data.getInt(INDEX_VERIFIED); + Date creation = new Date(data.getLong(INDEX_CREATION) * 1000); + + if (verified == Certs.VERIFIED_SECRET) { + if (userCert == null || userCert.after(creation)) { + userCert = creation; + } + } + + data.moveToNext(); + } + + if (userCert != null) { + PrettyTime format = new PrettyTime(); + if (mIsSecret) { + vCollapsed.setText("You created this identity " + + format.format(userCert) + "."); + } else { + vCollapsed.setText("You verified and confirmed this identity " + + format.format(userCert) + "."); + } + } else { + vCollapsed.setText("This identity is not yet verified or confirmed."); + } + + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + setVisibility(View.GONE); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java index 6cd33aada..6a51085f3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CertifyKeySpinner.java @@ -17,25 +17,24 @@ package org.sufficientlysecure.keychain.ui.widget; - -import java.util.Arrays; -import java.util.List; - import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.support.annotation.StringRes; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.util.AttributeSet; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; public class CertifyKeySpinner extends KeySpinner { private long mHiddenMasterKeyId = Constants.key.none; + private boolean mIsSingle; public CertifyKeySpinner(Context context) { super(context); @@ -94,9 +93,11 @@ public class CertifyKeySpinner extends KeySpinner { if (!data.isNull(mIndexHasCertify)) { if (selection == -1) { selection = data.getPosition() + 1; + mIsSingle = true; } else { // if selection is already set, we have more than one certify key! // get back to "none"! + mIsSingle = false; selection = 0; } } @@ -106,6 +107,9 @@ public class CertifyKeySpinner extends KeySpinner { } } + public boolean isSingleEntry() { + return mIsSingle && getSelectedItemPosition() != 0; + } @Override boolean isItemEnabled(Cursor cursor) { @@ -128,4 +132,10 @@ public class CertifyKeySpinner extends KeySpinner { return true; } + @Override + public @StringRes int getNoneString() { + return R.string.choice_select_cert; + } + + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java index 1884daf12..04ed35deb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/KeySpinner.java @@ -23,6 +23,7 @@ import android.database.Cursor; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; +import android.support.annotation.StringRes; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; @@ -34,6 +35,7 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.SpinnerAdapter; +import android.widget.TextView; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; @@ -117,8 +119,7 @@ public abstract class KeySpinner extends AppCompatSpinner implements if (getContext() instanceof FragmentActivity) { ((FragmentActivity) getContext()).getSupportLoaderManager().restartLoader(LOADER_ID, null, this); } else { - throw new AssertionError("KeySpinner must be attached to FragmentActivity, this is " - + getContext().getClass()); + // ignore, this happens during preview! we use fragmentactivities everywhere either way } } @@ -226,9 +227,11 @@ public abstract class KeySpinner extends AppCompatSpinner implements return inner.getView(position -1, convertView, parent); } - return convertView != null ? convertView : + View view = convertView != null ? convertView : LayoutInflater.from(getContext()).inflate( R.layout.keyspinner_item_none, parent, false); + ((TextView) view.findViewById(R.id.keyspinner_key_name)).setText(getNoneString()); + return view; } } @@ -259,4 +262,9 @@ public abstract class KeySpinner extends AppCompatSpinner implements bundle.putLong(ARG_KEY_ID, getSelectedKeyId()); return bundle; } + + public @StringRes int getNoneString() { + return R.string.cert_none; + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PrefixedEditText.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PrefixedEditText.java new file mode 100644 index 000000000..3cbb114e8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/PrefixedEditText.java @@ -0,0 +1,45 @@ +package org.sufficientlysecure.keychain.ui.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.*; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.widget.EditText; + +import org.sufficientlysecure.keychain.R; + +public class PrefixedEditText extends EditText { + + private String mPrefix; + private Rect mPrefixRect = new Rect(); + + public PrefixedEditText(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray style = context.getTheme().obtainStyledAttributes( + attrs, R.styleable.PrefixedEditText, 0, 0); + mPrefix = style.getString(R.styleable.PrefixedEditText_prefix); + if (mPrefix == null) { + mPrefix = ""; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + getPaint().getTextBounds(mPrefix, 0, mPrefix.length(), mPrefixRect); + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + canvas.drawText(mPrefix, super.getCompoundPaddingLeft(), getBaseline(), getPaint()); + } + + @Override + public int getCompoundPaddingLeft() { + return super.getCompoundPaddingLeft() + mPrefixRect.width(); + } + +}
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/StatusIndicator.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/StatusIndicator.java new file mode 100644 index 000000000..2784ac5f0 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/StatusIndicator.java @@ -0,0 +1,45 @@ +package org.sufficientlysecure.keychain.ui.widget; + + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.animation.AnimationUtils; + +import org.sufficientlysecure.keychain.R; + + +public class StatusIndicator extends ToolableViewAnimator { + + public enum Status { + IDLE, PROGRESS, OK, ERROR + } + + public StatusIndicator(Context context) { + super(context); + + LayoutInflater.from(context).inflate(R.layout.status_indicator, this, true); + setInAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_in)); + setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_out)); + } + + public StatusIndicator(Context context, AttributeSet attrs) { + super(context, attrs); + + LayoutInflater.from(context).inflate(R.layout.status_indicator, this, true); + setInAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_in)); + setOutAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_out)); + } + + @Override + public void setDisplayedChild(int whichChild) { + if (whichChild != getDisplayedChild()) { + super.setDisplayedChild(whichChild); + } + } + + public void setDisplayedChild(Status status) { + setDisplayedChild(status.ordinal()); + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java index 5f2329170..45dc33906 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ExportHelper.java @@ -20,6 +20,8 @@ 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; @@ -67,7 +69,7 @@ public class ExportHelper : R.string.specify_backup_dest_single); } - FileHelper.saveFile(new FileHelper.FileDialogCallback() { + FileHelper.saveDocumentDialog(new FileHelper.FileDialogCallback() { @Override public void onFileSelected(File file, boolean checked) { mExportFile = file; @@ -98,7 +100,10 @@ public class ExportHelper } @Override - public void onCryptoOperationSuccess(ExportResult result) { + 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(); } 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 4a00f46cb..9fb362412 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FileHelper.java @@ -28,16 +28,19 @@ 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.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; @@ -46,42 +49,95 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; 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. + * + * There are three entry points here: openDocument, saveDocument and + * saveDocumentDialog. Each behaves a little differently depending on whether + * the Android version used is pre or post KitKat. + * + * - openDocument queries for a document for reading. Used in "open encrypted + * file" ui flow. On pre-kitkat, this relies on an external file manager, + * and will fail with a toast message if none is installed. + * + * - saveDocument queries for a document name for saving. on pre-kitkat, this + * shows a dialog where a filename can be input. on kitkat and up, it + * directly triggers a "save document" intent. Used in "save encrypted file" + * ui flow. + * + * - saveDocumentDialog queries for a document. this shows a dialog on all + * versions of android. the browse button opens an external browser on + * pre-kitkat or the "save document" intent on post-kitkat devices. Used in + * "backup key" ui flow. + * + * It is noteworthy that the "saveDocument" call is essentially substituted + * by the "saveDocumentDialog" on pre-kitkat devices. + * + */ public class FileHelper { - /** - * Checks if external storage is mounted if file is located on external storage - * - * @return true if storage is mounted - */ - public static boolean isStorageMounted(String file) { - if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) { - if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - return false; - } + 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); } + } - return true; + 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 preferred installed file manager on Android and shows a toast if no manager is - * installed. - * - * @param last default selected Uri, not supported by all file managers - * @param mimeType can be text/plain for example - * @param requestCode requestCode used to identify the result coming back from file manager to - * onActivityResult() in your activity - */ - public static void openFile(Fragment fragment, Uri last, String mimeType, int 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); + } else { + saveDocumentKitKat(fragment, mimeType, targetName, 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( + Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); - + if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple); + } intent.setData(last); intent.setType(mimeType); @@ -92,11 +148,34 @@ public class FileHelper { Toast.makeText(fragment.getActivity(), R.string.no_filemanager_installed, Toast.LENGTH_SHORT).show(); } + + } + + /** Opens the storage browser on Android 4.4 or later for opening a file */ + @TargetApi(Build.VERSION_CODES.KITKAT) + private static void openDocumentKitKat(Fragment fragment, String mimeType, boolean multiple, int requestCode) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(mimeType); + 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 saveFile(final FileDialogCallback callback, final FragmentManager fragmentManager, - final String title, final String message, final File defaultFile, - final String checkMsg) { + 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 @@ -123,61 +202,6 @@ public class FileHelper { }); } - public static void saveFile(Fragment fragment, String title, String message, File defaultFile, int requestCode) { - saveFile(fragment, title, message, defaultFile, requestCode, null); - } - - public static void saveFile(final Fragment fragment, String title, String message, File defaultFile, - final int requestCode, String checkMsg) { - saveFile(new FileDialogCallback() { - @Override - public void onFileSelected(File file, boolean checked) { - Intent intent = new Intent(); - intent.setData(Uri.fromFile(file)); - fragment.onActivityResult(requestCode, Activity.RESULT_OK, intent); - } - }, fragment.getActivity().getSupportFragmentManager(), title, message, defaultFile, checkMsg); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void openDocument(Fragment fragment, String mimeType, int requestCode) { - openDocument(fragment, mimeType, false, requestCode); - } - - /** - * Opens the storage browser on Android 4.4 or later for opening a file - * - * @param mimeType can be text/plain for example - * @param multiple allow file chooser to return multiple files - * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your - */ - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void openDocument(Fragment fragment, String mimeType, boolean multiple, int requestCode) { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType(mimeType); - 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 - * - * @param mimeType can be text/plain for example - * @param suggestedName a filename desirable for the file to be saved - * @param requestCode used to identify the result coming back from storage browser onActivityResult() in your - */ - @TargetApi(Build.VERSION_CODES.KITKAT) - public static void saveDocument(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 String getFilename(Context context, Uri uri) { String filename = null; try { @@ -298,6 +322,17 @@ public class FileHelper { } } + /** 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())) { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + return false; + } + } + + return true; + } + public interface FileDialogCallback { void onFileSelected(File file, boolean checked); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java new file mode 100644 index 000000000..ab73f59b8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/FilterCursorWrapper.java @@ -0,0 +1,76 @@ +package org.sufficientlysecure.keychain.util; + + +import android.database.Cursor; +import android.database.CursorWrapper; + +public abstract class FilterCursorWrapper extends CursorWrapper { + private int[] mIndex; + private int mCount = 0; + private int mPos = 0; + + public abstract boolean isVisible(Cursor cursor); + + public FilterCursorWrapper(Cursor cursor) { + super(cursor); + mCount = super.getCount(); + mIndex = new int[mCount]; + for (int i = 0; i < mCount; i++) { + super.moveToPosition(i); + if (isVisible(cursor)) { + mIndex[mPos++] = i; + } + } + mCount = mPos; + mPos = 0; + super.moveToFirst(); + } + + @Override + public boolean move(int offset) { + return this.moveToPosition(mPos + offset); + } + + @Override + public boolean moveToNext() { + return this.moveToPosition(mPos + 1); + } + + @Override + public boolean moveToPrevious() { + return this.moveToPosition(mPos - 1); + } + + @Override + public boolean moveToFirst() { + return this.moveToPosition(0); + } + + @Override + public boolean moveToLast() { + return this.moveToPosition(mCount - 1); + } + + @Override + public boolean moveToPosition(int position) { + if (position >= mCount || position < 0) { + return false; + } + return super.moveToPosition(mIndex[position]); + } + + @Override + public int getCount() { + return mCount; + } + + public int getHiddenCount() { + return super.getCount() - mCount; + } + + @Override + public int getPosition() { + return mPos; + } + +}
\ No newline at end of file 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 0596b0079..4ef215036 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -25,7 +25,7 @@ import android.content.res.Resources; import android.preference.PreferenceManager; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Constants.Pref; -import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; import java.net.Proxy; import java.util.ArrayList; @@ -285,16 +285,19 @@ public class Preferences { } public Proxy.Type getProxyType() { - final String typeHttp = mResources.getString(R.string.pref_proxy_type_value_http); - final String typeSocks = mResources.getString(R.string.pref_proxy_type_value_socks); + final String typeHttp = Pref.ProxyType.TYPE_HTTP; + final String typeSocks = Pref.ProxyType.TYPE_SOCKS; String type = mSharedPreferences.getString(Pref.PROXY_TYPE, typeHttp); - if (type.equals(typeHttp)) return Proxy.Type.HTTP; - else if (type.equals(typeSocks)) return Proxy.Type.SOCKS; - else { // shouldn't happen - Log.e(Constants.TAG, "Invalid Proxy Type in preferences"); - return null; + switch (type) { + case typeHttp: + return Proxy.Type.HTTP; + case typeSocks: + return Proxy.Type.SOCKS; + default: // shouldn't happen + Log.e(Constants.TAG, "Invalid Proxy Type in preferences"); + return null; } } @@ -306,7 +309,7 @@ public class Preferences { return new ProxyPrefs(true, false, Constants.Orbot.PROXY_HOST, Constants.Orbot.PROXY_PORT, Constants.Orbot.PROXY_TYPE); } else if (useNormalProxy) { - return new ProxyPrefs(useTor, useNormalProxy, getProxyHost(), getProxyPort(), getProxyType()); + return new ProxyPrefs(false, true, getProxyHost(), getProxyPort(), getProxyType()); } else { return new ProxyPrefs(false, false, null, -1, null); } @@ -331,7 +334,7 @@ public class Preferences { } } - // proxy preference functions ends here + // cloud prefs public CloudSearchPrefs getCloudSearchPrefs() { return new CloudSearchPrefs(mSharedPreferences.getBoolean(Pref.SEARCH_KEYSERVER, true), @@ -356,7 +359,39 @@ public class Preferences { } } - public void upgradePreferences() { + // experimental prefs + + public void setExperimentalEnableWordConfirm(boolean enableWordConfirm) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM, enableWordConfirm); + editor.commit(); + } + + public boolean getExperimentalEnableWordConfirm() { + return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_WORD_CONFIRM, false); + } + + public void setExperimentalEnableLinkedIdentities(boolean enableLinkedIdentities) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES, enableLinkedIdentities); + editor.commit(); + } + + public boolean getExperimentalEnableLinkedIdentities() { + return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_LINKED_IDENTITIES, false); + } + + public void setExperimentalEnableKeybase(boolean enableKeybase) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(Pref.EXPERIMENTAL_ENABLE_KEYBASE, enableKeybase); + editor.commit(); + } + + public boolean getExperimentalEnableKeybase() { + return mSharedPreferences.getBoolean(Pref.EXPERIMENTAL_ENABLE_KEYBASE, false); + } + + public void upgradePreferences(Context context) { if (mSharedPreferences.getInt(Constants.Pref.PREF_DEFAULT_VERSION, 0) != Constants.Defaults.PREF_VERSION) { switch (mSharedPreferences.getInt(Constants.Pref.PREF_DEFAULT_VERSION, 0)) { @@ -394,6 +429,10 @@ public class Preferences { } // fall through case 5: { + KeyserverSyncAdapterService.enableKeyserverSync(context); + } + // fall through + case 6: { } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java index d5ee75f9b..d85ad9128 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/orbot/OrbotHelper.java @@ -361,7 +361,7 @@ public class OrbotHelper { .show(fragmentActivity.getSupportFragmentManager(), "OrbotHelperOrbotStartDialog"); } - }.startOrbotAndListen(fragmentActivity); + }.startOrbotAndListen(fragmentActivity, true); return false; } else { @@ -385,7 +385,8 @@ public class OrbotHelper { * activities wishing to respond to a change in Orbot state. */ public static void bestPossibleOrbotStart(final DialogActions dialogActions, - final Activity activity) { + final Activity activity, + boolean showProgress) { new SilentStartManager() { @Override @@ -397,23 +398,23 @@ public class OrbotHelper { protected void onSilentStartDisabled() { requestShowOrbotStart(activity); } - }.startOrbotAndListen(activity); + }.startOrbotAndListen(activity, showProgress); } /** * base class for listening to silent orbot starts. Also handles display of progress dialog. */ - private static abstract class SilentStartManager { + public static abstract class SilentStartManager { private ProgressDialog mProgressDialog; - public void startOrbotAndListen(Context context) { - mProgressDialog = new ProgressDialog(ThemeChanger.getDialogThemeWrapper(context)); - mProgressDialog.setMessage(context.getString(R.string.progress_starting_orbot)); - mProgressDialog.setCancelable(false); - mProgressDialog.show(); + public void startOrbotAndListen(final Context context, final boolean showProgress) { + Log.d(Constants.TAG, "starting orbot listener"); + if (showProgress) { + showProgressDialog(context); + } - BroadcastReceiver receiver = new BroadcastReceiver() { + final BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { switch (intent.getStringExtra(OrbotHelper.EXTRA_STATUS)) { @@ -423,14 +424,18 @@ public class OrbotHelper { new Handler().postDelayed(new Runnable() { @Override public void run() { - mProgressDialog.dismiss(); + if (showProgress) { + mProgressDialog.dismiss(); + } onOrbotStarted(); } }, 1000); break; case OrbotHelper.STATUS_STARTS_DISABLED: context.unregisterReceiver(this); - mProgressDialog.dismiss(); + if (showProgress) { + mProgressDialog.dismiss(); + } onSilentStartDisabled(); break; @@ -444,6 +449,13 @@ public class OrbotHelper { requestStartTor(context); } + private void showProgressDialog(Context context) { + mProgressDialog = new ProgressDialog(ThemeChanger.getDialogThemeWrapper(context)); + mProgressDialog.setMessage(context.getString(R.string.progress_starting_orbot)); + mProgressDialog.setCancelable(false); + mProgressDialog.show(); + } + protected abstract void onOrbotStarted(); protected abstract void onSilentStartDisabled(); |