package org.sufficientlysecure.keychain.linked; import android.content.Context; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; 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.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; public abstract class LinkedTokenResource extends LinkedResource { protected final URI mSubUri; protected final Set mFlags; protected final HashMap mParams; public static Pattern magicPattern = Pattern.compile("\\[Verifying my (?:Open|)?PGP key(?::|) openpgp4fpr:([a-zA-Z0-9]+)]"); protected LinkedTokenResource(Set flags, HashMap params, URI uri) { mFlags = flags; mParams = params; mSubUri = uri; } @SuppressWarnings("unused") public URI getSubUri () { return mSubUri; } public Set getFlags () { return new HashSet<>(mFlags); } public HashMap 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 flags = new HashSet<>(); HashMap 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 flags, HashMap 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 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; } } }