Java tutorial
/* * Copyright (C) 2014 Tim Bray <tbray@textuality.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 com.textuality.keybase.lib.prover; import android.util.Base64; import com.textuality.keybase.lib.JWalk; import com.textuality.keybase.lib.KeybaseException; import com.textuality.keybase.lib.Proof; import com.textuality.keybase.lib.Search; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; /** * Supports Keybase proof verification. This is self-contained with no dependencies, except on * the caller. Keybase proof checking requires checking OpenPGP signatures and fetching DNS * TXT records, but libraries to do these things can be complex and heavyweight. Therefore * this function requires that the caller provide signature-checking and DNS-fetching functions. * * A note on signature checking. Keybase proofs are ASCII-armored OpenPGP compressed messages * with a signature. That means, in terms of OpenPGP packet types, the message contains a * Compressed Data packet (tag=8). That in turn contains a one-pass signature packet (tag=4), * a Literal Data packet (tag=11), and a signature packet (tag=2). You need to do the * equivalent of pgp --decrypt, validating that the signature is correct and the signing key * is the one the proof concerns. * * How to use: * 1. call fetchProofData(), which will exhibit network latency. If it returns false the proof * verification failed; an explanation can be found in the log. * 2. fetch the PGP message with getPgpMessage(), check that its signed with the right fingerprint * (see above). * 3. Call dnsTxtCheckRequired() and if it returns non-null, the return value is a domain name; * retrieve TXT records from that domain and pass them to checkDnsTxt(); if it returns false * the proof verification failed; an explanation can be found in the log. * 4. call rawMessageCheckRequired() and if it returns true, feed the raw (de-armored) bytes * of the message to checkRawMessageBytes(). if it returns false the proof verification failed; * an explanation can be found in the log. This may exhibit crypto latency. * 5. Pass the message to validate(), which should have no real latency. If it returns false the * proof verification failed; an explanation can be found in the log. */ public abstract class Prover { String mPgpMessage; String mPayload; String mShortenedMessageHash; final Proof mProof; final List<String> mLog = new ArrayList<String>(); public static Prover findProverFor(Proof proof) { switch (proof.getType()) { case Proof.PROOF_TYPE_TWITTER: return new Twitter(proof); case Proof.PROOF_TYPE_GITHUB: return new GitHub(proof); case Proof.PROOF_TYPE_DNS: return new DNS(proof); case Proof.PROOF_TYPE_WEB_SITE: return new Website(proof); case Proof.PROOF_TYPE_HACKERNEWS: return new HackerNews(proof); case Proof.PROOF_TYPE_COINBASE: return new Coinbase(proof); case Proof.PROOF_TYPE_REDDIT: return new Reddit(proof); default: return null; } } public Prover(Proof proof) { mProof = proof; } abstract public boolean fetchProofData(); public String getPgpMessage() { return mPgpMessage; } public boolean validate(String decryptedMessage) { return mPayload.equals(decryptedMessage); } public List<String> getLog() { return mLog; } JSONObject readSig(String sigId) throws JSONException, KeybaseException { // fetch the sig JSONObject sigJSON = Search.getFromKeybase("_/api/1.0/sig/get.json?sig_id=", sigId); mLog.add("Successfully retrieved sig from Keybase"); sigJSON = JWalk.getArray(sigJSON, "sigs").getJSONObject(0); mPayload = JWalk.getString(sigJSON, "payload_json"); mPgpMessage = JWalk.getString(sigJSON, "sig"); mLog.add("Extracted payload & message from sig"); return sigJSON; } public boolean rawMessageCheckRequired() { return false; } public boolean checkRawMessageBytes(InputStream in) { try { MessageDigest digester = MessageDigest.getInstance("SHA-256"); byte[] buffer = new byte[8192]; int byteCount; while ((byteCount = in.read(buffer)) > 0) { digester.update(buffer, 0, byteCount); } String digest = Base64.encodeToString(digester.digest(), Base64.URL_SAFE); if (!digest.startsWith(mShortenedMessageHash)) { mLog.add("Proof post doesnt contain correct encoded message."); return false; } return true; } catch (NoSuchAlgorithmException e) { mLog.add("SHA-256 not available"); } catch (IOException e) { mLog.add("Error checking raw message: " + e.getLocalizedMessage()); } return false; } public String dnsTxtCheckRequired() { return null; } public boolean checkDnsTxt(List<List<byte[]>> records) { return false; } /* A proof narrative needs the following strings: * a Url for the actual proof document (may be null, e.g. for DNS) * a Url for the persons presence at the service e.g. https://twitter.com/timbray * a name for the person's presence, e.g. twitter.com/timbray */ public String getProofUrl() throws KeybaseException { return mProof.getHumanUrl(); } public String getPresenceUrl() throws KeybaseException { return mProof.getServiceUrl(); } public String getPresenceLabel() throws KeybaseException { String answer = mProof.getServiceUrl(); try { URL u = new URL(answer); answer = u.getHost() + u.getPath(); } catch (MalformedURLException e) { } return answer; } }