Java tutorial
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package com.cyphermessenger.client; import android.util.Log; import com.cyphermessenger.crypto.Decrypt; import com.cyphermessenger.crypto.ECKey; import com.cyphermessenger.crypto.Encrypt; import com.cyphermessenger.utils.Utils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.spongycastle.crypto.InvalidCipherTextException; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.*; public final class SyncRequest { final static String DOMAIN = "https://cyphermessenger.herokuapp.com/beta1/"; //final static String DOMAIN = "http://10.23.65.120:8080/"; final static ObjectMapper MAPPER = new ObjectMapper(); public static final boolean SINCE = true; public static final boolean UNTIL = false; public static Captcha requestCaptcha() throws IOException, APIErrorException { HttpURLConnection conn = doRequest("captcha"); if (conn.getResponseCode() != 200) { throw new IOException("Server error"); } InputStream in = conn.getInputStream(); JsonNode node = MAPPER.readTree(in); conn.disconnect(); int statusCode = node.get("status").asInt(); String captchaTokenString = node.get("captchaToken").asText(); String captchaHashString = node.get("captchaHash").asText(); String captchaImageString = node.get("captchaBytes").asText(); byte[] captchaHash = Utils.BASE64_URL.decode(captchaHashString); byte[] captchaImage = Utils.BASE64_URL.decode(captchaImageString); if (statusCode == StatusCode.OK) { return new Captcha(captchaTokenString, captchaHash, captchaImage); } else { throw new APIErrorException(statusCode); } } public static CypherUser registerUser(String username, String password, String captchaValue, Captcha captcha) throws IOException, APIErrorException { if (!captcha.verify(captchaValue)) { throw new APIErrorException(StatusCode.CAPTCHA_INVALID); } username = username.toLowerCase(); captchaValue = captchaValue.toLowerCase(); byte[] serverPassword = Utils.cryptPassword(password.getBytes(), username); byte[] localPassword = Utils.sha256(password); String serverPasswordEncoded = Utils.BASE64_URL.encode(serverPassword); ECKey key = new ECKey(); byte[] publicKey = key.getPublicKey(); byte[] privateKey = key.getPrivateKey(); try { privateKey = Encrypt.process(localPassword, privateKey); } catch (InvalidCipherTextException ex) { throw new RuntimeException(ex); } String[] keys = new String[] { "captchaToken", "captchaValue", "username", "password", "publicKey", "privateKey" }; String[] vals = new String[] { captcha.captchaToken, captchaValue, username, serverPasswordEncoded, Utils.BASE64_URL.encode(publicKey), Utils.BASE64_URL.encode(privateKey) }; HttpURLConnection conn = doRequest("register", null, keys, vals); if (conn.getResponseCode() != 200) { throw new IOException("Server error"); } InputStream in = conn.getInputStream(); JsonNode node = MAPPER.readTree(in); conn.disconnect(); int statusCode = node.get("status").asInt(); if (statusCode == StatusCode.OK) { long userID = node.get("userID").asLong(); long keyTime = node.get("timestamp").asLong(); key.setTime(keyTime); return new CypherUser(username, localPassword, serverPassword, userID, key, keyTime); } else { throw new APIErrorException(statusCode); } } public static CypherSession userLogin(String username, byte[] serverPassword, byte[] localPassword) throws IOException, APIErrorException { String passwordHashEncoded = Utils.BASE64_URL.encode(serverPassword); HttpURLConnection conn = doRequest("login", null, new String[] { "username", "password" }, new String[] { username, passwordHashEncoded }); if (conn.getResponseCode() != 200) { throw new IOException("Server error"); } InputStream in = conn.getInputStream(); JsonNode node = MAPPER.readTree(in); conn.disconnect(); int statusCode = node.get("status").asInt(); if (statusCode == StatusCode.OK) { long userID = node.get("userID").asLong(); ECKey key; try { key = Utils.decodeKey(node.get("publicKey").asText(), node.get("privateKey").asText(), localPassword); } catch (InvalidCipherTextException ex) { throw new RuntimeException(ex); } long keyTimestamp = node.get("keyTimestamp").asLong(); key.setTime(keyTimestamp); CypherUser newUser = new CypherUser(username, localPassword, serverPassword, userID, key, keyTimestamp); String sessionID = node.get("sessionID").asText(); return new CypherSession(newUser, sessionID); } else { throw new APIErrorException(statusCode); } } public static CypherSession userLogin(String username, String password) throws IOException, APIErrorException { username = username.toLowerCase(); byte[] serverPassword = Utils.cryptPassword(password.getBytes(), username); byte[] localPassword = Utils.sha256(password); return userLogin(username, serverPassword, localPassword); } public static CypherSession userLogin(CypherUser user) throws IOException, APIErrorException { return userLogin(user.getUsername(), user.getServerPassword(), user.getLocalPassword()); } public static void userLogout(CypherSession session) throws IOException, APIErrorException { HttpURLConnection conn = doRequest("logout", session, null); if (conn.getResponseCode() != 200) { throw new IOException("Server error"); } InputStream in = conn.getInputStream(); JsonNode node = MAPPER.readTree(in); conn.disconnect(); int statusCode = node.get("status").asInt(); if (statusCode != StatusCode.OK) { throw new APIErrorException(statusCode); } } public static List<String> findUser(CypherSession session, String username, int limit) throws IOException, APIErrorException { String[] keys = new String[] { "username", "limit" }; String[] vals = new String[] { username, limit + "" }; HttpURLConnection conn = doRequest("find", session, keys, vals); if (conn.getResponseCode() != 200) { throw new IOException("Server error"); } JsonNode node = MAPPER.readTree(conn.getInputStream()); conn.disconnect(); int statusCode = node.get("status").asInt(); if (statusCode != StatusCode.OK) { throw new APIErrorException(statusCode); } else { return Utils.MAPPER.treeToValue(node.get("users"), ArrayList.class); } } public static List<String> findUser(CypherSession session, String username) throws IOException, APIErrorException { return findUser(session, username, 10); } public static void sendMessage(CypherSession session, CypherUser contactUser, CypherMessage message) throws IOException, APIErrorException { CypherUser user = session.getUser(); byte[] timestampBytes = Utils.longToBytes(message.getTimestamp()); int messageIDLong = message.getMessageID(); Encrypt encryptionCtx = new Encrypt(user.getKey().getSharedSecret(contactUser.getKey())); encryptionCtx.updateAuthenticatedData(Utils.longToBytes(messageIDLong)); encryptionCtx.updateAuthenticatedData(timestampBytes); byte[] payload; try { payload = encryptionCtx.process(message.getText().getBytes()); } catch (InvalidCipherTextException e) { throw new RuntimeException(e); } HashMap<String, String> pairs = new HashMap<>(6); pairs.put("payload", Utils.BASE64_URL.encode(payload)); pairs.put("contactID", contactUser.getUserID() + ""); pairs.put("messageID", messageIDLong + ""); pairs.put("messageTimestamp", message.getTimestamp() + ""); pairs.put("userKeyTimestamp", session.getUser().getKeyTime() + ""); pairs.put("contactKeyTimestamp", contactUser.getKeyTime() + ""); HttpURLConnection conn = doRequest("message", session, pairs); if (conn.getResponseCode() != 200) { throw new IOException("Server error"); } InputStream in = conn.getInputStream(); JsonNode node = MAPPER.readTree(in); conn.disconnect(); int statusCode = node.get("status").asInt(); if (statusCode != StatusCode.OK) { throw new APIErrorException(statusCode); } } private static CypherContact manageContact(CypherSession session, String contactName, boolean add) throws IOException, APIErrorException { String action = "block"; if (add) { action = "add"; } String[] keys = new String[] { "action", "contactName" }; String[] vals = new String[] { action, contactName }; HttpURLConnection conn = doRequest("contact", session, keys, vals); if (conn.getResponseCode() != 200) { throw new IOException("Server error"); } InputStream in = conn.getInputStream(); JsonNode node = MAPPER.readTree(in); conn.disconnect(); int statusCode = node.get("status").asInt(); if (statusCode == StatusCode.OK && add) { long userID = node.get("contactID").asLong(); long keyTimestamp = node.get("keyTimestamp").asLong(); long contactTimestamp = node.get("contactTimestamp").asLong(); ECKey key = Utils.decodeKey(node.get("publicKey").asText()); key.setTime(keyTimestamp); boolean isFirst = node.get("isFirst").asBoolean(); return new CypherContact(contactName, userID, key, keyTimestamp, CypherContact.ACCEPTED, contactTimestamp, isFirst); } else { boolean isFirst = node.get("isFirst").asBoolean(); String status; switch (statusCode) { case StatusCode.CONTACT_WAITING: status = CypherContact.WAITING; break; case StatusCode.OK: case StatusCode.CONTACT_BLOCKED: status = CypherContact.BLOCKED; break; case StatusCode.CONTACT_DENIED: status = CypherContact.DENIED; break; default: throw new APIErrorException(statusCode); } return new CypherContact(contactName, null, null, null, status, null, isFirst); } } public static CypherContact addContact(CypherSession session, String username) throws IOException, APIErrorException { return manageContact(session, username, true); } public static CypherContact blockContact(CypherSession session, String username) throws IOException, APIErrorException { return manageContact(session, username, false); } private static JsonNode pullUpdate(CypherSession session, CypherUser contact, String action, Boolean since, Long time) throws IOException, APIErrorException { String timeBoundary = "since"; if (since != null && since == UNTIL) { timeBoundary = "until"; } HashMap<String, String> pairs = new HashMap<>(3); pairs.put("action", action); if (contact != null) { pairs.put("contactID", contact.getUserID().toString()); } if (time != null) { pairs.put(timeBoundary, time.toString()); } HttpURLConnection conn = doRequest("pull", session, pairs); if (conn.getResponseCode() != 200) { throw new IOException("Server error"); } InputStream in = conn.getInputStream(); JsonNode node = MAPPER.readTree(in); conn.disconnect(); int statusCode = node.get("status").asInt(); if (statusCode == StatusCode.OK) { return node; } else { throw new APIErrorException(statusCode); } } public static PullResults pullMessages(CypherSession session, CypherUser contact, Boolean since, Long time) throws IOException, APIErrorException { JsonNode node = pullUpdate(session, contact, "messages", since, time); int statusCode = node.get("status").asInt(); if (statusCode == StatusCode.OK) { ArrayList<CypherMessage> array = handleMessageNode(node, session.getUser().getKey(), contact.getKey()); return new PullResults(array, null, null, node.get("notifiedUntil").asLong()); } else { throw new APIErrorException(statusCode); } } public static PullResults pullContacts(CypherSession session, Boolean since, Long time) throws IOException, APIErrorException { JsonNode node = pullUpdate(session, null, "contacts", since, time); int statusCode = node.get("status").asInt(); if (statusCode == StatusCode.OK) { ArrayList<CypherContact> array = handleContactNode(node); return new PullResults(null, array, null, node.get("notifiedUntil").asLong()); } else { throw new APIErrorException(statusCode); } } public static PullResults pullKeys(CypherSession session, CypherUser contact, Boolean since, Long time) throws IOException, APIErrorException { JsonNode node = pullUpdate(session, contact, "keys", since, time); int statusCode = node.get("status").asInt(); if (statusCode == StatusCode.OK) { return new PullResults(null, null, handleKeyNode(node, contact == null ? session.getUser().getLocalPassword() : null), node.get("notifiedUntil").asLong()); } else { throw new APIErrorException(statusCode); } } public static PullResults pullAll(CypherSession session, CypherUser contact, Boolean since, Long time) throws IOException, APIErrorException { JsonNode node = pullUpdate(session, contact, "all", since, time); int statusCode = node.get("status").asInt(); if (statusCode == StatusCode.OK) { ArrayList<ECKey> keysArray = handleKeyNode(node, contact == null ? session.getUser().getLocalPassword() : null); ArrayList<CypherContact> contactsArray = handleContactNode(node); ArrayList<CypherMessage> messagesArray = handleMessageNode(node, session.getUser().getKey(), contact != null ? contact.getKey() : null); long notifiedUntil = node.get("notifiedUntil").asLong(); return new PullResults(messagesArray, contactsArray, keysArray, notifiedUntil); } else { throw new APIErrorException(statusCode); } } private static ArrayList<CypherMessage> handleMessageNode(JsonNode node, ECKey key1, ECKey key2) { ArrayList<CypherMessage> array = new ArrayList<>(); JsonNode arrayNode = node.get("messages"); if (arrayNode.isArray()) { for (JsonNode selectedNode : arrayNode) { boolean isSender = selectedNode.get("isSender").asBoolean(); long receivedContactID = selectedNode.get("contactID").asLong(); long timestamp = selectedNode.get("timestamp").asLong(); int messageID = selectedNode.get("messageID").asInt(); byte[] payload = Utils.BASE64_URL.decode(selectedNode.get("payload").asText()); String plainText = null; boolean handleDecryption = false; // try to decrypt message if (key2 != null) try { byte[] sharedSecret = key1.getSharedSecret(key2); plainText = new String(Decrypt.process(sharedSecret, payload, Utils.longToBytes(messageID), Utils.longToBytes(timestamp))); handleDecryption = true; } catch (InvalidCipherTextException ex) { Log.e("PULL", "Message decryption failed", ex); } CypherMessage message; if (handleDecryption) { message = new CypherMessage(messageID, plainText, timestamp, isSender, receivedContactID); } else { message = new CypherMessage(messageID, payload, timestamp, isSender, receivedContactID); } array.add(message); } } return array; } private static ArrayList<ECKey> handleKeyNode(JsonNode node, byte[] localPassword) { ArrayList<ECKey> array = new ArrayList<>(); JsonNode arrayNode = node.get("keys"); if (arrayNode.isArray()) { for (JsonNode selectedNode : arrayNode) { try { ECKey newECKey = Utils.decodeKey(selectedNode.get("publicKey").asText(), selectedNode.get("privateKey").asText(), localPassword); newECKey.setTime(selectedNode.get("timestamp").asLong()); array.add(newECKey); } catch (InvalidCipherTextException ex) { Log.e("PULL", "Key decryption failed", ex); /** TODO * handle key decryption error */ } } } return array; } private static ArrayList<CypherContact> handleContactNode(JsonNode node) { ArrayList<CypherContact> array = new ArrayList<>(); JsonNode arrayNode = node.get("contacts"); if (arrayNode.isArray()) { for (JsonNode selectedNode : arrayNode) { long receivedContactID = selectedNode.get("contactID").asLong(); long timestamp = selectedNode.get("contactTimestamp").asLong(); long keyTime = selectedNode.get("keyTimestamp").asLong(); String username = selectedNode.get("username").asText(); String contactStatus = selectedNode.get("contactStatus").asText(); ECKey publicKey = Utils.decodeKey(selectedNode.get("publicKey").asText()); boolean isFirst = selectedNode.get("isFirst").asBoolean(); publicKey.setTime(keyTime); CypherContact newContact = new CypherContact(username, receivedContactID, publicKey, keyTime, contactStatus, timestamp, isFirst); array.add(newContact); } } return array; } public static HttpURLConnection doRequest(String endpoint, CypherSession session, String[] keys, String[] values) throws IOException { HttpURLConnection connection = (java.net.HttpURLConnection) new URL(DOMAIN + endpoint).openConnection(); connection.setDoOutput(true); connection.setRequestMethod("POST"); connection.setRequestProperty("Accept-Charset", "UTF-8"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); connection.connect(); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream())); if (session != null) { writer.write("userID="); writer.write(session.getUser().getUserID() + "&sessionID="); writer.write(session.getSessionID()); } if (keys != null && keys.length > 0) { if (session != null) { writer.write('&'); } // write first row writer.write(keys[0]); writer.write('='); writer.write(URLEncoder.encode(values[0], "UTF-8")); for (int i = 1; i < keys.length; i++) { writer.write('&'); writer.write(keys[i]); writer.write('='); writer.write(URLEncoder.encode(values[i], "UTF-8")); } } writer.close(); return connection; } public static HttpURLConnection doRequest(String endpoint, CypherSession session, Map<String, String> m) throws IOException { String[] keys = null; String[] vals = null; if (m != null) { keys = (String[]) m.keySet().toArray(new String[] {}); vals = (String[]) m.values().toArray(new String[] {}); } return doRequest(endpoint, session, keys, vals); } public static HttpURLConnection doRequest(String endpoint) throws IOException { return doRequest(endpoint, null, null, null); } }