package com.nettgryppa.security; // Copyright 2006 Gregory Rubin grrubin@gmail.com // Permission is given to use, modify, and or distribute this code so long as this message remains attached // Please see the spec at: http://www.hashcash.org/ import java.util.*; import java.security.MessageDigest; import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.security.NoSuchAlgorithmException; /** * Class for generation and parsing of HashCash
* Copyright 2006 Gregory Rubin grrubin@gmail.com
* Permission is given to use, modify, and or distribute this code so long as this message remains attached
* Please see the spec at: http://www.hashcash.org/ * @author grrubin@gmail.com * @version 1.1 */ public class HashCash implements Comparable { public static final int DefaultVersion = 1; private static final int hashLength = 160; private static final String dateFormatString = "yyMMdd"; private static long milliFor16 = -1; private String myToken; private int myValue; private Calendar myDate; private Map > myExtensions; private int myVersion; private String myResource; // Constructors /** * Parses and validates a HashCash. * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public HashCash(String cash) throws NoSuchAlgorithmException { myToken = cash; String[] parts = cash.split(":"); myVersion = Integer.parseInt(parts[0]); if(myVersion < 0 || myVersion > 1) throw new IllegalArgumentException("Only supported versions are 0 and 1"); if((myVersion == 0 && parts.length != 6) || (myVersion == 1 && parts.length != 7)) throw new IllegalArgumentException("Improperly formed HashCash"); try { int index = 1; if(myVersion == 1) myValue = Integer.parseInt(parts[index++]); else myValue = 0; SimpleDateFormat dateFormat = new SimpleDateFormat(dateFormatString); Calendar tempCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); tempCal.setTime(dateFormat.parse(parts[index++])); myResource = parts[index++]; myExtensions = deserializeExtensions(parts[index++]); MessageDigest md = MessageDigest.getInstance("SHA1"); md.update(cash.getBytes()); byte[] tempBytes = md.digest(); int tempValue = numberOfLeadingZeros(tempBytes); if(myVersion == 0) myValue = tempValue; else if (myVersion == 1) myValue = (tempValue > myValue ? myValue : tempValue); } catch (java.text.ParseException ex) { throw new IllegalArgumentException("Improperly formed HashCash", ex); } } private HashCash() throws NoSuchAlgorithmException { } /** * Mints a version 1 HashCash using now as the date * @param resource the string to be encoded in the HashCash * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static HashCash mintCash(String resource, int value) throws NoSuchAlgorithmException { Calendar now = Calendar.getInstance(TimeZone.getTimeZone("GMT")); return mintCash(resource, null, now, value, DefaultVersion); } /** * Mints a HashCash using now as the date * @param resource the string to be encoded in the HashCash * @param version Which version to mint. Only valid values are 0 and 1 * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static HashCash mintCash(String resource, int value, int version) throws NoSuchAlgorithmException { Calendar now = Calendar.getInstance(TimeZone.getTimeZone("GMT")); return mintCash(resource, null, now, value, version); } /** * Mints a version 1 HashCash * @param resource the string to be encoded in the HashCash * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static HashCash mintCash(String resource, Calendar date, int value) throws NoSuchAlgorithmException { return mintCash(resource, null, date, value, DefaultVersion); } /** * Mints a HashCash * @param resource the string to be encoded in the HashCash * @param version Which version to mint. Only valid values are 0 and 1 * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static HashCash mintCash(String resource, Calendar date, int value, int version) throws NoSuchAlgorithmException { return mintCash(resource, null, date, value, version); } /** * Mints a version 1 HashCash using now as the date * @param resource the string to be encoded in the HashCash * @param extensions Extra data to be encoded in the HashCash * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static HashCash mintCash(String resource, Map > extensions, int value) throws NoSuchAlgorithmException { Calendar now = Calendar.getInstance(TimeZone.getTimeZone("GMT")); return mintCash(resource, extensions, now, value, DefaultVersion); } /** * Mints a HashCash using now as the date * @param resource the string to be encoded in the HashCash * @param extensions Extra data to be encoded in the HashCash * @param version Which version to mint. Only valid values are 0 and 1 * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static HashCash mintCash(String resource, Map > extensions, int value, int version) throws NoSuchAlgorithmException { Calendar now = Calendar.getInstance(TimeZone.getTimeZone("GMT")); return mintCash(resource, extensions, now, value, version); } /** * Mints a version 1 HashCash * @param resource the string to be encoded in the HashCash * @param extensions Extra data to be encoded in the HashCash * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static HashCash mintCash(String resource, Map > extensions, Calendar date, int value) throws NoSuchAlgorithmException { return mintCash(resource, extensions, date, value, DefaultVersion); } /** * Mints a HashCash * @param resource the string to be encoded in the HashCash * @param extensions Extra data to be encoded in the HashCash * @param version Which version to mint. Only valid values are 0 and 1 * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static HashCash mintCash(String resource, Map > extensions, Calendar date, int value, int version) throws NoSuchAlgorithmException { if(version < 0 || version > 1) throw new IllegalArgumentException("Only supported versions are 0 and 1"); if(value < 0 || value > hashLength) throw new IllegalArgumentException("Value must be between 0 and " + hashLength); if(resource.contains(":")) throw new IllegalArgumentException("Resource may not contain a colon."); HashCash result = new HashCash(); MessageDigest md = MessageDigest.getInstance("SHA1"); result.myResource = resource; result.myExtensions = (null == extensions ? new HashMap >() : extensions); result.myDate = date; result.myVersion = version; String prefix; SimpleDateFormat dateFormat = new SimpleDateFormat(dateFormatString); switch(version) { case 0: prefix = version + ":" + dateFormat.format(date.getTime()) + ":" + resource + ":" + serializeExtensions(extensions) + ":"; result.myToken = generateCash(prefix, value, md); md.reset(); md.update(result.myToken.getBytes()); result.myValue = numberOfLeadingZeros(md.digest()); break; case 1: result.myValue = value; prefix = version + ":" + value + ":" + dateFormat.format(date.getTime()) + ":" + resource + ":" + serializeExtensions(extensions) + ":"; result.myToken = generateCash(prefix, value, md); break; default: throw new IllegalArgumentException("Only supported versions are 0 and 1"); } return result; } // Accessors /** * Two objects are considered equal if they are both of type HashCash and have an identical string representation */ public boolean equals(Object obj) { if(obj instanceof HashCash) return toString().equals(obj.toString()); else return super.equals(obj); } /** * Returns the canonical string representation of the HashCash */ public String toString() { return myToken; } /** * Extra data encoded in the HashCash */ public Map > getExtensions() { return myExtensions; } /** * The primary resource being protected */ public String getResource() { return myResource; } /** * The minting date */ public Calendar getDate() { return myDate; } /** * The value of the HashCash (e.g. how many leading zero bits it has) */ public int getValue() { return myValue; } /** * Which version of HashCash is used here */ public int getVersion() { return myVersion; } // Private utility functions /** * Actually tries various combinations to find a valid hash. Form is of prefix + random_hex + ":" + random_hex * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ private static String generateCash(String prefix, int value, MessageDigest md) throws NoSuchAlgorithmException { SecureRandom rnd = SecureRandom.getInstance("SHA1PRNG"); byte[] tmpBytes = new byte[8]; rnd.nextBytes(tmpBytes); long random = bytesToLong(tmpBytes); rnd.nextBytes(tmpBytes); long counter = bytesToLong(tmpBytes); prefix = prefix + Long.toHexString(random) + ":"; String temp; int tempValue; byte[] bArray; do { counter++; temp = prefix + Long.toHexString(counter); md.reset(); md.update(temp.getBytes()); bArray = md.digest(); tempValue = numberOfLeadingZeros(bArray); } while ( tempValue < value); return temp; } /** * Converts a 8 byte array of unsigned bytes to an long * @param b an array of 8 unsigned bytes */ private static long bytesToLong(byte[] b) { long l = 0; l |= b[0] & 0xFF; l <<= 8; l |= b[1] & 0xFF; l <<= 8; l |= b[2] & 0xFF; l <<= 8; l |= b[3] & 0xFF; l <<= 8; l |= b[4] & 0xFF; l <<= 8; l |= b[5] & 0xFF; l <<= 8; l |= b[6] & 0xFF; l <<= 8; l |= b[7] & 0xFF; return l; } /** * Serializes the extensions with (key, value) seperated by semi-colons and values seperated by commas */ private static String serializeExtensions(Map > extensions) { if(null == extensions || extensions.isEmpty()) return ""; StringBuffer result = new StringBuffer(); List tempList; boolean first = true; for(String key: extensions.keySet()) { if(key.contains(":") || key.contains(";") || key.contains("=")) throw new IllegalArgumentException("Extension key contains an illegal character. " + key); if(!first) result.append(";"); first = false; result.append(key); tempList = extensions.get(key); if(null != tempList) { result.append("="); for(int i = 0; i < tempList.size(); i++) { if(tempList.get(i).contains(":") || tempList.get(i).contains(";") || tempList.get(i).contains(",")) throw new IllegalArgumentException("Extension value contains an illegal character. " + tempList.get(i)); if(i > 0) result.append(","); result.append(tempList.get(i)); } } } return result.toString(); } /** * Inverse of {@link #serializeExtensions(Map)} */ private static Map > deserializeExtensions(String extensions) { Map > result = new HashMap >(); if(null == extensions || extensions.length() == 0) return result; String[] items = extensions.split(";"); for(int i = 0; i < items.length; i++) { String[] parts = items[i].split("=", 2); if(parts.length == 1) result.put(parts[0], null); else result.put(parts[0], Arrays.asList(parts[1].split(","))); } return result; } /** * Counts the number of leading zeros in a byte array. */ private static int numberOfLeadingZeros(byte[] values) { int result = 0; int temp = 0; for(int i = 0; i < values.length; i++) { temp = numberOfLeadingZeros(values[i]); result += temp; if(temp != 8) break; } return result; } /** * Returns the number of leading zeros in a bytes binary represenation */ private static int numberOfLeadingZeros(byte value) { if(value < 0) return 0; if(value < 1) return 8; else if (value < 2) return 7; else if (value < 4) return 6; else if (value < 8) return 5; else if (value < 16) return 4; else if (value < 32) return 3; else if (value < 64) return 2; else if (value < 128) return 1; else return 0; } /** * Estimates how many milliseconds it would take to mint a cash of the specified value. *
    *
  • NOTE1: Minting time can vary greatly in fact, half of the time it will take half as long) *
  • NOTE2: The first time that an estimation function is called it is expensive (on the order of seconds). After that, it is very quick. *
