/* * Copyright 2013 mpowers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.trsst; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.KeyFactory; import java.security.KeyManagementException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Security; import java.security.cert.X509Certificate; import java.security.spec.ECGenParameterSpec; import java.security.spec.X509EncodedKeySpec; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.jar.Attributes; import java.util.jar.Manifest; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.abdera.Abdera; import org.apache.abdera.model.Document; import org.apache.abdera.model.Element; import org.apache.commons.codec.binary.Base64; import org.apache.commons.httpclient.protocol.Protocol; import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; import org.apache.commons.lang3.StringEscapeUtils; import org.bouncycastle.crypto.digests.RIPEMD160Digest; import org.bouncycastle.util.encoders.Hex; import org.w3c.tidy.Tidy; import com.trsst.client.AnonymSSLSocketFactory; /** * Shared utilities and constants used by both clients and servers. Portions * borrowed from bitsofproof, abdera, and apache commons. * * @author mpowers */ public class Common { public static final String ROOT_ALIAS = "home"; public static final String ACCOUNT_PREFIX = "acct:"; public static final String ACCOUNT_URN_PREFIX = "urn:acct:"; public static final String ACCOUNT_URN_FEED_PREFIX = ":feed:"; public static final String FEED_URN_PREFIX = "urn:feed:"; public static final String ENTRY_URN_PREFIX = "urn:entry:"; public static final String URN_SEPARATOR = ":"; public static final String CURVE_NAME = "secp256k1"; public static final String NS_URI = "http://trsst.com/spec/0.1"; public static final String NS_ABBR = "trsst"; public static final String SIGN = "sign"; public static final String ENCRYPT = "encrypt"; public static final String MENTION_URN = "urn:mention"; public static final String MENTION_URN_LEGACY = "urn:com.trsst.mention"; public static final String TAG_URN = "urn:tag"; public static final String TAG_URN_LEGACY = "urn:com.trsst.tag"; public static final String PREDECESSOR = "predecessor"; public static final String ATTACHMENT_DIGEST = "digest"; public static final String PREDECESSOR_ID = "id"; public static final String KEY_EXTENSION = ".p12"; public static final String VERB_DELETE = "delete"; public static final String VERB_DELETED = "deleted"; public static final String STAMP = "stamp"; public static final int STAMP_BITS = 20; /** * Default public rights are like CC ND BY but with added right of * revocation. This lets you delete an entry and require takedown of that * entry whereever it has been distributed. */ public static final String RIGHTS_NDBY_REVOCABLE = "attribution, no derivatives, revoked if deleted"; // "You may copy, distribute, display and perform only verbatim copies of // the work, not derivative works based on it, and only if fully attributed // to the author. Your license to the work is revoked worldwide if the // author publicly deletes the original work."; /** * Default private rights are are explicity ARR if only to clearly * differentiate private posts from public ones. */ public static final String RIGHTS_RESERVED = "all reserved"; private final static org.slf4j.Logger log; static { log = org.slf4j.LoggerFactory.getLogger(Common.class); try { Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); } catch (Throwable t) { log.error( "Could not initialize security provider: " + t.getMessage(), t); } } /** * Hashes an elliptic curve public key into a shortened "satoshi-style" * string that we use for a publicly-readable account id. Borrowed from * bitsofproof. * * @param key * the account EC public key. * @return the account id */ public static String toFeedId(PublicKey key) { byte[] keyDigest = keyHash(key.getEncoded()); byte[] addressBytes = new byte[keyDigest.length + 4]; // note: now leaving out BTC's first byte identifier System.arraycopy(keyDigest, 0, addressBytes, 0, keyDigest.length); byte[] check = hash(addressBytes, 0, keyDigest.length); System.arraycopy(check, 0, addressBytes, keyDigest.length, 4); return toBase58(addressBytes); } public static File getClientRoot() { String path = System.getProperty("user.home", "."); File root = new File(path, "trsstd"); path = System.getProperty("com.trsst.client.storage"); if (path != null) { try { root = new File(path); root.mkdirs(); } catch (Throwable t) { System.err.println("Invalid path: " + path + " : " + t.getMessage()); } } return root; } public static File getServerRoot() { String path = System.getProperty("user.home", "."); File root = new File(path, "trsstd"); path = System.getProperty("com.trsst.server.storage"); if (path != null) { try { root = new File(path); root.mkdirs(); } catch (Throwable t) { System.err.println("Invalid path: " + path + " : " + t.getMessage()); } } return root; } public static final String toFeedIdString(Object feedOrEntryUrn) { String feedId = feedOrEntryUrn.toString(); if (feedId.startsWith(FEED_URN_PREFIX)) { feedId = feedId.substring(FEED_URN_PREFIX.length()); } else if (feedId.startsWith(ENTRY_URN_PREFIX)) { feedId = feedId.substring(ENTRY_URN_PREFIX.length()); feedId = feedId.substring(0, feedId.lastIndexOf(':')); } return feedId; } /** * Returns the shorthand alias for a full alias uri, for example: * 'acct:mpowers.trsst.com' becomes 'mpowers'. */ public static final String getShortAliasFromAliasUri(Object urn) { String aliasUri = urn.toString(); if (aliasUri.indexOf(ACCOUNT_URN_PREFIX) == 0) { aliasUri = aliasUri.substring(ACCOUNT_URN_PREFIX.length()); } if (aliasUri.indexOf(ACCOUNT_PREFIX) == 0) { aliasUri = aliasUri.substring(ACCOUNT_PREFIX.length()); } int i; // remove feed extension if any i = aliasUri.indexOf(ACCOUNT_URN_FEED_PREFIX); if (i != -1) { aliasUri = aliasUri.substring(0, i); } // remove last domain extension if any i = aliasUri.lastIndexOf("."); if (i != -1) { aliasUri = aliasUri.substring(0, i); } // do it again i = aliasUri.lastIndexOf("."); if (i != -1) { aliasUri = aliasUri.substring(0, i); } return aliasUri; } /** * Returns the associated feed, if any, from a full alias uri, for example: * 'urn:acct:mpowers.trsst.com:feed:GhzsrQb7PmbvbdeG13Xr7VJiC59kSk4JW' * becomes 'GhzsrQb7PmbvbdeG13Xr7VJiC59kSk4JW'. */ public static final String getFeedIdFromAliasUri(Object urn) { // remove feed extension if any String aliasUri = urn.toString(); int i = aliasUri.indexOf(ACCOUNT_URN_FEED_PREFIX); if (i != -1) { aliasUri = aliasUri.substring(i + 6); } return aliasUri; } public static final String toEntryIdString(Object entryUrn) { String entryId = entryUrn.toString(); int i = entryId.lastIndexOf(URN_SEPARATOR); if (i != -1) { entryId = entryId.substring(i + 1); } if (entryId.startsWith(ENTRY_URN_PREFIX)) { entryId = entryId.substring(ENTRY_URN_PREFIX.length()); } return entryId; } public static final long toEntryId(Object entryUrn) { return Long.parseLong(toEntryIdString(entryUrn), 16); } public static final long generateEntryId() { try { // sleep to ensure a unique id // if creating multiple entries Thread.sleep(3); } catch (InterruptedException e) { // should never ever happen log.warn("generateEntryId: interrupted", e); } return System.currentTimeMillis(); } public static final String toEntryUrn(String feedId, long entryId) { return ENTRY_URN_PREFIX + feedId + URN_SEPARATOR + Long.toHexString(entryId); } public static final String fromFeedUrn(Object feedUrn) { if (feedUrn != null) { String feedId = feedUrn.toString(); if (feedId.startsWith(FEED_URN_PREFIX)) { feedId = feedId.substring(9); } return feedId; } return null; } public static final String toFeedUrn(String feedId) { if (feedId != null) { if (!feedId.startsWith(FEED_URN_PREFIX)) { feedId = FEED_URN_PREFIX + feedId; } } return feedId; } public static final byte[] keyHash(byte[] key) { try { byte[] sha256 = MessageDigest.getInstance("SHA-256").digest(key); return ripemd160(sha256); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static final byte[] ripemd160(byte[] data) { byte[] ph = new byte[20]; RIPEMD160Digest digest = new RIPEMD160Digest(); digest.update(data, 0, data.length); digest.doFinal(ph, 0); return ph; } public static final byte[] hash(byte[] data, int offset, int len) { try { MessageDigest a = MessageDigest.getInstance("SHA-256"); a.update(data, offset, len); return a.digest(a.digest()); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static boolean isFeedId(String id) { if (id.startsWith(FEED_URN_PREFIX)) { id = id.substring(FEED_URN_PREFIX.length()); } return (decodeChecked(id) != null); } public static boolean isExternalId(String id) { // "external id" a.k.a. URL try { // test for valid url id = fromFeedUrn(id); new URL(decodeURL(id)); return true; } catch (MalformedURLException e) { return false; } } public static boolean isAggregateId(String id) { // "aggregate id" a.k.a. query result: // currently, these start with a '?'. id = fromFeedUrn(id); return (id != null && id.length() > 0 && id.charAt(0) == '?'); } /** * Uses the checksum in the last 4 bytes of the decoded data to verify the * rest are correct. The checksum is removed from the returned data. Returns * null if invalid. Borrowed from bitcoinj. */ private static final byte[] decodeChecked(String input) { byte tmp[]; try { tmp = fromBase58(input); } catch (IllegalArgumentException e) { log.trace("decodeChecked: could not decode: " + input); return null; } if (tmp.length < 4) { log.trace("decodeChecked: input too short: " + input); return null; } byte[] bytes = copyOfRange(tmp, 0, tmp.length - 4); byte[] checksum = copyOfRange(tmp, tmp.length - 4, tmp.length); tmp = doubleDigest(bytes, 0, bytes.length); byte[] hash = copyOfRange(tmp, 0, 4); if (!Arrays.equals(checksum, hash)) { log.trace("decodeChecked: checksum does not validate: " + input); return null; } log.trace("decodeChecked: input is valid: " + input); return bytes; } private static final byte[] copyOfRange(byte[] source, int from, int to) { byte[] range = new byte[to - from]; System.arraycopy(source, from, range, 0, range.length); return range; } /** * Calculates the SHA-256 hash of the given byte range, and then hashes the * resulting hash again. This is standard procedure in Bitcoin. The * resulting hash is in big endian form. Borrowed from bitcoinj. */ private static final byte[] doubleDigest(byte[] input, int offset, int length) { MessageDigest digest; try { digest = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { log.error( "Should never happen: could not find SHA-256 MD algorithm", e); return null; } digest.reset(); digest.update(input, offset, length); byte[] first = digest.digest(); return digest.digest(first); } /** * Converts a X509-encoded EC key to a PublicKey. */ public static PublicKey toPublicKeyFromX509(String stored) throws GeneralSecurityException { KeyFactory factory = KeyFactory.getInstance("EC"); byte[] data = Base64.decodeBase64(stored); X509EncodedKeySpec spec = new X509EncodedKeySpec(data); return factory.generatePublic(spec); } /** * Converts an EC PublicKey to an X509-encoded string. */ public static String toX509FromPublicKey(PublicKey publicKey) throws GeneralSecurityException { KeyFactory factory = KeyFactory.getInstance("EC"); X509EncodedKeySpec spec = factory.getKeySpec(publicKey, X509EncodedKeySpec.class); return new Base64(0, null, true).encodeToString(spec.getEncoded()); } static final KeyPair generateSigningKeyPair() { try { KeyPairGenerator kpg; // kpg = KeyPairGenerator.getInstance("EC", "BC"); kpg = new org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC(); kpg.initialize(new ECGenParameterSpec(CURVE_NAME)); KeyPair kp = kpg.generateKeyPair(); return kp; // } catch (NoSuchAlgorithmException e) { // log.error("Error while generating key: " + e.getMessage(), e); // } catch (NoSuchProviderException e) { // log.error("Error while generating key: " + e.getMessage(), e); } catch (InvalidAlgorithmParameterException e) { log.error("Error while generating key: " + e.getMessage(), e); } return null; } static final KeyPair generateEncryptionKeyPair() { try { KeyPairGenerator kpg; // kpg = KeyPairGenerator.getInstance("EC", "BC"); kpg = new org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC(); kpg.initialize(new ECGenParameterSpec(CURVE_NAME)); KeyPair kp = kpg.generateKeyPair(); return kp; // } catch (NoSuchAlgorithmException e) { // log.error("Error while generating key: " + e.getMessage(), e); // } catch (NoSuchProviderException e) { // e.printStackTrace(); } catch (InvalidAlgorithmParameterException e) { e.printStackTrace(); } return null; } private static final char[] b58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" .toCharArray(); private static final int[] r58 = new int[256]; static { for (int i = 0; i < 256; ++i) { r58[i] = -1; } for (int i = 0; i < b58.length; ++i) { r58[b58[i]] = i; } } public static String toBase58(byte[] b) { if (b.length == 0) { return ""; } int lz = 0; while (lz < b.length && b[lz] == 0) { ++lz; } StringBuffer s = new StringBuffer(); BigInteger n = new BigInteger(1, b); while (n.compareTo(BigInteger.ZERO) > 0) { BigInteger[] r = n.divideAndRemainder(BigInteger.valueOf(58)); n = r[0]; char digit = b58[r[1].intValue()]; s.append(digit); } while (lz > 0) { --lz; s.append("1"); } return s.reverse().toString(); } public static String toBase58WithChecksum(byte[] b) { byte[] cs = Common.hash(b, 0, b.length); byte[] extended = new byte[b.length + 4]; System.arraycopy(b, 0, extended, 0, b.length); System.arraycopy(cs, 0, extended, b.length, 4); return toBase58(extended); } public static byte[] fromBase58WithChecksum(String s) { byte[] b = fromBase58(s); if (b.length < 4) { throw new IllegalArgumentException("Too short for checksum " + s); } byte[] cs = new byte[4]; System.arraycopy(b, b.length - 4, cs, 0, 4); byte[] data = new byte[b.length - 4]; System.arraycopy(b, 0, data, 0, b.length - 4); byte[] h = new byte[4]; System.arraycopy(hash(data, 0, data.length), 0, h, 0, 4); if (Arrays.equals(cs, h)) { return data; } throw new IllegalArgumentException("Checksum mismatch " + s); } public static byte[] fromBase58(String s) { try { boolean leading = true; int lz = 0; BigInteger b = BigInteger.ZERO; for (char c : s.toCharArray()) { if (leading && c == '1') { ++lz; } else { leading = false; b = b.multiply(BigInteger.valueOf(58)); b = b.add(BigInteger.valueOf(r58[c])); } } byte[] encoded = b.toByteArray(); if (encoded[0] == 0) { if (lz > 0) { --lz; } else { byte[] e = new byte[encoded.length - 1]; System.arraycopy(encoded, 1, e, 0, e.length); encoded = e; } } byte[] result = new byte[encoded.length + lz]; System.arraycopy(encoded, 0, result, lz, encoded.length); return result; } catch (ArrayIndexOutOfBoundsException e) { throw new IllegalArgumentException("Invalid character in address"); } catch (Exception e) { throw new IllegalArgumentException(e); } } public static byte[] reverse(byte[] data) { for (int i = 0, j = data.length - 1; i < data.length / 2; i++, j--) { data[i] ^= data[j]; data[j] ^= data[i]; data[i] ^= data[j]; } return data; } public static String toHex(byte[] data) { try { return new String(Hex.encode(data), "US-ASCII"); } catch (UnsupportedEncodingException e) { } return null; } public static byte[] fromHex(String hex) { return Hex.decode(hex); } public static boolean isLessThanUnsigned(long n1, long n2) { return (n1 < n2) ^ ((n1 < 0) != (n2 < 0)); } public static byte[] readFully(InputStream data) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); int c; byte[] buf = new byte[1024]; while ((c = data.read(buf)) != -1) { output.write(buf, 0, c); } return output.toByteArray(); } public static String encodeURL(String parameter) { try { return URLEncoder.encode(parameter, "UTF-8"); } catch (UnsupportedEncodingException e) { log.error("encodeURL: should never happen", e); return null; } } public static String decodeURL(String parameter) { try { return URLDecoder.decode(parameter, "UTF-8"); } catch (UnsupportedEncodingException e) { log.error("encodeURL: should never happen", e); return null; } } public static String escapeHTML(String html) { return StringEscapeUtils.escapeHtml3(html); } public static String unescapeHTML(String escapedHtml) { return StringEscapeUtils.unescapeHtml3(escapedHtml); } public static org.w3c.dom.Document fomToDom(Document<Element> doc) { org.w3c.dom.Document dom = null; if (doc != null) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); doc.writeTo(out); ByteArrayInputStream in = new ByteArrayInputStream( out.toByteArray()); DocumentBuilderFactory dbf = DocumentBuilderFactory .newInstance(); dbf.setValidating(false); dbf.setNamespaceAware(true); DocumentBuilder db = dbf.newDocumentBuilder(); dom = db.parse(in); } catch (Exception e) { } } return dom; } public static Document<Element> domToFom(org.w3c.dom.Document dom) { Document<Element> doc = null; if (dom != null) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); TransformerFactory tf = TransformerFactory.newInstance(); Transformer t = tf.newTransformer(); t.transform(new DOMSource(dom), new StreamResult(out)); ByteArrayInputStream in = new ByteArrayInputStream( out.toByteArray()); doc = Abdera.getInstance().getParser().parse(in); } catch (Exception e) { } } return doc; } public static org.w3c.dom.Element fomToDom(Element element) { org.w3c.dom.Element dom = null; if (element != null) { try { ByteArrayInputStream in = new ByteArrayInputStream(element .toString().getBytes()); DocumentBuilderFactory dbf = DocumentBuilderFactory .newInstance(); dbf.setValidating(false); dbf.setNamespaceAware(true); DocumentBuilder db = dbf.newDocumentBuilder(); dom = db.parse(in).getDocumentElement(); } catch (Exception e) { } } return dom; } public static Element domToFom(org.w3c.dom.Element element) { Element el = null; if (element != null) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); TransformerFactory tf = TransformerFactory.newInstance(); Transformer t = tf.newTransformer(); t.transform(new DOMSource(element), new StreamResult(out)); ByteArrayInputStream in = new ByteArrayInputStream( out.toByteArray()); el = Abdera.getInstance().getParser().parse(in).getRoot(); } catch (Exception e) { } } return el; } public static String formatXML(String xml) { Tidy tidy = new Tidy(); tidy.setXmlTags(true); tidy.setXmlOut(true); StringWriter writer = new StringWriter(); tidy.parse(new StringReader(xml), writer); return writer.toString(); } public static Attributes getManifestAttributes() { Attributes result = null; Class<Common> clazz = Common.class; String className = clazz.getSimpleName() + ".class"; URL classPath = clazz.getResource(className); if (classPath == null || !classPath.toString().startsWith("jar")) { // Class not from JAR return null; } String classPathString = classPath.toString(); String manifestPath = classPathString.substring(0, classPathString.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF"; try { Manifest manifest = new Manifest(new URL(manifestPath).openStream()); result = manifest.getMainAttributes(); } catch (MalformedURLException e) { log.error("Could not locate manifest: " + manifestPath); } catch (IOException e) { log.error("Could not open manifest: " + manifestPath); } return result; } public static Date getBuildDate() { Date result = null; Attributes attributes = getManifestAttributes(); if (attributes != null) { String dateString = attributes.getValue("Built-On"); try { result = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") .parse(dateString); } catch (Throwable t) { log.warn("Could not parse build timestamp: " + dateString); } } else { log.warn("Could not find manifest attributes."); } return result; } public static String getBuildId() { Attributes attributes = getManifestAttributes(); if (attributes != null) { return attributes.getValue("Implementation-Build"); } else { log.warn("Could not find manifest attributes."); } return null; } public static String getBuildString() { String result = null; String[] keys = new String[] { "Implementation-Title", "Implementation-Version", "Implementation-Build", "Built-On", }; Attributes attributes = getManifestAttributes(); if (attributes != null) { Object value; for (String key : keys) { value = attributes.getValue(key); if (value != null) { if (result == null) { result = value.toString(); } else { result = result + ' ' + value.toString(); } } } } else { result = "trsst client"; } return result; } /** * Most trsst nodes run with self-signed certificates, so by default we * accept them. While posts are still signed and/or encrypted, a MITM can * still refuse our out-going posts and suppress incoming new ones, but this * the reason to relay with many trsst servers. Use the -strict option to * require CA-signed certificates. Note that nowadays CA-signed certs are no * guarantee either. */ public static void enableAnonymousSSL() { TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] certs, String authType) { } public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; SSLContext sc; try { sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, new java.security.SecureRandom()); HttpsURLConnection .setDefaultSSLSocketFactory(sc.getSocketFactory()); } catch (NoSuchAlgorithmException e) { log.error("Can't get SSL context", e); } catch (KeyManagementException e) { log.error("Can't set SSL socket factory", e); } // Create all-trusting host name verifier HostnameVerifier allHostsValid = new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } }; // Install the all-trusting host verifier HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid); // For apache http client Protocol anonhttps = new Protocol("https", (ProtocolSocketFactory) new AnonymSSLSocketFactory(), 443); // Protocol.registerProtocol("https", anonhttps); } }