diff --git a/apps/addressbook/java/src/addressbook/AddressBook.java b/apps/addressbook/java/src/addressbook/AddressBook.java
index 547cb71cb5..ca1e1916e0 100644
--- a/apps/addressbook/java/src/addressbook/AddressBook.java
+++ b/apps/addressbook/java/src/addressbook/AddressBook.java
@@ -159,6 +159,9 @@ public class AddressBook {
return this.addresses.toString();
}
+ private static final int MIN_DEST_LENGTH = 516;
+ private static final int MAX_DEST_LENGTH = MIN_DEST_LENGTH + 100; // longer than any known cert type for now
+
/**
* Do basic validation of the hostname and dest
* hostname was already converted to lower case by ConfigParser.parse()
@@ -184,8 +187,8 @@ public class AddressBook {
(! host.endsWith(".router.i2p")) &&
(! host.endsWith(".console.i2p")) &&
- dest.length() == 516 &&
- dest.endsWith("AAAA") &&
+ ((dest.length() == MIN_DEST_LENGTH && dest.endsWith("AAAA")) ||
+ (dest.length() > MIN_DEST_LENGTH && dest.length() <= MAX_DEST_LENGTH)) &&
dest.replaceAll("[a-zA-Z0-9~-]", "").length() == 0
;
}
diff --git a/apps/routerconsole/jsp/default.css b/apps/routerconsole/jsp/default.css
index d308818c10..b5a63a1ad4 100644
--- a/apps/routerconsole/jsp/default.css
+++ b/apps/routerconsole/jsp/default.css
@@ -45,6 +45,7 @@ div.routersummary {
color: inherit;
font-size: small;
clear: left; /* fixes a bug in Opera */
+ overflow: auto;
}
div.warning {
diff --git a/core/java/src/com/nettgryppa/security/HashCash.java b/core/java/src/com/nettgryppa/security/HashCash.java
new file mode 100644
index 0000000000..7d4e16e927
--- /dev/null
+++ b/core/java/src/com/nettgryppa/security/HashCash.java
@@ -0,0 +1,480 @@
+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()));
+ }
+}
\ No newline at end of file
diff --git a/core/java/src/net/i2p/data/Certificate.java b/core/java/src/net/i2p/data/Certificate.java
index 89a5aca97b..4932139fc2 100644
--- a/core/java/src/net/i2p/data/Certificate.java
+++ b/core/java/src/net/i2p/data/Certificate.java
@@ -36,6 +36,9 @@ public class Certificate extends DataStructureImpl {
public final static int CERTIFICATE_TYPE_HASHCASH = 1;
/** we should not be used for anything (don't use us in the netDb, in tunnels, or tell others about us) */
public final static int CERTIFICATE_TYPE_HIDDEN = 2;
+ /** Signed with 40-byte Signature and (optional) 32-byte hash */
+ public final static int CERTIFICATE_TYPE_SIGNED = 3;
+ public final static int CERTIFICATE_LENGTH_SIGNED_WITH_HASH = Signature.SIGNATURE_BYTES + Hash.HASH_LENGTH;
public Certificate() {
_type = 0;
@@ -149,19 +152,29 @@ public class Certificate extends DataStructureImpl {
buf.append("Null certificate");
else if (getCertificateType() == CERTIFICATE_TYPE_HASHCASH)
buf.append("Hashcash certificate");
+ else if (getCertificateType() == CERTIFICATE_TYPE_HIDDEN)
+ buf.append("Hidden certificate");
+ else if (getCertificateType() == CERTIFICATE_TYPE_SIGNED)
+ buf.append("Signed certificate");
else
- buf.append("Unknown certificiate type (").append(getCertificateType()).append(")");
+ buf.append("Unknown certificate type (").append(getCertificateType()).append(")");
if (_payload == null) {
buf.append(" null payload");
} else {
buf.append(" payload size: ").append(_payload.length);
- int len = 32;
- if (len > _payload.length) len = _payload.length;
- buf.append(" first ").append(len).append(" bytes: ");
- buf.append(DataHelper.toString(_payload, len));
+ if (getCertificateType() == CERTIFICATE_TYPE_HASHCASH) {
+ buf.append(" Stamp: ").append(new String(_payload));
+ } else if (getCertificateType() == CERTIFICATE_TYPE_SIGNED && _payload.length == CERTIFICATE_LENGTH_SIGNED_WITH_HASH) {
+ buf.append(" Signed by hash: ").append(Base64.encode(_payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH));
+ } else {
+ int len = 32;
+ if (len > _payload.length) len = _payload.length;
+ buf.append(" first ").append(len).append(" bytes: ");
+ buf.append(DataHelper.toString(_payload, len));
+ }
}
buf.append("]");
return buf.toString();
}
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/data/Destination.java b/core/java/src/net/i2p/data/Destination.java
index cc647877bb..30589e2116 100644
--- a/core/java/src/net/i2p/data/Destination.java
+++ b/core/java/src/net/i2p/data/Destination.java
@@ -17,17 +17,17 @@ import java.io.OutputStream;
import net.i2p.util.Log;
/**
- * Defines an end point in the I2P network. The Destination may move aroundn
+ * Defines an end point in the I2P network. The Destination may move around
* in the network, but messages sent to the Destination will find it
*
* @author jrandom
*/
public class Destination extends DataStructureImpl {
- private final static Log _log = new Log(Destination.class);
- private Certificate _certificate;
- private SigningPublicKey _signingKey;
- private PublicKey _publicKey;
- private Hash __calculatedHash;
+ protected final static Log _log = new Log(Destination.class);
+ protected Certificate _certificate;
+ protected SigningPublicKey _signingKey;
+ protected PublicKey _publicKey;
+ protected Hash __calculatedHash;
public Destination() {
setCertificate(null);
@@ -174,4 +174,4 @@ public class Destination extends DataStructureImpl {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java
index 85e7614505..edb4d8ceea 100644
--- a/core/java/src/net/i2p/data/PrivateKeyFile.java
+++ b/core/java/src/net/i2p/data/PrivateKeyFile.java
@@ -1,39 +1,79 @@
package net.i2p.data;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
import java.util.Properties;
+import com.nettgryppa.security.HashCash;
+
import net.i2p.I2PException;
import net.i2p.client.I2PClient;
+import net.i2p.client.I2PClientFactory;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
+import net.i2p.crypto.DSAEngine;
+/**
+ * This helper class reads and writes files in the
+ * same "eepPriv.dat" format used by the client code.
+ * The format is:
+ * - Destination (387 bytes if no certificate, otherwise longer)
+ * - Public key (256 bytes)
+ * - Signing Public key (128 bytes)
+ * - Cert. type (1 byte)
+ * - Cert. length (2 bytes)
+ * - Certificate if length != 0
+ * - Private key (256 bytes)
+ * - Signing Private key (20 bytes)
+ * Total 663 bytes
+ *
+ * @author welterde, zzz
+ */
public class PrivateKeyFile {
public PrivateKeyFile(File file, I2PClient client) {
this.file = file;
this.client = client;
this.dest = null;
+ this.privKey = null;
+ this.signingPrivKey = null;
}
- public void createIfAbsent() throws I2PException, IOException {
+ /** Also reads in the file to get the privKay and signingPrivKey,
+ * which aren't available from I2PClient.
+ */
+ public Destination createIfAbsent() throws I2PException, IOException, DataFormatException {
if(!this.file.exists()) {
FileOutputStream out = new FileOutputStream(this.file);
- this.dest = this.client.createDestination(out);
+ this.client.createDestination(out);
out.close();
}
+ return getDestination();
}
- public Destination getDestination() {
- // TODO: how to load destination if this is an old key?
- return dest;
+ /** Also sets the local privKay and signingPrivKey */
+ public Destination getDestination() throws I2PSessionException, IOException, DataFormatException {
+ if (dest == null) {
+ I2PSession s = open();
+ if (s != null) {
+ this.dest = new VerifiedDestination(s.getMyDestination());
+ this.privKey = s.getDecryptionKey();
+ this.signingPrivKey = s.getPrivateKey();
+ }
+ }
+ return this.dest;
}
+ public PrivateKey getPrivKey() { return this.privKey; }
+ public SigningPrivateKey getSigningPrivKey() { return this.signingPrivKey; }
+
public I2PSession open() throws I2PSessionException, IOException {
return this.open(new Properties());
}
@@ -50,10 +90,205 @@ public class PrivateKeyFile {
return s;
}
-
-
-
+ /**
+ * Copied from I2PClientImpl.createDestination()
+ */
+ public void write() throws IOException, DataFormatException {
+ FileOutputStream out = new FileOutputStream(this.file);
+ this.dest.writeBytes(out);
+ this.privKey.writeBytes(out);
+ this.signingPrivKey.writeBytes(out);
+ out.flush();
+ out.close();
+ }
+
+ public String toString() {
+ StringBuffer s = new StringBuffer(128);
+ s.append("Dest: ");
+ s.append(this.dest.toBase64());
+ s.append("\nContains: ");
+ s.append(this.dest);
+ s.append("\nPrivate Key: ");
+ s.append(this.privKey);
+ s.append("\nSigining Private Key: ");
+ s.append(this.signingPrivKey);
+ s.append("\n");
+ return s.toString();
+ }
+
private File file;
private I2PClient client;
private Destination dest;
-}
\ No newline at end of file
+ private PrivateKey privKey;
+ private SigningPrivateKey signingPrivKey;
+
+ private static final int HASH_EFFORT = VerifiedDestination.MIN_HASHCASH_EFFORT;
+
+ /**
+ * Create a new PrivateKeyFile, or modify an existing one, with various
+ * types of Certificates.
+ *
+ * Changing a Certificate does not change the public or private keys.
+ * But it does change the Destination Hash, which effectively makes it
+ * a new Destination. In other words, don't change the Certificate on
+ * a Destination you've already registered in a hosts.txt key add form.
+ *
+ * Copied and expanded from that in Destination.java
+ */
+ public static void main(String args[]) {
+ if (args.length == 0) {
+ System.err.println("Usage: PrivateKeyFile filename (generates if nonexistent, then prints)");
+ System.err.println(" PrivateKeyFile -h filename (generates if nonexistent, adds hashcash cert)");
+ System.err.println(" PrivateKeyFile -h effort filename (specify HashCash effort instead of default " + HASH_EFFORT + ")");
+ System.err.println(" PrivateKeyFile -n filename (changes to null cert)");
+ System.err.println(" PrivateKeyFile -s filename signwithdestfile (generates if nonexistent, adds cert signed by 2nd dest)");
+ System.err.println(" PrivateKeyFile -u filename (changes to unknown cert)");
+ System.err.println(" PrivateKeyFile -x filename (changes to hidden cert)");
+ return;
+ }
+ I2PClient client = I2PClientFactory.createClient();
+
+ int filearg = 0;
+ if (args.length > 1) {
+ if (args.length >= 2 && args[0].equals("-h"))
+ filearg = args.length - 1;
+ else
+ filearg = 1;
+ }
+ try {
+ File f = new File(args[filearg]);
+ PrivateKeyFile pkf = new PrivateKeyFile(f, client);
+ Destination d = pkf.createIfAbsent();
+ System.out.println("Original Destination:");
+ System.out.println(pkf);
+ verifySignature(d);
+ if (args.length == 1)
+ return;
+ Certificate c = new Certificate();
+ if (args[0].equals("-n")) {
+ // Cert constructor generates a null cert
+ } else if (args[0].equals("-u")) {
+ c.setCertificateType(99);
+ } else if (args[0].equals("-x")) {
+ c.setCertificateType(Certificate.CERTIFICATE_TYPE_HIDDEN);
+ } else if (args[0].equals("-h")) {
+ int hashEffort = HASH_EFFORT;
+ if (args.length == 3)
+ hashEffort = Integer.parseInt(args[1]);
+ System.out.println("Estimating hashcash generation time, stand by...");
+ // takes a lot longer than the estimate usually...
+ // maybe because the resource string is much longer than used in the estimate?
+ long low = HashCash.estimateTime(hashEffort);
+ System.out.println("It is estimated this will take " + DataHelper.formatDuration(low) +
+ " to " + DataHelper.formatDuration(4*low));
+
+ long begin = System.currentTimeMillis();
+ System.out.println("Starting hashcash generation now...");
+ String resource = d.getPublicKey().toBase64() + d.getSigningPublicKey().toBase64();
+ HashCash hc = HashCash.mintCash(resource, hashEffort);
+ System.out.println("Generation took: " + DataHelper.formatDuration(System.currentTimeMillis() - begin));
+ System.out.println("Full Hashcash is: " + hc);
+ // Take the resource out of the stamp
+ String hcs = hc.toString();
+ int end1 = 0;
+ for (int i = 0; i < 3; i++) {
+ end1 = 1 + hcs.indexOf(':', end1);
+ if (end1 < 0) {
+ System.out.println("Bad hashcash");
+ return;
+ }
+ }
+ int start2 = hcs.indexOf(':', end1);
+ if (start2 < 0) {
+ System.out.println("Bad hashcash");
+ return;
+ }
+ hcs = hcs.substring(0, end1) + hcs.substring(start2);
+ System.out.println("Short Hashcash is: " + hcs);
+
+ c.setCertificateType(Certificate.CERTIFICATE_TYPE_HASHCASH);
+ c.setPayload(hcs.getBytes());
+ } else if (args.length == 3 && args[0].equals("-s")) {
+ // Sign dest1 with dest2's Signing Private Key
+ File f2 = new File(args[2]);
+ I2PClient client2 = I2PClientFactory.createClient();
+ PrivateKeyFile pkf2 = new PrivateKeyFile(f2, client2);
+ Destination d2 = pkf2.getDestination();
+ SigningPrivateKey spk2 = pkf2.getSigningPrivKey();
+ System.out.println("Signing With Dest:");
+ System.out.println(pkf2.toString());
+
+ int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert
+ byte[] data = new byte[len];
+ System.arraycopy(d.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES);
+ System.arraycopy(d.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES);
+ byte[] payload = new byte[Hash.HASH_LENGTH + Signature.SIGNATURE_BYTES];
+ byte[] sig = DSAEngine.getInstance().sign(new ByteArrayInputStream(data), spk2).getData();
+ System.arraycopy(sig, 0, payload, 0, Signature.SIGNATURE_BYTES);
+ // Add dest2's Hash for reference
+ byte[] h2 = d2.calculateHash().getData();
+ System.arraycopy(h2, 0, payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH);
+ c.setCertificateType(Certificate.CERTIFICATE_TYPE_SIGNED);
+ c.setPayload(payload);
+ }
+ d.setCertificate(c); // do this rather than just change the existing cert so the hash is recalculated
+ System.out.println("New signed destination is:");
+ System.out.println(pkf);
+ pkf.write();
+ verifySignature(d);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Sample code to verify a 3rd party signature.
+ * This just goes through all the hosts.txt files and tries everybody.
+ * You need to be in the $I2P directory or have a local hosts.txt for this to work.
+ * Doubt this is what you want as it is super-slow, and what good is
+ * a signing scheme where anybody is allowed to sign?
+ *
+ * In a real application you would make a list of approved signers,
+ * do a naming lookup to get their Destinations, and try those only.
+ * Or do a netDb lookup of the Hash in the Certificate, do a reverse
+ * naming lookup to see if it is allowed, then verify the Signature.
+ */
+ public static boolean verifySignature(Destination d) {
+ if (d.getCertificate().getCertificateType() != Certificate.CERTIFICATE_TYPE_SIGNED)
+ return false;
+ int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert
+ byte[] data = new byte[len];
+ System.arraycopy(d.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES);
+ System.arraycopy(d.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES);
+ Signature sig = new Signature(d.getCertificate().getPayload());
+
+ String[] filenames = new String[] {"privatehosts.txt", "userhosts.txt", "hosts.txt"};
+ for (int i = 0; i < filenames.length; i++) {
+ Properties hosts = new Properties();
+ try {
+ File f = new File(filenames[i]);
+ if ( (f.exists()) && (f.canRead()) ) {
+ DataHelper.loadProps(hosts, f, true);
+
+ for (Iterator iter = hosts.entrySet().iterator(); iter.hasNext(); ) {
+ Map.Entry entry = (Map.Entry)iter.next();
+ String s = (String) entry.getValue();
+ Destination signer = new Destination(s);
+ if (checkSignature(sig, data, signer.getSigningPublicKey())) {
+ System.out.println("Good signature from: " + entry.getKey());
+ return true;
+ }
+ }
+ }
+ } catch (Exception ioe) {
+ }
+ // not found, continue to the next file
+ }
+ System.out.println("No valid signer found");
+ return false;
+ }
+
+ public static boolean checkSignature(Signature s, byte[] data, SigningPublicKey spk) {
+ return DSAEngine.getInstance().verifySignature(s, data, spk);
+ }
+}
diff --git a/core/java/src/net/i2p/data/VerifiedDestination.java b/core/java/src/net/i2p/data/VerifiedDestination.java
new file mode 100644
index 0000000000..4e90b8cdff
--- /dev/null
+++ b/core/java/src/net/i2p/data/VerifiedDestination.java
@@ -0,0 +1,163 @@
+package net.i2p.data;
+
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by jrandom in 2003 and released into the public domain
+ * with no warranty of any kind, either expressed or implied.
+ * It probably won't make your computer catch on fire, or eat
+ * your children, but it might. Use at your own risk.
+ *
+ */
+
+import java.security.NoSuchAlgorithmException;
+
+import com.nettgryppa.security.HashCash;
+
+import net.i2p.util.Log;
+
+/**
+ * Extend Destination with methods to verify its Certificate.
+ * The router does not check Certificates, it doesn't care.
+ * Apps however (particularly addressbook) may wish to enforce various
+ * cert content, format, and policies.
+ * This class is written such that apps may extend it to
+ * create their own policies.
+ *
+ * @author zzz
+ */
+public class VerifiedDestination extends Destination {
+ protected final static Log _log = new Log(Destination.class);
+
+ public VerifiedDestination() {
+ super();
+ }
+
+ /**
+ * alternative constructor which takes a base64 string representation
+ * @param s a Base64 representation of the destination, as (eg) is used in hosts.txt
+ */
+ public VerifiedDestination(String s) throws DataFormatException {
+ this();
+ fromBase64(s);
+ }
+
+ /**
+ * create from an existing Dest
+ * @param d must be non-null
+ */
+ public VerifiedDestination(Destination d) throws DataFormatException {
+ this(d.toBase64());
+ }
+
+ /**
+ * verify the certificate.
+ * @param allowNone If true, allow a NULL or HIDDEN certificate.
+ */
+ public boolean verifyCert(boolean allowNone) {
+ if (_publicKey == null || _signingKey == null || _certificate == null)
+ return false;
+ switch (_certificate.getCertificateType()) {
+ case Certificate.CERTIFICATE_TYPE_NULL:
+ case Certificate.CERTIFICATE_TYPE_HIDDEN:
+ return allowNone;
+ case Certificate.CERTIFICATE_TYPE_HASHCASH:
+ return verifyHashCashCert();
+ case Certificate.CERTIFICATE_TYPE_SIGNED:
+ return verifySignedCert();
+ }
+ return verifyUnknownCert();
+ }
+
+ /** Defaults for HashCash Certs */
+ public final static int MIN_HASHCASH_EFFORT = 20;
+
+ /**
+ * HashCash Certs are used to demonstrate proof-of-work.
+ *
+ * We define a HashCash Certificate as follows:
+ * - length: typically 47 bytes, but may vary somewhat
+ * - contents: A version 1 HashCash Stamp,
+ * defined at http://www.hashcash.org/docs/hashcash.html#stamp_format__version_1_
+ * modified to remove the contents of the 4th field (the resource)
+ * original is ver:bits:date:resource:[ext]:rand:counter
+ * I2P version is ver:bits:date::[ext]:rand:counter
+ * The HashCash is calculated with the following resource:
+ * The Base64 of the Public Key concatenated with the Base64 of the Signing Public Key
+ * (NOT the Base64 of the concatenated keys)
+ * To generate a Cert of this type, see PrivateKeyFile.main()
+ * To verify, we must put the keys back into the resource field of the stamp,
+ * then pass it to the HashCash constructor, then get the number of leading
+ * zeros and see if it meets our minimum effort.
+ */
+ protected boolean verifyHashCashCert() {
+ String hcs = new String(_certificate.getPayload());
+ int end1 = 0;
+ for (int i = 0; i < 3; i++) {
+ end1 = 1 + hcs.indexOf(':', end1);
+ if (end1 < 0)
+ return false;
+ }
+ int start2 = hcs.indexOf(':', end1);
+ if (start2 < 0)
+ return false;
+ // put the keys back into the 4th field of the stamp
+ hcs = hcs.substring(0, end1) + _publicKey.toBase64() + _signingKey.toBase64() + hcs.substring(start2);
+ HashCash hc;
+ try {
+ hc = new HashCash(hcs);
+ } catch (IllegalArgumentException iae) {
+ return false;
+ } catch (NoSuchAlgorithmException nsae) {
+ return false;
+ }
+ return hc.getValue() >= MIN_HASHCASH_EFFORT;
+ }
+
+ /** Defaults for Signed Certs */
+ public final static int CERTIFICATE_LENGTH_SIGNED = Signature.SIGNATURE_BYTES;
+ public final static int CERTIFICATE_LENGTH_SIGNED_WITH_HASH = Signature.SIGNATURE_BYTES + Hash.HASH_LENGTH;
+
+ /**
+ * Signed Certs are signed by a 3rd-party Destination.
+ * They can be used for a second-level domain, for example, to sign the
+ * Destination for a third-level domain. Or for a central authority
+ * to approve a destination.
+ *
+ * We define a Signed Certificate as follows:
+ * - length: Either 44 or 72 bytes
+ * - contents:
+ * 1: a 44 byte Signature
+ * 2 (optional): a 32 byte Hash of the signing Destination
+ * This can be a hint to the verification process to help find
+ * the identity and keys of the signing Destination.
+ * Data which is signed: The first 384 bytes of the Destination
+ * (i.e. the Public Key and Signing Public Key, WITHOUT the Certificate)
+ *
+ * It is not appropriate to enforce a particular delegation scheme here.
+ * The application will need to apply additional steps to select
+ * an appropriate signing Destination and verify the signature.
+ *
+ * See PrivateKeyFile.verifySignature() for sample verification code.
+ *
+ */
+ protected boolean verifySignedCert() {
+ return _certificate.getPayload() != null &&
+ (_certificate.getPayload().length == CERTIFICATE_LENGTH_SIGNED ||
+ _certificate.getPayload().length == CERTIFICATE_LENGTH_SIGNED_WITH_HASH);
+ }
+
+ /**
+ * Reject all unknown certs
+ */
+ protected boolean verifyUnknownCert() {
+ return false;
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer(128);
+ buf.append(super.toString());
+ buf.append("\n\tVerified Certificate? ").append(verifyCert(true));
+ return buf.toString();
+ }
+
+}
diff --git a/history.txt b/history.txt
index 16901547a6..1410468005 100644
--- a/history.txt
+++ b/history.txt
@@ -1,4 +1,22 @@
-2008-10-27 zzz
+2008-11-02 zzz
+ * Certificates:
+ - Add a signed Certificate type
+ - Add a main() to PrivateKeyFile to generate
+ Destinations with various Certificate types
+ - Add a VerifiedDestination class to check Certificates
+ of various types
+ - Add a HashCash library from http://www.nettgryppa.com/code/
+ (no distribution restrictions)
+ - Allow non-null Certificates in addressbook
+ * I2PTunnel: Move some wayward stats to the I2PTunnel group
+ * NamingServices: Implement caching in the abstract class
+ * NewsFetcher: Fix last updated time
+ * Streaming: Increase MTU to 1730 (was 960);
+ see ConnectionOptions.java for analysis
+ * Throttle: Reduce default max tunnels to 2000 (was 2500)
+ * clients.config: Disable SAM and BOB by default for new installs
+
+2008-10-26 zzz
* config.jsp: Add more help
* peers.jsp: Clean up 'Listening on' formatting
* profiles.jsp: Don't override locale number format
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index 2f0d4e9d01..56124cd55a 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -17,7 +17,7 @@ import net.i2p.CoreVersion;
public class RouterVersion {
public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $";
public final static String VERSION = "0.6.4";
- public final static long BUILD = 7;
+ public final static long BUILD = 8;
public static void main(String args[]) {
System.out.println("I2P Router version: " + VERSION + "-" + BUILD);
System.out.println("Router ID: " + RouterVersion.ID);