* @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static long estimateTime(int value) throws NoSuchAlgorithmException { initEstimates(); return (long)(milliFor16 * Math.pow(2, value - 16)); } /** * Estimates what value (e.g. how many bits of collision) are required for the specified length of time. *
    *
  • NOTE1: Minting time can vary greatly in fact, half of the time it will take half as long) *
  • NOTE2: The first time that an estimation function is called it is expensive (on the order of seconds). After that, it is very quick. *
* @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ public static int estimateValue(int secs) throws NoSuchAlgorithmException { initEstimates(); int result = 0; long millis = secs * 1000 * 65536; millis /= milliFor16; while(millis > 1) { result++; millis /= 2; } return result; } /** * Seeds the estimates by determining how long it takes to calculate a 16bit collision on average. * @throws NoSuchAlgorithmException If SHA1 is not a supported Message Digest */ private static void initEstimates() throws NoSuchAlgorithmException { if(milliFor16 == -1) { long duration; duration = Calendar.getInstance().getTimeInMillis(); for(int i = 0; i < 11; i++) { mintCash("estimation", 16); } duration = Calendar.getInstance().getTimeInMillis() - duration; milliFor16 = (duration /10); } } /** * Compares the value of two HashCashes * @param other * @see java.lang.Comparable#compareTo(Object) */ public int compareTo(HashCash other) { if(null == other) throw new NullPointerException(); return Integer.valueOf(getValue()).compareTo(Integer.valueOf(other.getValue())); } }