aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked
diff options
context:
space:
mode:
authorVincent Breitmoser <valodim@mugenguild.com>2015-05-09 12:24:48 +0200
committerVincent Breitmoser <valodim@mugenguild.com>2015-05-09 12:24:48 +0200
commit4378f8f871f6a47321352f90a59cfaad7f52279b (patch)
tree1283eb8a92ce1eeea64ab09c21f13e644569ac91 /OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked
parent39382e978f554a8da0ee7698bd84cbb2023b186d (diff)
downloadopen-keychain-4378f8f871f6a47321352f90a59cfaad7f52279b.tar.gz
open-keychain-4378f8f871f6a47321352f90a59cfaad7f52279b.tar.bz2
open-keychain-4378f8f871f6a47321352f90a59cfaad7f52279b.zip
linked-ids: code cleanup, handle all lint errors
Diffstat (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked')
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedCookieResource.java282
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedIdentity.java32
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedResource.java25
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/RawLinkedIdentity.java79
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/DnsResource.java130
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java94
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java217
-rw-r--r--OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java244
8 files changed, 1103 insertions, 0 deletions
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedCookieResource.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedCookieResource.java
new file mode 100644
index 000000000..d09854583
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedCookieResource.java
@@ -0,0 +1,282 @@
+package org.sufficientlysecure.keychain.linked;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+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 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;
+
+
+public abstract class LinkedCookieResource 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 LinkedCookieResource(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 LinkedCookieResource fromUri (URI uri) {
+
+ if (!"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 LinkedCookieResource findResourceType (Set<String> flags,
+ HashMap<String,String> params,
+ URI subUri) {
+
+ LinkedCookieResource 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+cookie:");
+
+ // 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(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(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 (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(HttpRequestBase request) throws IOException, HttpStatusException {
+ StringBuilder sb = new StringBuilder();
+
+ request.setHeader("User-Agent", "Open Keychain");
+
+ DefaultHttpClient httpClient = new DefaultHttpClient(new BasicHttpParams());
+ 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/LinkedIdentity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedIdentity.java
new file mode 100644
index 000000000..af19aefdd
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/LinkedIdentity.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 LinkedIdentity extends RawLinkedIdentity {
+
+ public final LinkedResource mResource;
+
+ protected LinkedIdentity(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/RawLinkedIdentity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/RawLinkedIdentity.java
new file mode 100644
index 000000000..0bdc1b280
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/RawLinkedIdentity.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 RawLinkedIdentity {
+
+ public final URI mUri;
+
+ protected RawLinkedIdentity(URI uri) {
+ mUri = uri;
+ }
+
+ public byte[] getEncoded() {
+ return Strings.toUTF8ByteArray(mUri.toASCIIString());
+ }
+
+ public static RawLinkedIdentity 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 RawLinkedIdentity fromSubpacketData(byte[] data) {
+
+ try {
+ String uriStr = Strings.fromUTF8ByteArray(data);
+ URI uri = URI.create(uriStr);
+
+ LinkedResource res = LinkedCookieResource.fromUri(uri);
+ if (res == null) {
+ return new RawLinkedIdentity(uri);
+ }
+
+ return new LinkedIdentity(uri, res);
+
+ } catch (IllegalArgumentException e) {
+ Log.e(Constants.TAG, "error parsing uri in (suspected) linked id packet");
+ return null;
+ }
+ }
+
+ public static RawLinkedIdentity fromResource (LinkedCookieResource res) {
+ return new RawLinkedIdentity(res.toUri());
+ }
+
+
+ public WrappedUserAttribute toUserAttribute () {
+ return WrappedUserAttribute.fromSubpacket(WrappedUserAttribute.UAT_LINKED_ID, getEncoded());
+ }
+
+ public @DrawableRes int getDisplayIcon() {
+ return R.drawable.ic_warning_grey_24dp;
+ }
+
+ public String getDisplayTitle(Context context) {
+ return "unknown";
+ }
+
+ 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..d63cafcc2
--- /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.LinkedCookieResource;
+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 LinkedCookieResource {
+
+ final static Pattern magicPattern =
+ Pattern.compile("openpgpid\\+cookie=([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("openpgpid+cookie=%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);
+ 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 (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..55f998952
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GenericHttpsResource.java
@@ -0,0 +1,94 @@
+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.LinkedCookieResource;
+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 LinkedCookieResource {
+
+ GenericHttpsResource(Set<String> flags, HashMap<String,String> params, URI uri) {
+ super(flags, params, uri);
+ }
+
+ public static String generateText (Context context, byte[] fingerprint) {
+ String cookie = LinkedCookieResource.generate(fingerprint);
+
+ return String.format(context.getResources().getString(R.string.linked_id_generic_text),
+ cookie, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24));
+ }
+
+ @SuppressWarnings("deprecation") // HttpGet is deprecated
+ @Override
+ protected String fetchResource (OperationLog log, int indent) throws HttpStatusException, IOException {
+
+ log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
+ HttpGet httpGet = new HttpGet(mSubUri);
+ return getResponseBody(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..078328198
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/GithubResource.java
@@ -0,0 +1,217 @@
+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.LinkedCookieResource;
+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 LinkedCookieResource {
+
+ 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 cookie = LinkedCookieResource.generate(fingerprint);
+
+ return String.format(context.getResources().getString(R.string.linked_id_github_text), cookie);
+ }
+
+ @SuppressWarnings("deprecation") // HttpGet is deprecated
+ @Override
+ protected String fetchResource (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(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;
+
+ }
+
+ @SuppressWarnings("deprecation")
+ public static GithubResource searchInGithubStream(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 cookie 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(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(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..c981e2bd6
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/linked/resources/TwitterResource.java
@@ -0,0 +1,244 @@
+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.LinkedCookieResource;
+
+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 LinkedCookieResource {
+
+ 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(OperationLog log, int indent) throws IOException, HttpStatusException,
+ JSONException {
+
+ String authToken;
+ try {
+ authToken = getAuthToken();
+ } 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(httpGet);
+ 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(
+ String screenName, String needle, OperationLog log) {
+
+ String authToken;
+ try {
+ authToken = getAuthToken();
+ } 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(httpGet);
+ 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() 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(httpPost));
+
+ // 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();
+ }
+
+}