diff --git a/apps/phttprelay/java/lib/javax.servlet.jar b/apps/phttprelay/java/lib/javax.servlet.jar new file mode 100644 index 000000000..6bee0cc32 Binary files /dev/null and b/apps/phttprelay/java/lib/javax.servlet.jar differ diff --git a/core/c/build.sh b/core/c/build.sh new file mode 100644 index 000000000..0476a6202 --- /dev/null +++ b/core/c/build.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# linux settings: +CC="gcc" +ANT="ant" +JAVA="java" + +COMPILEFLAGS="-fPIC -Wall" +LINKFLAGS="-shared -Wl,-soname,libjbigi.so" + +INCLUDES="-Iinclude -I$JAVA_HOME/include -I$JAVA_HOME/include/linux" +INCLUDELIBS="-lgmp" +STATICLIBS="" + +LIBFILE="libjbigi.so" + +# jrandom's mingw setup: +#INCLUDES="-Iinclude -Ic:/software/j2sdk1.4.2/include/win32/ -Ic:/software/j2sdk1.4.2/include/ -Ic:/dev/gmp-4.1.2/" +#LINKFLAGS="-shared -Wl,--kill-at" +#LIBFILE="jbigi.dll" +#INCLUDELIBS="" +#STATICLIBS="c:/dev/libgmp.a" + +rm -f jbigi.o $LIBFILE +$CC -c $COMPILEFLAGS $INCLUDES src/jbigi.c +$CC $LINKFLAGS $INCLUDES $INCLUDELIBS -o $LIBFILE jbigi.o $STATICLIBS + +echo "built, now testing" +(cd ../java/src/ ; $ANT ) +LD_LIBRARY_PATH=. $JAVA -cp ../java/src/i2p.jar -DloggerConfigLocation=../java/src/logger.config net.i2p.util.NativeBigInteger + + +echo "" +echo "" +echo "test complete. please review the lines 'native run time:', 'java run time:', and 'native = '" diff --git a/core/c/include/jbigi.h b/core/c/include/jbigi.h new file mode 100644 index 000000000..0b5932959 --- /dev/null +++ b/core/c/include/jbigi.h @@ -0,0 +1,44 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class net_i2p_util_NativeBigInteger */ + +#ifndef _Included_net_i2p_util_NativeBigInteger +#define _Included_net_i2p_util_NativeBigInteger +#ifdef __cplusplus +extern "C" { +#endif +#undef net_i2p_util_NativeBigInteger_serialVersionUID +#define net_i2p_util_NativeBigInteger_serialVersionUID -8742448824652078965LL +#undef net_i2p_util_NativeBigInteger_LONG_MASK +#define net_i2p_util_NativeBigInteger_LONG_MASK 4294967295LL +/* Inaccessible static: bitsPerDigit */ +/* Inaccessible static: SMALL_PRIME_PRODUCT */ +#undef net_i2p_util_NativeBigInteger_MAX_CONSTANT +#define net_i2p_util_NativeBigInteger_MAX_CONSTANT 16L +/* Inaccessible static: posConst */ +/* Inaccessible static: negConst */ +/* Inaccessible static: ZERO */ +/* Inaccessible static: ONE */ +/* Inaccessible static: TWO */ +/* Inaccessible static: bnExpModThreshTable */ +/* Inaccessible static: trailingZeroTable */ +/* Inaccessible static: zeros */ +/* Inaccessible static: digitsPerLong */ +/* Inaccessible static: longRadix */ +/* Inaccessible static: digitsPerInt */ +/* Inaccessible static: intRadix */ +#undef net_i2p_util_NativeBigInteger_serialVersionUID +#define net_i2p_util_NativeBigInteger_serialVersionUID -8287574255936472291LL +/* Inaccessible static: _nativeOk */ +/* + * Class: net_i2p_util_NativeBigInteger + * Method: nativeModPow + * Signature: ([B[B[B)[B + */ +JNIEXPORT jbyteArray JNICALL Java_net_i2p_util_NativeBigInteger_nativeModPow + (JNIEnv *, jclass, jbyteArray, jbyteArray, jbyteArray); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/core/c/src/jbigi.c b/core/c/src/jbigi.c new file mode 100644 index 000000000..c134ecd65 --- /dev/null +++ b/core/c/src/jbigi.c @@ -0,0 +1,123 @@ +#include +#include +#include "jbigi.h" + +/********/ +//function prototypes + +//FIXME: should these go into jbigi.h? -- ughabugha + +void convert_j2mp(JNIEnv* env, jbyteArray jvalue, mpz_t* mvalue); +void convert_mp2j(JNIEnv* env, mpz_t mvalue, jbyteArray* jvalue); + +/********/ +/* + * Class: net_i2p_util_NativeBigInteger + * Method: nativeModPow + * Signature: ([B[B[B)[B + * + * From the javadoc: + * + * calculate (base ^ exponent) % modulus. + * @param curVal big endian twos complement representation of the base (but it must be positive) + * @param exponent big endian twos complement representation of the exponent + * @param modulus big endian twos complement representation of the modulus + * @return big endian twos complement representation of (base ^ exponent) % modulus + */ + +JNIEXPORT jbyteArray JNICALL Java_net_i2p_util_NativeBigInteger_nativeModPow + (JNIEnv* env, jclass cls, jbyteArray jbase, jbyteArray jexp, jbyteArray jmod) { + // convert base, exponent, modulus into the format libgmp understands + // call libgmp's modPow + // convert libgmp's result into a big endian twos complement number + + mpz_t mbase; + mpz_t mexp; + mpz_t mmod; + //mpz_t mresult; + jbyteArray jresult; + + convert_j2mp(env, jbase, &mbase); + convert_j2mp(env, jexp, &mexp); + convert_j2mp(env, jmod, &mmod); + + //gmp_printf("mbase =%Zd\n", mbase); + //gmp_printf("mexp =%Zd\n", mexp); + //gmp_printf("mmod =%Zd\n", mmod); + + mpz_powm(mmod, mbase, mexp, mmod); + //we use mod for the result because it is always at least as big + + //gmp_printf("mresult=%Zd\n", mmod); + + convert_mp2j(env, mmod, &jresult); + //convert_j2mp(env, jresult, &mresult); + + //gmp_printf("", mpz_cmp(mmod, mresult) == 0 ? "true" : "false"); + + mpz_clear(mbase); + mpz_clear(mexp); + mpz_clear(mmod); + //mpz_clear(mresult); + + return jresult; +} + +/********/ +/* + * Initializes the GMP value with enough preallocated size, and converts the + * Java value into the GMP value. The value that mvalue is pointint to + * should be uninitialized + */ + +void convert_j2mp(JNIEnv* env, jbyteArray jvalue, mpz_t* mvalue) +{ + jsize size; + jbyte* jbuffer; + + size = (*env)->GetArrayLength(env, jvalue); + jbuffer = (*env)->GetByteArrayElements(env, jvalue, NULL); + + mpz_init2(*mvalue, sizeof(jbyte) * 8 * size); //preallocate the size + + /* + * void mpz_import (mpz_t rop, size_t count, int order, int size, int endian, size_t nails, const void *op) + * order = 1 - order can be 1 for most significant word first or -1 for least significant first. + * endian = 1 - Within each word endian can be 1 for most significant byte first, -1 for least significant first + * nails = 0 - The most significant nails bits of each word are skipped, this can be 0 to use the full words + */ + mpz_import(*mvalue, size, 1, sizeof(jbyte), 1, 0, (void*)jbuffer); + (*env)->ReleaseByteArrayElements(env, jvalue, jbuffer, JNI_ABORT); +} + +/********/ +/* + * Converts the GMP value into the Java value; Doesn't do anything else. + */ + +void convert_mp2j(JNIEnv* env, mpz_t mvalue, jbyteArray* jvalue) +{ + jsize size; + jbyte* buffer; + jboolean copy; + + copy = JNI_FALSE; + + size = (mpz_sizeinbase(mvalue, 2) + 7) / 8 + sizeof(jbyte); //+7 => ceil division + *jvalue = (*env)->NewByteArray(env, size); + + buffer = (*env)->GetByteArrayElements(env, *jvalue, ©); + + buffer[0] = 0; + + /* + * void *mpz_export (void *rop, size_t *count, int order, int size, int endian, size_t nails, mpz_t op) + */ + mpz_export((void*)&buffer[1], &size, 1, sizeof(jbyte), 1, 0, mvalue); + + (*env)->ReleaseByteArrayElements(env, *jvalue, buffer, 0); + //mode has (supposedly) no effect if elems is not a copy of the elements in array +} + +/********/ + diff --git a/core/doc/readme.license.txt b/core/doc/readme.license.txt new file mode 100644 index 000000000..0475736d8 --- /dev/null +++ b/core/doc/readme.license.txt @@ -0,0 +1,11 @@ +$Id$ + +the i2p/core/ module is the root of the I2P SDK, and +everything within it is released according to the +terms of the I2P license policy. For the I2P SDK, +that means everything contained within the i2p/core +module is released into the public domain unless +otherwise marked. Alternate licenses that may be +used include BSD (used by thecrypto's DSA, ElGamal, +and SHA256 implementations), Cryptix (used by cryptix's +AES implementation), and MIT. diff --git a/core/java/build.xml b/core/java/build.xml new file mode 100644 index 000000000..0dc434787 --- /dev/null +++ b/core/java/build.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/java/src/net/i2p/CoreVersion.java b/core/java/src/net/i2p/CoreVersion.java new file mode 100644 index 000000000..e86c9353a --- /dev/null +++ b/core/java/src/net/i2p/CoreVersion.java @@ -0,0 +1,22 @@ +package net.i2p; +/* + * 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. + * + */ + +/** + * Expose a version string + * + */ +public class CoreVersion { + public final static String ID = "$Revision: 1.33 $ $Date: 2004/04/04 13:40:34 $"; + public final static String VERSION = "0.3.0.3"; + public static void main(String args[]) { + System.out.println("I2P Core version: " + VERSION); + System.out.println("ID: " + ID); + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/I2PException.java b/core/java/src/net/i2p/I2PException.java new file mode 100644 index 000000000..671ea426c --- /dev/null +++ b/core/java/src/net/i2p/I2PException.java @@ -0,0 +1,48 @@ +package net.i2p; +/* + * 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.io.PrintStream; +import java.io.PrintWriter; + +/** + * Base class of I2P exceptions + * + * @author jrandom + */ +public class I2PException extends Exception { + private Throwable _source; + + public I2PException() { + this(null, null); + } + public I2PException(String msg) { + this(msg, null); + } + public I2PException(String msg, Throwable source) { + super(msg); + _source = source; + } + + public void printStackTrace() { + if (_source != null) + _source.printStackTrace(); + super.printStackTrace(); + } + public void printStackTrace(PrintStream ps) { + if (_source != null) + _source.printStackTrace(ps); + super.printStackTrace(ps); + } + public void printStackTrace(PrintWriter pw) { + if (_source != null) + _source.printStackTrace(pw); + super.printStackTrace(pw); + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/ATalk.java b/core/java/src/net/i2p/client/ATalk.java new file mode 100644 index 000000000..5b3a09bfa --- /dev/null +++ b/core/java/src/net/i2p/client/ATalk.java @@ -0,0 +1,338 @@ +package net.i2p.client; +/* + * 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.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Properties; + +import net.i2p.I2PException; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; +import net.i2p.util.LogManager; +import net.i2p.util.Clock; + +/** + * ATalk - anonymous talk, demonstrating a trivial I2P usage scenario. + * Run this class with no arguments for a manual. + * + * @author jrandom + */ +public class ATalk implements I2PSessionListener, Runnable { + /** logging hook - status messages are piped to this */ + private final static Log _log = new Log(ATalk.class); + /** platform independent newline */ + private final static String NL = System.getProperty("line.separator"); + /** the current session */ + private I2PSession _session; + /** who am i */ + private Destination _myDestination; + /** who are you? */ + private Destination _peerDestination; + /** location of my secret key file */ + private String _myKeyFile; + /** location of their public key */ + private String _theirDestinationFile; + /** where the application reads input from. currently set to standard input */ + private BufferedReader _in; + /** where the application sends output to. currently set to standard output */ + private BufferedWriter _out; + /** string that messages must begin with to be treated as files */ + private final static String FILE_COMMAND = ".file: "; + /** the, erm, manual */ + private final static String MANUAL = + "ATalk: Anonymous Talk, a demo program for the Invisible Internet Project SDK" + NL + + "To generate a new destination:" + NL + + "\tATalk [fileToSavePrivateKeyIn] [fileToSavePublicKeyIn]" + NL + + "To talk to another destination:" + NL + + "\tATalk [myPrivateKeyFile] [peerPublicKey] [shouldLogToScreen]" + NL + + "shouldLogToScreen is 'true' or 'false', depending on whether you want log info on the screen" + NL + + "When talking to another destination, messages are sent after you hit return" + NL + + "To send a file, send a message saying:" + NL + + "\t" + FILE_COMMAND + "[filenameToSend]" + NL + + "The peer will then recieve the file and be notified of where it has been saved" + NL + + "To end the talk session, enter a period on a line by itself and hit return" + NL; + public final static String PROP_CONFIG_LOCATION = "configFile"; + private static final SimpleDateFormat _fmt = new SimpleDateFormat("hh:mm:ss.SSS"); + + /** Construct the talk engine, but don't connect yet */ + public ATalk(String myKeyFile, String theirDestFile) { + _myKeyFile = myKeyFile; + _theirDestinationFile = theirDestFile; + } + + /** Actually start up the connection to the I2P network. + * Successful connect does not mean the peer is online or reachable. + * + * @throws IOException if there is a problem reading in the keys from the files specified + * @throws DataFormatException if the key files are not in the valid format + * @throws I2PSessionException if there is a problem contacting the I2P router + */ + public void connect() throws IOException, I2PSessionException, DataFormatException { + I2PClient client = I2PClientFactory.createClient(); + File myFile = new File(_myKeyFile); + Properties props = new Properties(); + String configLocation = System.getProperty(PROP_CONFIG_LOCATION, "atalk.config"); + try { + props.load(new FileInputStream(configLocation)); + } catch (FileNotFoundException fnfe) { + _log.warn("Unable to load up the ATalk config file " + configLocation); + } + // Provide any router or client API configuration here. + if (!props.containsKey(I2PClient.PROP_TCP_HOST)) + props.setProperty(I2PClient.PROP_TCP_HOST, "localhost"); + if (!props.containsKey(I2PClient.PROP_TCP_PORT)) + props.setProperty(I2PClient.PROP_TCP_PORT, "7654"); + if (!props.containsKey(I2PClient.PROP_RELIABILITY)) + props.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT); + _session = client.createSession(new FileInputStream(myFile), props); + _session.setSessionListener(this); + _session.connect(); + + File peerDestFile = new File(_theirDestinationFile); + _peerDestination = new Destination(); + _peerDestination.readBytes(new FileInputStream(peerDestFile)); + return; + } + + /** Actual bulk processing of the application, reading in user input, + * sending messages, and displaying results. When this function exits, the + * application is complete. + * + */ + public void run() { + try { + connect(); + _in = new BufferedReader(new InputStreamReader(System.in)); + _out = new BufferedWriter(new OutputStreamWriter(System.out)); + + _out.write("Starting up anonymous talk session"+NL); + + while (true) { + String line = _in.readLine(); + if ( (line == null) || (line.trim().length() <= 0) ) + continue; + if (".".equals(line)) { + boolean ok = _session.sendMessage(_peerDestination, ("Peer disconnected at " + now()).getBytes()); + // ignore ok, we're closing + break; + } + if (line.startsWith(FILE_COMMAND) && (line.trim().length() > FILE_COMMAND.length())) { + try { + String file = line.substring(FILE_COMMAND.length()); + boolean sent = sendFile(file); + if (!sent) { + _out.write("Failed sending the file: " + file+NL); + } + } catch (IOException ioe) { + _out.write("Error sending the file: " + ioe.getMessage()+NL); + _log.error("Error sending the file", ioe); + } + } else { + boolean ok = _session.sendMessage(_peerDestination, ("[" + now() + "] " + line).getBytes()); + if (!ok) { + _out.write("Failed sending message. Peer disconnected?" + NL); + } + } + } + } catch (IOException ioe) { + _log.error("Error running", ioe); + } catch (I2PSessionException ise) { + _log.error("Error communicating", ise); + } catch (DataFormatException dfe) { + _log.error("Peer destination file is not valid", dfe); + } finally { + try { + _log.debug("Exiting anonymous talk session"); + if (_out != null) + _out.write("Exiting anonymous talk session"); + } catch (IOException ioe) { + // ignored + } + if (_session != null) { + try { + _session.destroySession(); + } catch (I2PSessionException ise) { + // ignored + } + } + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + } + } + + private String now() { + Date now = new Date(Clock.getInstance().now()); + return _fmt.format(now); + } + + /** Send the given file to the current peer. This works by sending a message + * saying ".file: filename\nbodyOfFile", where filename is the name of the file + * (which the recipient will be shown), and the bodyOfFile is the set of raw + * bytes in the file. + * + * @throws IOException if the file could not be found or read + * @return false if the file could not be sent to the peer + */ + private boolean sendFile(String filename) throws IOException, I2PSessionException { + _log.debug("Sending file [" + filename + "]"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); + baos.write((FILE_COMMAND + filename+"\n").getBytes()); + FileInputStream fin = new FileInputStream(filename); + byte buf[] = new byte[4096]; + try { + while (true) { + int len = fin.read(buf); + if (len == -1) + break; + baos.write(buf, 0, len); + } + } catch (IOException ioe) { + _log.debug("Failed reading the file", ioe); + return false; + } + baos.close(); + byte val[] = baos.toByteArray(); + _log.debug("Sending " + filename + " with a full payload of " + val.length); + try { + boolean rv = _session.sendMessage(_peerDestination, val); + _log.debug("Sending " + filename + " complete: rv = " + rv); + return rv; + } catch (Throwable t) { + _log.error("Error sending file", t); + return false; + } + } + + /** I2PSessionListener.messageAvailable requires this method to be called whenever + * I2P wants to tell the session that a message is available. ATalk always grabs + * the message immediately and either processes it as a "send file" command (passing + * it off to {@link #handleRecieveFile handleRecieveFile}) or simply displays the + * message to the user. + * + */ + public void messageAvailable(I2PSession session, int msgId, long size) { + _log.debug("Message available: id = " + msgId + " size = " + size); + try { + byte msg[] = session.receiveMessage(msgId); + // inefficient way to just read the first line of text, but its easy + BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(msg))); + String line = reader.readLine(); + if (line.startsWith(FILE_COMMAND)) { + handleRecieveFile(line, msg); + } else { + // not a file command, so just plop 'er out on the screen + _out.write(now() + " --> " + new String(msg)); + _out.write(NL); + _out.flush(); + } + } catch (I2PSessionException ise) { + _log.error("Error fetching available message", ise); + } catch (IOException ioe) { + _log.error("Error writing out the message", ioe); + } + } + + /** React to a file being sent our way from the peer via {@link #sendFile sendFile} + * by saving the file to a temporary location and displaying where, what, and how large + * it is. + * + * @param firstline the first line of the message that, according to the sendFile + * implementation, contains the command and the filename that it was stored + * at on the peer's computer + * @param msg the entire message recieved, including the firstline + */ + private void handleRecieveFile(String firstline, byte msg[]) throws IOException { + _log.debug("handleRecieveFile called"); + File f = File.createTempFile("recieve", ".dat", new File(".")); + FileOutputStream fos = new FileOutputStream(f); + int lineLen = firstline.getBytes().length+"\n".getBytes().length; + int lenToCopy = msg.length - lineLen; + byte buf[] = new byte[lenToCopy]; + System.arraycopy(msg, lineLen, buf, 0, lenToCopy); + fos.write(buf); + fos.close(); + String name = firstline.substring(FILE_COMMAND.length()); + _out.write("Recieved a file called [" + name + "] of size [" + lenToCopy + "] bytes, saved as [" + f.getAbsolutePath() + "]" + NL); + _out.flush(); + } + + /** driver */ + public static void main(String args[]) { + if (args.length == 2) { + String myKeyFile = args[0]; + String myDestinationFile = args[1]; + boolean success = generateKeys(myKeyFile, myDestinationFile); + if (success) + _log.debug("Keys generated (private key file: " + myKeyFile + " destination file: " + myDestinationFile + ")"); + else + _log.debug("Keys generation failed"); + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + } else if (args.length == 3) { + _log.debug("Starting chat"); + String myKeyfile = args[0]; + String peerDestFile = args[1]; + String shouldLog = args[2]; + if (Boolean.TRUE.toString().equalsIgnoreCase(shouldLog)) + LogManager.getInstance().setDisplayOnScreen(true); + else + LogManager.getInstance().setDisplayOnScreen(false); + String logFile = args[2]; + Thread talkThread = new I2PThread(new ATalk(myKeyfile, peerDestFile)); + talkThread.start(); + } else { + System.out.println(MANUAL); + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + System.exit(-1); + } + } + + /** Generate a new Destination, saving that destination and the associated + * private keys in the privKeyFile, and also saving the destination without + * any private keys to destinationFile. + * + * @param privKeyFile private key file, including the destination and the various + * private keys, as defined by {@link I2PClient#createDestination I2PClient.createDestination} + * @param destinationFile file in which the Destination is serialized in + */ + private static boolean generateKeys(String privKeyFile, String destinationFile) { + try { + Destination d = I2PClientFactory.createClient().createDestination(new FileOutputStream(privKeyFile)); + FileOutputStream fos = new FileOutputStream(destinationFile); + d.writeBytes(fos); + fos.flush(); + fos.close(); + return true; + } catch (IOException ioe) { + _log.error("Error generating keys", ioe); + } catch (I2PException ipe) { + _log.error("Error generating keys", ipe); + } + return false; + } + + /** required by {@link I2PSessionListener I2PSessionListener} to notify of disconnect */ + public void disconnected(I2PSession session) { _log.debug("Disconnected"); } + /** required by {@link I2PSessionListener I2PSessionListener} to notify of error */ + public void errorOccurred(I2PSession session, String message, Throwable error) { _log.debug("Error occurred: " + message, error); } + /** required by {@link I2PSessionListener I2PSessionListener} to notify of abuse */ + public void reportAbuse(I2PSession session, int severity) { _log.debug("Abuse reported of severity " + severity); } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/ConnectionRunner.java b/core/java/src/net/i2p/client/ConnectionRunner.java new file mode 100644 index 000000000..a63ae6454 --- /dev/null +++ b/core/java/src/net/i2p/client/ConnectionRunner.java @@ -0,0 +1,375 @@ +package net.i2p.client; +/* + * 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 net.i2p.util.Log; +import net.i2p.util.Clock; + +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.I2CPMessageReader; +import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.data.i2cp.SessionStatusMessage; +import net.i2p.data.i2cp.SendMessageMessage; +import net.i2p.data.i2cp.CreateSessionMessage; +import net.i2p.data.i2cp.MessageStatusMessage; +import net.i2p.data.i2cp.DisconnectMessage; +import net.i2p.data.i2cp.ReceiveMessageBeginMessage; +import net.i2p.data.i2cp.ReceiveMessageEndMessage; +import net.i2p.data.i2cp.MessagePayloadMessage; +import net.i2p.data.i2cp.RequestLeaseSetMessage; +import net.i2p.data.i2cp.SessionId; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.SessionConfig; + +import net.i2p.data.Destination; +import net.i2p.data.Payload; +import net.i2p.data.TunnelId; +import net.i2p.data.RouterIdentity; +import net.i2p.data.PublicKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.data.Certificate; + +import net.i2p.crypto.KeyGenerator; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.HashSet; +import java.util.Collections; +import java.util.Iterator; +import java.util.Date; + +import java.net.Socket; + +import java.io.OutputStream; +import java.io.IOException; + +/** + * Run the server side of a connection as part of the TestServer. This class + * actually manages the state of that system too, but this is a very, very, very + * rudimentary implementation. And not a very clean one at that. + * + * @author jrandom + */ +class ConnectionRunner implements I2CPMessageReader.I2CPMessageEventListener { + private final static Log _log = new Log(ConnectionRunner.class); + /** + * static mapping of Destination to ConnectionRunner, allowing connections to pass + * messages to each other + */ + private static Map _connections = Collections.synchronizedMap(new HashMap()); + /** + * static mapping of MessageId to Payload, storing messages for retrieval + * + */ + private static Map _messages = Collections.synchronizedMap(new HashMap()); + /** socket for this particular peer connection */ + private Socket _socket; + /** + * output stream of the socket that I2CP messages bound to the client + * should be written to + */ + private OutputStream _out; + /** session ID of the current client */ + private SessionId _sessionId; + /** next available session id */ + private static int _id = 0; + /** next available message id */ + private static int _messageId = 0; + private SessionConfig _config; + + private Object _sessionIdLock = new Object(); + private Object _messageIdLock = new Object(); + // this *should* be mod 65536, but UnsignedInteger is still b0rked. FIXME + protected int getNextSessionId() { synchronized (_sessionIdLock) { int id = (++_id)%32767; _id = id; return id; } } + // this *should* be mod 65536, but UnsignedInteger is still b0rked. FIXME + protected int getNextMessageId() { synchronized (_messageIdLock) { int id = (++_messageId)%32767; _messageId = id; return id; } } + protected SessionId getSessionId() { return _sessionId; } + + protected ConnectionRunner getRunner(Destination dest) { + return (ConnectionRunner)_connections.get(dest); + } + protected Set getRunnerDestinations() { + return new HashSet(_connections.keySet()); + } + + /** + * Create a new runner against the given socket + * + */ + public ConnectionRunner(Socket socket) { + _socket = socket; + _config = null; + } + + /** + * Actually run the connection - listen for I2CP messages and respond. This + * is the main driver for this class, though it gets all its meat from the + * {@link net.invisiblenet.i2p.data.i2cp.I2CPMessageReader I2CPMessageReader} + * + */ + public void doYourThing() throws IOException { + I2CPMessageReader reader = new I2CPMessageReader(_socket.getInputStream(), this); + _out = _socket.getOutputStream(); + reader.startReading(); + } + + /** + * Recieve notifiation that the peer disconnected + */ + public void disconnected(I2CPMessageReader reader) { + _log.info("Disconnected"); + } + + /** + * Handle an incoming message and dispatch it to the appropriate handler + * + */ + public void messageReceived(I2CPMessageReader reader, I2CPMessage message) { + _log.info("Message recieved: \n" + message); + switch (message.getType()) { + case CreateSessionMessage.MESSAGE_TYPE: + handleCreateSession(reader, (CreateSessionMessage)message); + break; + case SendMessageMessage.MESSAGE_TYPE: + handleSendMessage(reader, (SendMessageMessage)message); + break; + case ReceiveMessageBeginMessage.MESSAGE_TYPE: + handleReceiveBegin(reader, (ReceiveMessageBeginMessage)message); + break; + case ReceiveMessageEndMessage.MESSAGE_TYPE: + handleReceiveEnd(reader, (ReceiveMessageEndMessage)message); + break; + } + } + + /** + * Handle a CreateSessionMessage + * + */ + protected void handleCreateSession(I2CPMessageReader reader, CreateSessionMessage message) { + if (message.getSessionConfig().verifySignature()) { + _log.debug("Signature verified correctly on create session message"); + } else { + _log.error("Signature verification *FAILED* on a create session message. Hijack attempt?"); + DisconnectMessage msg = new DisconnectMessage(); + msg.setReason("Invalid signature on CreateSessionMessage"); + try { + doSend(msg); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the disconnect message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the disconnect message", ioe); + } + return; + } + SessionStatusMessage msg = new SessionStatusMessage(); + SessionId id = new SessionId(); + id.setSessionId(getNextSessionId()); // should be mod 65535, but UnsignedInteger isn't fixed yet. FIXME. + _sessionId = id; + msg.setSessionId(id); + msg.setStatus(SessionStatusMessage.STATUS_CREATED); + try { + doSend(msg); + _connections.put(message.getSessionConfig().getDestination(), this); + _config = message.getSessionConfig(); + sessionCreated(); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the session status message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the session status message", ioe); + } + + // lets also request a new fake lease + RequestLeaseSetMessage rlsm = new RequestLeaseSetMessage(); + rlsm.setEndDate(new Date(Clock.getInstance().now() + 60*60*1000)); + rlsm.setSessionId(id); + RouterIdentity ri = new RouterIdentity(); + Object rikeys[] = KeyGenerator.getInstance().generatePKIKeypair(); + Object riSigningkeys[] = KeyGenerator.getInstance().generateSigningKeypair(); + ri.setCertificate(new Certificate(Certificate.CERTIFICATE_TYPE_NULL, null)); + ri.setPublicKey((PublicKey)rikeys[0]); + ri.setSigningPublicKey((SigningPublicKey)riSigningkeys[0]); + TunnelId tunnel = new TunnelId(); + tunnel.setTunnelId(42); + rlsm.addEndpoint(ri, tunnel); + try { + doSend(rlsm); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the request for a lease set", ime); + } catch (IOException ioe) { + _log.error("Error writing out the request for a lease set", ioe); + } + + } + + protected void sessionCreated() { } + protected SessionConfig getConfig() { return _config; } + + /** + * Handle a SendMessageMessage + * + */ + protected void handleSendMessage(I2CPMessageReader reader, SendMessageMessage message) { + _log.debug("handleSendMessage called"); + Payload payload = message.getPayload(); + Destination dest = message.getDestination(); + MessageId id = new MessageId(); + id.setMessageId(getNextMessageId()); + _log.debug("** Recieving message [" + id.getMessageId() + "] with payload: " + "[" + payload + "]"); + _messages.put(id, payload); + MessageStatusMessage status = new MessageStatusMessage(); + status.setMessageId(id); + status.setSessionId(message.getSessionId()); + status.setSize(0L); + status.setNonce(message.getNonce()); + status.setStatus(MessageStatusMessage.STATUS_SEND_ACCEPTED); + try { + doSend(status); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the message status message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the message status message", ioe); + } + distributeMessageToPeer(status, dest, id); + } + + /** + * distribute the message to the destination, passing on the appropriate status + * messages to the sender of the SendMessageMessage + * + */ + private void distributeMessageToPeer(MessageStatusMessage status, Destination dest, MessageId id) { + ConnectionRunner runner = (ConnectionRunner)_connections.get(dest); + if (runner == null) { + distributeNonLocal(status, dest, id); + } else { + distributeLocal(runner, status, dest, id); + } + _log.debug("Done handling send message"); + } + + protected void distributeLocal(ConnectionRunner runner, MessageStatusMessage status, Destination dest, MessageId id) { + if (runner.messageAvailable(id, 0L)) { + status.setStatus(MessageStatusMessage.STATUS_SEND_GUARANTEED_SUCCESS); + status.setNonce(2); + try { + doSend(status); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the success status message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the success status message", ioe); + } + _log.debug("Guaranteed success with the status message sent"); + } else { + status.setStatus(MessageStatusMessage.STATUS_SEND_GUARANTEED_FAILURE); + try { + doSend(status); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the failure status message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the failure status message", ioe); + } + _log.debug("Guaranteed failure since messageAvailable failed"); + } + } + + protected void distributeNonLocal(MessageStatusMessage status, Destination dest, MessageId id) { + status.setStatus(MessageStatusMessage.STATUS_SEND_GUARANTEED_FAILURE); + try { + doSend(status); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the failure status message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the failure status message", ioe); + } + _log.debug("Guaranteed failure!"); + } + + /** + * The client asked for a message, so we send it to them. This currently + * does not do any security checking (like making sure they're the one to + * whom the message ID is destined, but its encrypted, so why not... + * (bad attitude, I know. consider this a bug to be fixed) + * + */ + public void handleReceiveBegin(I2CPMessageReader reader, ReceiveMessageBeginMessage message) { + _log.debug("Handling recieve begin: id = " + message.getMessageId()); + MessagePayloadMessage msg = new MessagePayloadMessage(); + msg.setMessageId(message.getMessageId()); + msg.setSessionId(_sessionId); + Payload payload = (Payload)_messages.get(message.getMessageId()); + if (payload == null) { + _log.error("Payload for message id [" + message.getMessageId() + "] is null! Unknown message id?", new Exception("Error, null payload")); + StringBuffer buf = new StringBuffer(); + for (Iterator iter = _messages.keySet().iterator(); iter.hasNext(); ) { + buf.append("messageId: ").append(iter.next()).append(", "); + } + _log.error("Known message IDs: " + buf.toString()); + return; + } + msg.setPayload(payload); + try { + doSend(msg); + } catch (IOException ioe) { + _log.error("Error delivering the payload", ioe); + } catch (I2CPMessageException ime) { + _log.error("Error delivering the payload", ime); + } + } + + /** + * The client told us that the message has been recieved completely. This currently + * does not do any security checking prior to removing the message from the + * pending queue, though it should. + * + */ + public void handleReceiveEnd(I2CPMessageReader reader, ReceiveMessageEndMessage message) { + _messages.remove(message.getMessageId()); + } + + /** + * Deliver notification to the client that the given message is available. + * This is called from the ConnectionRunner the message was sent from. + * + */ + public boolean messageAvailable(MessageId id, long size) { + MessageStatusMessage msg = new MessageStatusMessage(); + msg.setMessageId(id); + msg.setSessionId(_sessionId); + msg.setSize(size); + msg.setNonce(1); + msg.setStatus(MessageStatusMessage.STATUS_AVAILABLE); + try { + doSend(msg); + return true; + } catch (I2CPMessageException ime) { + _log.error("Error writing out the message status message", ime); + } catch (IOException ioe) { + _log.error("Error writing out the message status message", ioe); + } + return false; + } + + /** + * Handle notifiation that there was an error + * + */ + public void readError(I2CPMessageReader reader, Exception error) { + _log.info("Error occurred", error); + } + + private Object _sendLock = new Object(); + protected void doSend(I2CPMessage msg) throws I2CPMessageException, IOException { + synchronized (_sendLock) { + msg.writeMessage(_out); + _out.flush(); + } + } +} diff --git a/core/java/src/net/i2p/client/DisconnectMessageHandler.java b/core/java/src/net/i2p/client/DisconnectMessageHandler.java new file mode 100644 index 000000000..b757317e6 --- /dev/null +++ b/core/java/src/net/i2p/client/DisconnectMessageHandler.java @@ -0,0 +1,26 @@ +package net.i2p.client; +/* + * 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 net.i2p.data.i2cp.*; + +/** + * Handle I2CP disconnect messages from the router + * + * @author jrandom + */ +class DisconnectMessageHandler extends HandlerImpl { + public DisconnectMessageHandler() { + super(DisconnectMessage.MESSAGE_TYPE); + } + public void handleMessage(I2CPMessage message, I2PSessionImpl session) { + _log.debug("Handle message " + message); + session.destroySession(false); + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/HandlerImpl.java b/core/java/src/net/i2p/client/HandlerImpl.java new file mode 100644 index 000000000..c38e94937 --- /dev/null +++ b/core/java/src/net/i2p/client/HandlerImpl.java @@ -0,0 +1,26 @@ +package net.i2p.client; +/* + * 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 net.i2p.util.Log; + +/** + * Base class for handling I2CP messages + * + * @author jrandom + */ +abstract class HandlerImpl implements I2CPMessageHandler { + protected Log _log; + private int _type; + public HandlerImpl(int type) { + _type = type; + _log = new Log(getClass()); + } + public int getType() { return _type; } +} diff --git a/core/java/src/net/i2p/client/I2CPMessageHandler.java b/core/java/src/net/i2p/client/I2CPMessageHandler.java new file mode 100644 index 000000000..ddaa760d7 --- /dev/null +++ b/core/java/src/net/i2p/client/I2CPMessageHandler.java @@ -0,0 +1,21 @@ +package net.i2p.client; +/* + * 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 net.i2p.data.i2cp.I2CPMessage; + +/** + * Define a way to handle a particular type of message + * + * @author jrandom + */ +interface I2CPMessageHandler { + public int getType(); + public void handleMessage(I2CPMessage message, I2PSessionImpl session); +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/I2CPMessageProducer.java b/core/java/src/net/i2p/client/I2CPMessageProducer.java new file mode 100644 index 000000000..03e1b88bf --- /dev/null +++ b/core/java/src/net/i2p/client/I2CPMessageProducer.java @@ -0,0 +1,153 @@ +package net.i2p.client; +/* + * 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.util.HashSet; +import java.util.Set; + +import net.i2p.crypto.ElGamalAESEngine; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.data.LeaseSet; +import net.i2p.data.Payload; +import net.i2p.data.PrivateKey; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.i2cp.AbuseReason; +import net.i2p.data.i2cp.AbuseSeverity; +import net.i2p.data.i2cp.CreateLeaseSetMessage; +import net.i2p.data.i2cp.CreateSessionMessage; +import net.i2p.data.i2cp.DestroySessionMessage; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.ReportAbuseMessage; +import net.i2p.data.i2cp.SendMessageMessage; +import net.i2p.data.i2cp.SessionConfig; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; + +/** + * Produce the various messages the session needs to send to the router. + * + * @author jrandom + */ +class I2CPMessageProducer { + private final static Log _log = new Log(I2CPMessageProducer.class); + private final static RandomSource _rand = RandomSource.getInstance(); + + /** + * Send all the messages that a client needs to send to a router to establish + * a new session. + */ + public void connect(I2PSessionImpl session) throws I2PSessionException { + CreateSessionMessage msg = new CreateSessionMessage(); + SessionConfig cfg = new SessionConfig(); + cfg.setDestination(session.getMyDestination()); + cfg.setOptions(session.getOptions()); + try { + cfg.signSessionConfig(session.getPrivateKey()); + } catch (DataFormatException dfe) { + throw new I2PSessionException("Unable to sign the session config", dfe); + } + msg.setSessionConfig(cfg); + session.sendMessage(msg); + } + + /** + * Send messages to the router destroying the session and disconnecting + * + */ + public void disconnect(I2PSessionImpl session) throws I2PSessionException { + DestroySessionMessage dmsg = new DestroySessionMessage(); + dmsg.setSessionId(session.getSessionId()); + session.sendMessage(dmsg); + // use DisconnectMessage only if we fail and drop connection... + // todo: update the code to fire off DisconnectMessage on socket error + //DisconnectMessage msg = new DisconnectMessage(); + //msg.setReason("Destroy called"); + //session.sendMessage(msg); + } + + /** + * Package up and send the payload to the router for delivery + * + */ + public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, SessionTag tag, SessionKey key, Set tags, SessionKey newKey) throws I2PSessionException { + SendMessageMessage msg = new SendMessageMessage(); + msg.setDestination(dest); + msg.setSessionId(session.getSessionId()); + msg.setNonce(nonce); + Payload data = createPayload(dest, payload, tag, key, tags, newKey); + msg.setPayload(data); + session.sendMessage(msg); + } + + /** + * Create a new signed payload and send it off to the destination + * + */ + private Payload createPayload(Destination dest, byte[] payload, SessionTag tag, SessionKey key, Set tags, SessionKey newKey) throws I2PSessionException { + if (dest == null) + throw new I2PSessionException("No destination specified"); + if (payload == null) + throw new I2PSessionException("No payload specified"); + + Payload data = new Payload(); + // randomize padding + int size = payload.length + RandomSource.getInstance().nextInt(1024); + byte encr[] = ElGamalAESEngine.encrypt(payload, dest.getPublicKey(), key, tags, tag, newKey, size); + // yes, in an intelligent component, newTags would be queued for confirmation along with key, and + // generateNewTags would only generate tags if necessary + + data.setEncryptedData(encr); + _log.debug("Encrypting the payload to public key " + dest.getPublicKey().toBase64() + "\nPayload: " + data.calculateHash()); + return data; + } + + private static Set generateNewTags() { + Set tags = new HashSet(); + for (int i = 0; i < 10; i++) { + byte tag[] = new byte[SessionTag.BYTE_LENGTH]; + RandomSource.getInstance().nextBytes(tag); + tags.add(new SessionTag(tag)); + } + return tags; + } + + /** + * Send an abuse message to the router + */ + public void reportAbuse(I2PSessionImpl session, int msgId, int severity) throws I2PSessionException { + ReportAbuseMessage msg = new ReportAbuseMessage(); + MessageId id = new MessageId(); + id.setMessageId(msgId); + msg.setMessageId(id); + AbuseReason reason = new AbuseReason(); + reason.setReason("Not specified"); + msg.setReason(reason); + AbuseSeverity sv = new AbuseSeverity(); + sv.setSeverity(severity); + msg.setSeverity(sv); + session.sendMessage(msg); + } + + /** + * Create a new signed leaseSet in response to a request to do so and send it + * to the router + * + */ + public void createLeaseSet(I2PSessionImpl session, LeaseSet leaseSet, SigningPrivateKey signingPriv, PrivateKey priv) throws I2PSessionException { + CreateLeaseSetMessage msg = new CreateLeaseSetMessage(); + msg.setLeaseSet(leaseSet); + msg.setPrivateKey(priv); + msg.setSigningPrivateKey(signingPriv); + msg.setSessionId(session.getSessionId()); + session.sendMessage(msg); + } +} diff --git a/core/java/src/net/i2p/client/I2PClient.java b/core/java/src/net/i2p/client/I2PClient.java new file mode 100644 index 000000000..794221cf1 --- /dev/null +++ b/core/java/src/net/i2p/client/I2PClient.java @@ -0,0 +1,67 @@ +package net.i2p.client; +/* + * 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 net.i2p.I2PException; +import net.i2p.data.Destination; +import net.i2p.data.Certificate; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import java.util.Properties; + +/** + * Define the standard means of interacting with the I2P system + * + * @author jrandom + */ +public interface I2PClient { + /** Standard host property, defaulting to localhost if not specified */ + public final static String PROP_TCP_HOST = "i2cp.tcp.host"; + /** Standard port number property */ + public final static String PROP_TCP_PORT = "i2cp.tcp.port"; + /** Reliability property */ + public final static String PROP_RELIABILITY = "i2cp.messageReliability"; + /** Reliability value: best effort */ + public final static String PROP_RELIABILITY_BEST_EFFORT = "BestEffort"; + /** Reliability value: guaranteed */ + public final static String PROP_RELIABILITY_GUARANTEED = "Guaranteed"; + + /** protocol flag that must be sent when opening the i2cp connection to the router */ + public final static int PROTOCOL_BYTE = 0x2A; + + /** Create a new client session for the Destination stored at the destKeyStream + * using the specified options to both connect to the router, to instruct + * the router how to handle the new session, and to configure the end to end + * encryption. + * @param destKeyStream location from which to read the Destination, PrivateKey, and SigningPrivateKey from + * @param options set of options to configure the router with + * @return new session allowing a Destination to recieve all of its messages and send messages to any other Destination. + */ + public I2PSession createSession(InputStream destKeyStream, Properties options) throws I2PSessionException; + + /** Create a new destination with the default certificate creation properties and store + * it, along with the private encryption and signing keys at the specified location + * @param destKeyStream create a new destination and write out the object to the given stream, + * formatted as Destination, PrivateKey, and SigningPrivateKey + * @return new destination + */ + public Destination createDestination(OutputStream destKeyStream) throws I2PException, IOException; + + /** Create a new destination with the given certificate and store it, along with the private + * encryption and signing keys at the specified location + * + * @param destKeyStream location to write out the destination, PrivateKey, and SigningPrivateKey + * @param cert certificate to tie to the destination + * @return newly created destination + */ + public Destination createDestination(OutputStream destKeyStream, Certificate cert) throws I2PException, IOException; +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/I2PClientFactory.java b/core/java/src/net/i2p/client/I2PClientFactory.java new file mode 100644 index 000000000..d9fa216bc --- /dev/null +++ b/core/java/src/net/i2p/client/I2PClientFactory.java @@ -0,0 +1,23 @@ +package net.i2p.client; +/* + * 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. + * + */ + +/** + * Provide a means of hooking into an appropriate I2PClient implementation + * + * @author jrandom + */ +public class I2PClientFactory { + /** Create a new instance of the appropriate I2PClient + * @return client implementation + */ + public static I2PClient createClient() { + return new I2PClientImpl(); + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/I2PClientImpl.java b/core/java/src/net/i2p/client/I2PClientImpl.java new file mode 100644 index 000000000..de01de514 --- /dev/null +++ b/core/java/src/net/i2p/client/I2PClientImpl.java @@ -0,0 +1,75 @@ +package net.i2p.client; +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; + +import net.i2p.I2PException; +import net.i2p.crypto.KeyGenerator; +import net.i2p.data.Certificate; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; + +/** + * Base client implementation + * + * @author jrandom + */ +class I2PClientImpl implements I2PClient { + /** + * Create the destination with a null payload + */ + public Destination createDestination(OutputStream destKeyStream) throws I2PException, IOException { + Certificate cert = new Certificate(); + cert.setCertificateType(Certificate.CERTIFICATE_TYPE_NULL); + cert.setPayload(null); + return createDestination(destKeyStream, cert); + } + + /** + * Create the destination with the given payload and write it out along with + * the PrivateKey and SigningPrivateKey to the destKeyStream + * + */ + public Destination createDestination(OutputStream destKeyStream, Certificate cert) throws I2PException, IOException { + Destination d = new Destination(); + d.setCertificate(cert); + PublicKey publicKey = new PublicKey(); + Object keypair[] = KeyGenerator.getInstance().generatePKIKeypair(); + publicKey = (PublicKey)keypair[0]; + PrivateKey privateKey = (PrivateKey)keypair[1]; + Object signingKeys[] = KeyGenerator.getInstance().generateSigningKeypair(); + SigningPublicKey signingPubKey = (SigningPublicKey)signingKeys[0]; + SigningPrivateKey signingPrivKey = (SigningPrivateKey)signingKeys[1]; + d.setPublicKey(publicKey); + d.setSigningPublicKey(signingPubKey); + + d.writeBytes(destKeyStream); + privateKey.writeBytes(destKeyStream); + signingPrivKey.writeBytes(destKeyStream); + destKeyStream.flush(); + + return d; + } + + /** + * Create a new session (though do not connect it yet) + * + */ + public I2PSession createSession(InputStream destKeyStream, Properties options) throws I2PSessionException { + //return new I2PSessionImpl(destKeyStream, options); // not thread safe + return new I2PSessionImpl2(destKeyStream, options); // thread safe + } +} diff --git a/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java new file mode 100644 index 000000000..e056c549a --- /dev/null +++ b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java @@ -0,0 +1,41 @@ +package net.i2p.client; +/* + * 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 net.i2p.data.i2cp.*; +import net.i2p.util.Log; + +import java.util.Map; +import java.util.HashMap; + +/** + * Contains a map of message handlers that a session will want to use + * + * @author jrandom + */ +class I2PClientMessageHandlerMap { + private final static Log _log = new Log(I2PClientMessageHandlerMap.class); + /** map of message type id --> I2CPMessageHandler */ + private static Map _handlers; + + static { + _handlers = new HashMap(); + _handlers.put(new Integer(DisconnectMessage.MESSAGE_TYPE), new DisconnectMessageHandler()); + _handlers.put(new Integer(SessionStatusMessage.MESSAGE_TYPE), new SessionStatusMessageHandler()); + _handlers.put(new Integer(RequestLeaseSetMessage.MESSAGE_TYPE), new RequestLeaseSetMessageHandler()); + _handlers.put(new Integer(MessagePayloadMessage.MESSAGE_TYPE), new MessagePayloadMessageHandler()); + _handlers.put(new Integer(MessageStatusMessage.MESSAGE_TYPE), new MessageStatusMessageHandler()); + _handlers.put(new Integer(SetDateMessage.MESSAGE_TYPE), new SetDateMessageHandler()); + } + + public static I2CPMessageHandler getHandler(int messageTypeId) { + I2CPMessageHandler handler = (I2CPMessageHandler)_handlers.get(new Integer(messageTypeId)); + return handler; + } +} diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java new file mode 100644 index 000000000..22152c0f6 --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSession.java @@ -0,0 +1,109 @@ +package net.i2p.client; +/* + * 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.util.Set; + +import net.i2p.data.Destination; +import net.i2p.data.SessionKey; +import net.i2p.data.PrivateKey; +import net.i2p.data.SigningPrivateKey; + +/** + * Define the standard means of sending and receiving messages on the + * I2P Network. + * + * @author jrandom + */ +public interface I2PSession { + /** Send a new message to the given destination, containing the specified + * payload, returning true if the router feels confident that the message + * was delivered. + * @param dest location to send the message + * @param payload body of the message to be sent (unencrypted) + * @return whether it was accepted by the router for delivery or not + */ + public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException; + /** + * Like sendMessage above, except the key used and the tags sent are exposed to the + * application, so that if some application layer message delivery confirmation is used, + * rather than i2p's (slow) built in confirmation via guaranteed delivery mode, the + * application can update the SessionKeyManager, ala: + *
+     *   SessionKeyManager.getInstance().tagsDelivered(dest.getPublicKey(), keyUsed, tagsSent);
+     * 
+ * If an application is using guaranteed delivery mode, this is not useful, but for + * applications using best effort delivery mode, if they can know with certainty that a message + * was delivered and can update the SessionKeyManager appropriately, a significant performance + * boost will occur (subsequent message encryption and decryption will be done via AES and a SessionTag, + * rather than ElGamal+AES, which is 1000x slower). + * + * @param dest location to send the message + * @param payload body of the message to be sent (unencrypted) + * @param keyUsed session key delivered to the destination for association with the tags sent. This is essentially + * an output parameter - keyUsed.getData() is ignored during this call, but after the call completes, + * it will be filled with the bytes of the session key delivered. Typically the key delivered is the + * same one as the key encrypted with, but not always. If this is null then the key data will not be + * exposed. + * @param tagsSent set of tags delivered to the peer and associated with the keyUsed. This is also an output parameter - + * the contents of the set is ignored during the call, but afterwards it contains a set of SessionTag + * objects that were sent along side the given keyUsed. + */ + public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException; + + /** Receive a message that the router has notified the client about, returning + * the payload. + * @param msgId message to fetch + * @return unencrypted body of the message + */ + public byte[] receiveMessage(int msgId) throws I2PSessionException; + + /** Instruct the router that the message received was abusive (including how + * abusive on a 1-100 scale) in the hopes the router can do something to + * minimize receiving abusive messages like that in the future. + * @param msgId message that was abusive (or -1 for not message related) + * @param severity how abusive + */ + public void reportAbuse(int msgId, int severity) throws I2PSessionException; + + /** Instruct the I2PSession where it should send event notifications + * @param lsnr listener to retrieve events + */ + public void setSessionListener(I2PSessionListener lsnr); + + /** + * Tear down the session and release any resources. + * + */ + public void destroySession() throws I2PSessionException; + + /** + * Actually connect the session and start recieving/sending messages + * + */ + public void connect() throws I2PSessionException; + + /** + * Retrieve the Destination this session serves as the endpoint for. + * Returns null if no destination is available. + * + */ + public Destination getMyDestination(); + + /** + * Retrieve the decryption PrivateKey associated with the Destination + * + */ + public PrivateKey getDecryptionKey(); + + /** + * Retrieve the signing SigningPrivateKey associated with the Destination + */ + public SigningPrivateKey getPrivateKey(); +} diff --git a/core/java/src/net/i2p/client/I2PSessionException.java b/core/java/src/net/i2p/client/I2PSessionException.java new file mode 100644 index 000000000..f69c07d03 --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSessionException.java @@ -0,0 +1,28 @@ +package net.i2p.client; +/* + * 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 net.i2p.util.Log; +import net.i2p.I2PException; + +/** + * Thrown when there is a problem doing something on the session + * + * @author jrandom + */ +public class I2PSessionException extends I2PException { + private final static Log _log = new Log(I2PSessionException.class); + + public I2PSessionException(String msg, Throwable t) { + super(msg, t); + } + public I2PSessionException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java new file mode 100644 index 000000000..31c563e44 --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -0,0 +1,492 @@ +package net.i2p.client; +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.data.LeaseSet; +import net.i2p.data.PrivateKey; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.i2cp.GetDateMessage; +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.data.i2cp.I2CPMessageReader; +import net.i2p.data.i2cp.MessagePayloadMessage; +import net.i2p.data.i2cp.SessionId; +import net.i2p.util.Clock; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Implementation of an I2P session running over TCP. This class is NOT thread safe - + * only one thread should send messages at any given time + * + * @author jrandom + */ +abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessageEventListener { + private final static Log _log = new Log(I2PSessionImpl.class); + /** who we are */ + private Destination _myDestination; + /** private key for decryption */ + private PrivateKey _privateKey; + /** private key for signing */ + private SigningPrivateKey _signingPrivateKey; + /** configuration options */ + private Properties _options; + /** this session's Id */ + private SessionId _sessionId; + /** currently granted lease set, or null */ + private LeaseSet _leaseSet; + + /** hostname of router */ + private String _hostname; + /** port num to router */ + private int _portNum; + /** socket for comm */ + private Socket _socket; + /** reader that always searches for messages */ + private I2CPMessageReader _reader; + /** where we pipe our messages */ + private OutputStream _out; + + /** who we send events to */ + private I2PSessionListener _sessionListener; + + /** class that generates new messages */ + protected I2CPMessageProducer _producer; + /** map of integer --> MessagePayloadMessage */ + Map _availableMessages; + + /** MessageStatusMessage status from the most recent send that hasn't been consumed */ + private List _receivedStatus; + private int _totalReconnectAttempts; + + /** monitor for waiting until a lease set has been granted */ + private Object _leaseSetWait = new Object(); + + /** whether the session connection has already been closed (or not yet opened) */ + private boolean _closed; + + /** have we received the current date from the router yet? */ + private boolean _dateReceived; + /** lock that we wait upon, that the SetDateMessageHandler notifies */ + private Object _dateReceivedLock = new Object(); + + void dateUpdated() { + _dateReceived = true; + synchronized (_dateReceivedLock) { + _dateReceivedLock.notifyAll(); + } + } + + /** + * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey + * from the destKeyStream, and using the specified options to connect to the router + * + * @throws I2PSessionException if there is a problem loading the private keys or + */ + public I2PSessionImpl(InputStream destKeyStream, Properties options) throws I2PSessionException { + _closed = true; + _producer = new I2CPMessageProducer(); + _availableMessages = new HashMap(); + try { + readDestination(destKeyStream); + } catch (DataFormatException dfe) { + throw new I2PSessionException("Error reading the destination key stream", dfe); + } catch (IOException ioe) { + throw new I2PSessionException("Error reading the destination key stream", ioe); + } + loadConfig(options); + _sessionId = null; + _receivedStatus = new LinkedList(); + _leaseSet = null; + _totalReconnectAttempts = 0; + } + + /** + * Parse the config for anything we know about + * + */ + private void loadConfig(Properties options) { + _options = new Properties(); + _options.putAll(filter(options)); + _hostname = _options.getProperty(I2PClient.PROP_TCP_HOST, "localhost"); + String portNum = _options.getProperty(I2PClient.PROP_TCP_PORT, TestServer.LISTEN_PORT+""); + try { + _portNum = Integer.parseInt(portNum); + } catch (NumberFormatException nfe) { + if (_log.shouldLog(Log.WARN)) _log.warn("Invalid port number specified, defaulting to "+TestServer.LISTEN_PORT, nfe); + _portNum = TestServer.LISTEN_PORT; + } + } + + private static Properties filter(Properties options) { + Properties rv = new Properties(); + for (Iterator iter = options.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = options.getProperty(key); + if (key.startsWith("java")) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping java.* property: " + key); + } else if (key.startsWith("user")) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping user.* property: " + key); + } else if (key.startsWith("os")) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping os.* property: " + key); + } else if (key.startsWith("sun")) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping sun.* property: " + key); + } else if (key.startsWith("file")) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping file.* property: " + key); + } else if (key.startsWith("line")) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping line.* property: " + key); + } else if ( (key.length() > 255) || (val.length() > 255) ) { + if (_log.shouldLog(Log.WARN)) _log.warn("Not passing on property [" + key + "] in the session configuration as the value is too long (max = 255): " + val); + } else { + rv.setProperty(key, val); + } + } + return rv; + } + + void setLeaseSet(LeaseSet ls) { + _leaseSet = ls; + if (ls != null) { + synchronized (_leaseSetWait) { + _leaseSetWait.notifyAll(); + } + } + } + LeaseSet getLeaseSet() { return _leaseSet; } + + /** + * Load up the destKeyFile for our Destination, PrivateKey, and SigningPrivateKey + * + * @throws DataFormatException if the file is in the wrong format or keys are invalid + * @throws IOException if there is a problem reading the file + */ + private void readDestination(InputStream destKeyStream) throws DataFormatException, IOException { + _myDestination = new Destination(); + _privateKey = new PrivateKey(); + _signingPrivateKey = new SigningPrivateKey(); + _myDestination.readBytes(destKeyStream); + _privateKey.readBytes(destKeyStream); + _signingPrivateKey.readBytes(destKeyStream); + } + + /** + * Connect to the router and establish a session. This call blocks until + * a session is granted. + * + * @throws I2PSessionException if there is a configuration error or the router is + * not reachable + */ + public void connect() throws I2PSessionException { + _closed = false; + long startConnect = Clock.getInstance().now(); + try { + if (_log.shouldLog(Log.DEBUG)) _log.debug("connect begin to " + _hostname + ":" + _portNum); + _socket = new Socket(_hostname, _portNum); + _out = _socket.getOutputStream(); + synchronized (_out) { + _out.write(I2PClient.PROTOCOL_BYTE); + } + InputStream in = _socket.getInputStream(); + _reader = new I2CPMessageReader(in, this); + if (_log.shouldLog(Log.DEBUG)) _log.debug("before startReading"); + _reader.startReading(); + + if (_log.shouldLog(Log.DEBUG)) _log.debug("Before getDate"); + sendMessage(new GetDateMessage()); + if (_log.shouldLog(Log.DEBUG)) _log.debug("After getDate / begin waiting for a response"); + while (!_dateReceived) { + try { + synchronized (_dateReceivedLock) { _dateReceivedLock.wait(1000); } + } catch (InterruptedException ie) {} + } + if (_log.shouldLog(Log.DEBUG)) _log.debug("After received a SetDate response"); + + if (_log.shouldLog(Log.DEBUG)) _log.debug("Before producer.connect()"); + _producer.connect(this); + if (_log.shouldLog(Log.DEBUG)) _log.debug("After producer.connect()"); + + // wait until we have created a lease set + while (_leaseSet == null) { + synchronized (_leaseSetWait) { + try { _leaseSetWait.wait(1000); } catch (InterruptedException ie) {} + } + } + long connected = Clock.getInstance().now(); + if (_log.shouldLog(Log.INFO)) _log.info("Lease set created with inbound tunnels after " + (connected-startConnect) + "ms - ready to participate in the network!"); + } catch (UnknownHostException uhe) { + _closed = true; + throw new I2PSessionException("Invalid session configuration", uhe); + } catch (IOException ioe) { + _closed = true; + throw new I2PSessionException("Problem connecting to " + _hostname + " on port " + _portNum, ioe); + } + } + + /** + * Pull the unencrypted data from the message that we've already prefetched and + * notified the user that its available. + * + */ + public byte[] receiveMessage(int msgId) throws I2PSessionException { + MessagePayloadMessage msg = (MessagePayloadMessage)_availableMessages.remove(new Integer(msgId)); + if (msg == null) return null; + return msg.getPayload().getUnencryptedData(); + } + + /** + * Report abuse with regards to the given messageId + */ + public void reportAbuse(int msgId, int severity) throws I2PSessionException { + if (isClosed()) throw new I2PSessionException("Already closed"); + _producer.reportAbuse(this, msgId, severity); + } + + /** + * Send the data to the destination. + * TODO: this currently always returns true, regardless of whether the message was + * delivered successfully. make this wait for at least ACCEPTED + * + */ + public abstract boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException; + public abstract boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException; + public abstract void receiveStatus(int msgId, long nonce, int status); + + protected boolean isGuaranteed() { + return I2PClient.PROP_RELIABILITY_GUARANTEED.equals(_options.getProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_GUARANTEED)); + } + + protected static final Set createNewTags(int num) { + Set tags = new HashSet(); + for (int i = 0; i < num; i++) + tags.add(new SessionTag(true)); + return tags; + } + + /** + * Recieve a payload message and let the app know its available + */ + public void addNewMessage(MessagePayloadMessage msg) { + _availableMessages.put(new Integer(msg.getMessageId().getMessageId()), msg); + final int id = msg.getMessageId().getMessageId(); + byte data[] = msg.getPayload().getUnencryptedData(); + if ( (data == null) || (data.length <= 0) ) { + if (_log.shouldLog(Log.ERROR)) _log.error("addNewMessage of a message with no unencrypted data", new Exception("Empty message")); + } else { + final long size = data.length; + Thread notifier = new I2PThread(new Runnable() { + public void run() { + if (_sessionListener != null) + _sessionListener.messageAvailable(I2PSessionImpl.this, id, size); + } + }); + notifier.setName("Notifier [" + _sessionId + "/" + id + "]"); + notifier.setDaemon(true); + notifier.start(); + } + } + + /** + * Recieve notification of some I2CP message and handle it if possible + * + */ + public void messageReceived(I2CPMessageReader reader, I2CPMessage message) { + I2CPMessageHandler handler = I2PClientMessageHandlerMap.getHandler(message.getType()); + if (handler == null) { + if (_log.shouldLog(Log.WARN)) _log.warn("Unknown message or unhandleable message received: type = " + message.getType()); + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Message received of type " + message.getType() + " to be handled by " + handler); + handler.handleMessage(message, this); + } + } + + /** + * Recieve notifiation of an error reading the I2CP stream + * + */ + public void readError(I2CPMessageReader reader, Exception error) { + propogateError("There was an error reading data", error); + disconnect(); + } + + /** + * Retrieve the destination of the session + */ + public Destination getMyDestination() { return _myDestination; } + /** + * Retrieve the decryption PrivateKey + */ + public PrivateKey getDecryptionKey() { return _privateKey; } + /** + * Retrieve the signing SigningPrivateKey + */ + public SigningPrivateKey getPrivateKey() { return _signingPrivateKey; } + /** + * Retrieve the helper that generates I2CP messages + */ + I2CPMessageProducer getProducer() { return _producer; } + /** + * Retrieve the configuration options + */ + Properties getOptions() { return _options; } + /** + * Retrieve the session's ID + */ + SessionId getSessionId() { return _sessionId; } + /** + * Configure the session's ID + */ + void setSessionId(SessionId id) { + _sessionId = id; + } + + /** configure the listener */ + public void setSessionListener(I2PSessionListener lsnr) { _sessionListener = lsnr; } + + /** has the session been closed (or not yet connected)? */ + public boolean isClosed() { return _closed; } + + /** + * Deliver an I2CP message to the router + * + * @throws I2PSessionException if the message is malformed or there is an error writing it out + */ + void sendMessage(I2CPMessage message) throws I2PSessionException { + if (isClosed()) throw new I2PSessionException("Already closed"); + + try { + synchronized(_out) { + message.writeMessage(_out); + _out.flush(); + } + if (_log.shouldLog(Log.DEBUG)) _log.debug("Message written out and flushed"); + } catch (I2CPMessageException ime) { + throw new I2PSessionException("Error writing out the message", ime); + } catch (IOException ioe) { + throw new I2PSessionException("Error writing out the message", ioe); + } + } + + /** + * Pass off the error to the listener + */ + void propogateError(String msg, Throwable error) { + if (_log.shouldLog(Log.ERROR)) _log.error("Error occurred: " + msg, error); + if (_sessionListener != null) + _sessionListener.errorOccurred(this, msg, error); + } + + /** + * Tear down the session, and do NOT reconnect + */ + public void destroySession() { destroySession(true); } + public void destroySession(boolean sendDisconnect) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Destroy the session", new Exception("DestroySession()")); + _closed = true; + if (sendDisconnect) { + try { + _producer.disconnect(this); + } catch (I2PSessionException ipe) { + propogateError("Error destroying the session", ipe); + } + } + closeSocket(); + if (_sessionListener != null) + _sessionListener.disconnected(this); + } + + /** + * Close the socket carefully + * + */ + private void closeSocket() { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Closing the socket", new Exception("closeSocket")); + _closed = true; + if (_reader != null) + _reader.stopReading(); + _reader = null; + + if (_socket != null) { + try { + _socket.close(); + } catch (IOException ioe) { + propogateError("Caught an IO error closing the socket. ignored", ioe); + } finally { + _socket = null; // so when propogateError calls closeSocket, it doesnt loop + } + } + } + + /** + * Recieve notification that the I2CP connection was disconnected + */ + public void disconnected(I2CPMessageReader reader) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Disconnected", new Exception("Disconnected")); + disconnect(); + } + + protected void disconnect() { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Disconnect() called", new Exception("Disconnect")); + if (shouldReconnect()) { + if (reconnect()) { + if (_log.shouldLog(Log.INFO)) _log.info("I2CP reconnection successful"); + return; + } else { + _log.error("I2CP reconnection failed"); + } + } + + _log.error("Disconned from the router, and not trying to reconnect further. I hope you're not hoping anything else will happen"); + if (_sessionListener != null) + _sessionListener.disconnected(this); + + closeSocket(); + } + + private final static int MAX_RECONNECT_ATTEMPTS = 1; + private final static int MAX_TOTAL_RECONNECT_ATTEMPTS = 3; + + protected boolean shouldReconnect() { return true; } + protected boolean reconnect() { + closeSocket(); + if (_totalReconnectAttempts < MAX_TOTAL_RECONNECT_ATTEMPTS) { + _totalReconnectAttempts++; + } else { + if (_log.shouldLog(Log.CRIT)) _log.log(Log.CRIT, "Max number of reconnects exceeded [" + _totalReconnectAttempts + "], we give up!"); + return false; + } + if (_log.shouldLog(Log.INFO)) _log.info("Reconnecting..."); + for (int i = 0; i < MAX_RECONNECT_ATTEMPTS; i++) { + try { + connect(); + return true; + } catch (I2PSessionException ise) { + if (_log.shouldLog(Log.ERROR)) _log.error("Error reconnecting on attempt " + i, ise); + } + } + return false; + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java new file mode 100644 index 000000000..dda10617b --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java @@ -0,0 +1,287 @@ +package net.i2p.client; +/* + * 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.io.InputStream; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Properties; +import java.util.Set; + +import net.i2p.crypto.KeyGenerator; +import net.i2p.crypto.SessionKeyManager; +import net.i2p.data.Destination; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.data.DataHelper; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.MessageStatusMessage; +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; + +/** + * Thread safe implementation of an I2P session running over TCP. + * + * @author jrandom + */ +class I2PSessionImpl2 extends I2PSessionImpl { + private final static Log _log = new Log(I2PSessionImpl2.class); + + /** set of MessageState objects, representing all of the messages in the process of being sent */ + private Set _sendingStates; + /** max # seconds to wait for confirmation of the message send */ + private final static long SEND_TIMEOUT = 60*1000; // 60 seconds to send + /** should we gzip each payload prior to sending it? */ + private final static boolean SHOULD_COMPRESS = true; + + /** + * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey + * from the destKeyStream, and using the specified options to connect to the router + * + * @throws I2PSessionException if there is a problem loading the private keys or + */ + public I2PSessionImpl2(InputStream destKeyStream, Properties options) throws I2PSessionException { + super(destKeyStream, options); + _sendingStates = new HashSet(32); + } + + protected long getTimeout() { return SEND_TIMEOUT; } + + public void destroySession(boolean sendDisconnect) { + clearStates(); + super.destroySession(sendDisconnect); + } + + public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException { + return sendMessage(dest, payload, new SessionKey(), new HashSet(64)); + } + + public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException { + if (isClosed()) throw new I2PSessionException("Already closed"); + if (SHOULD_COMPRESS) + payload = DataHelper.compress(payload); + if (isGuaranteed()) { + return sendGuaranteed(dest, payload, keyUsed, tagsSent); + } else { + return sendBestEffort(dest, payload, keyUsed, tagsSent); + } + } + + /** + * pull the unencrypted AND DECOMPRESSED data + */ + public byte[] receiveMessage(int msgId) throws I2PSessionException { + byte compressed[] = super.receiveMessage(msgId); + if (SHOULD_COMPRESS) + return DataHelper.decompress(compressed); + else + return compressed; + } + + + private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent) throws I2PSessionException { + SessionKey key = SessionKeyManager.getInstance().getCurrentKey(dest.getPublicKey()); + if (key == null) + key = SessionKeyManager.getInstance().createSession(dest.getPublicKey()); + SessionTag tag = SessionKeyManager.getInstance().consumeNextAvailableTag(dest.getPublicKey(), key); + Set sentTags = null; + if (SessionKeyManager.getInstance().getAvailableTags(dest.getPublicKey(), key) < 10) { + sentTags = createNewTags(50); + } else if (SessionKeyManager.getInstance().getAvailableTimeLeft(dest.getPublicKey(), key) < 30*1000) { + // if we have > 10 tags, but they expire in under 30 seconds, we want more + sentTags = createNewTags(50); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Tags are almost expired, adding 50 new ones"); + } + SessionKey newKey = null; + if (false) // rekey + newKey = KeyGenerator.getInstance().generateSessionKey(); + + long nonce = (long)RandomSource.getInstance().nextInt(Integer.MAX_VALUE); + MessageState state = new MessageState(nonce); + state.setKey(key); + state.setTags(sentTags); + state.setNewKey(newKey); + state.setTo(dest); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Setting key = " + key); + + if (keyUsed != null) { + if (newKey != null) + keyUsed.setData(newKey.getData()); + else + keyUsed.setData(key.getData()); + } + if (tagsSent != null) { + if (sentTags != null) { + tagsSent.addAll(sentTags); + } + } + + synchronized (_sendingStates) { + _sendingStates.add(state); + } + if (_log.shouldLog(Log.DEBUG)) _log.debug("Adding sending state " + state.getMessageId() + " / " + state.getNonce()); + _producer.sendMessage(this, dest, nonce, payload, tag, key, sentTags, newKey); + state.waitFor(MessageStatusMessage.STATUS_SEND_ACCEPTED, Clock.getInstance().now() + getTimeout()); + synchronized (_sendingStates) { + _sendingStates.remove(state); + } + boolean found = state.received(MessageStatusMessage.STATUS_SEND_ACCEPTED); + if (_log.shouldLog(Log.DEBUG)) _log.debug("After waitFor sending state " + state.getMessageId().getMessageId() + " / " + state.getNonce() + " found = " + found); + if (found) { + if (_log.shouldLog(Log.INFO)) _log.info("Message sent after " + state.getElapsed() + "ms with " + payload.length + " bytes"); + } else { + if (_log.shouldLog(Log.INFO)) _log.info("Message send failed after " + state.getElapsed() + "ms with " + payload.length + " bytes"); + if (_log.shouldLog(Log.ERROR)) _log.error("Never received *accepted* from the router! dropping and reconnecting"); + disconnect(); + return false; + } + return found; + } + + private boolean sendGuaranteed(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent) throws I2PSessionException { + SessionKey key = SessionKeyManager.getInstance().getCurrentKey(dest.getPublicKey()); + if (key == null) + key = SessionKeyManager.getInstance().createSession(dest.getPublicKey()); + SessionTag tag = SessionKeyManager.getInstance().consumeNextAvailableTag(dest.getPublicKey(), key); + Set sentTags = null; + if (SessionKeyManager.getInstance().getAvailableTags(dest.getPublicKey(), key) < 10) { + sentTags = createNewTags(50); + } else if (SessionKeyManager.getInstance().getAvailableTimeLeft(dest.getPublicKey(), key) < 30*1000) { + // if we have > 10 tags, but they expire in under 30 seconds, we want more + sentTags = createNewTags(50); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Tags are almost expired, adding 50 new ones"); + } + SessionKey newKey = null; + if (false) // rekey + newKey = KeyGenerator.getInstance().generateSessionKey(); + + long nonce = (long)RandomSource.getInstance().nextInt(Integer.MAX_VALUE); + MessageState state = new MessageState(nonce); + state.setKey(key); + state.setTags(sentTags); + state.setNewKey(newKey); + state.setTo(dest); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Setting key = " + key); + + if (keyUsed != null) { + if (newKey != null) + keyUsed.setData(newKey.getData()); + else + keyUsed.setData(key.getData()); + } + if (tagsSent != null) { + if (sentTags != null) { + tagsSent.addAll(sentTags); + } + } + + + synchronized (_sendingStates) { + _sendingStates.add(state); + } + if (_log.shouldLog(Log.DEBUG)) _log.debug("Adding sending state " + state.getMessageId() + " / " + state.getNonce()); + _producer.sendMessage(this, dest, nonce, payload, tag, key, sentTags, newKey); + state.waitFor(MessageStatusMessage.STATUS_SEND_GUARANTEED_SUCCESS, Clock.getInstance().now() + SEND_TIMEOUT); + synchronized (_sendingStates) { + _sendingStates.remove(state); + } + boolean found = state.received(MessageStatusMessage.STATUS_SEND_GUARANTEED_SUCCESS); + boolean accepted = state.received(MessageStatusMessage.STATUS_SEND_ACCEPTED); + + if ( (!accepted) || (state.getMessageId() == null) ) { + if (_log.shouldLog(Log.ERROR)) _log.error("State with nonce " + state.getNonce() + " was not accepted? (no messageId!!)"); + nackTags(state); + if (_log.shouldLog(Log.CRIT)) _log.log(Log.CRIT,"Disconnecting/reconnecting because we never were accepted!"); + disconnect(); + return false; + } + + if (_log.shouldLog(Log.DEBUG)) _log.debug("After waitFor sending state " + state.getMessageId().getMessageId() + " / " + state.getNonce() + " found = " + found); + if (found) { + if (_log.shouldLog(Log.INFO)) _log.info("Message sent after " + state.getElapsed() + "ms with " + payload.length + " bytes"); + ackTags(state); + } else { + if (_log.shouldLog(Log.INFO)) _log.info("Message send failed after " + state.getElapsed() + "ms with " + payload.length + " bytes"); + nackTags(state); + } + return found; + } + + + private void ackTags(MessageState state) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("ack tags for msgId " + state.getMessageId() + " / " + state.getNonce() + " key = " + state.getKey() + ", tags = " + state.getTags()); + if ( (state.getTags() != null) && (state.getTags().size() > 0) ) { + if (state.getNewKey() == null) + SessionKeyManager.getInstance().tagsDelivered(state.getTo().getPublicKey(), state.getKey(), state.getTags()); + else + SessionKeyManager.getInstance().tagsDelivered(state.getTo().getPublicKey(), state.getNewKey(), state.getTags()); + } + } + + private void nackTags(MessageState state) { + if (_log.shouldLog(Log.INFO)) _log.info("nack tags for msgId " + state.getMessageId() + " / " + state.getNonce() + " key = " + state.getKey()); + SessionKeyManager.getInstance().failTags(state.getTo().getPublicKey()); + } + + public void receiveStatus(int msgId, long nonce, int status) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Received status " + status + " for msgId " + msgId + " / " + nonce); + MessageState state = null; + synchronized (_sendingStates) { + for (Iterator iter = _sendingStates.iterator(); iter.hasNext(); ) { + state = (MessageState)iter.next(); + if (_log.shouldLog(Log.DEBUG)) _log.debug("State " + state.getMessageId() + " / " + state.getNonce()); + if (state.getNonce() == nonce) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Found a matching state"); + break; + } else if ( (state.getMessageId() != null) && (state.getMessageId().getMessageId() == msgId) ) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Found a matching state by msgId"); + break; + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("State does not match"); + state = null; + } + } + } + + if (state != null) { + if (state.getMessageId() == null) { + MessageId id = new MessageId(); + id.setMessageId(msgId); + state.setMessageId(id); + } + state.receive(status); + } else { + if (_log.shouldLog(Log.INFO)) _log.info("No matching state for messageId " + msgId + " / " + nonce + " w/ status = " + status); + } + } + + /** + * Called whenever we want to reconnect (used only in the superclass). We need + * to override this to clear out the message state + * + */ + protected boolean reconnect() { + // even if we succeed in reconnecting, we want to clear the old states, + // since this will be a new sessionId + clearStates(); + return super.reconnect(); + } + + private void clearStates() { + synchronized (_sendingStates) { + for (Iterator iter = _sendingStates.iterator(); iter.hasNext(); ) { + MessageState state = (MessageState)iter.next(); + state.cancel(); + } + if (_log.shouldLog(Log.INFO)) _log.info("Disconnecting " + _sendingStates.size() + " states"); + _sendingStates.clear(); + } + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/I2PSessionListener.java b/core/java/src/net/i2p/client/I2PSessionListener.java new file mode 100644 index 000000000..9403fc5f0 --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSessionListener.java @@ -0,0 +1,44 @@ +package net.i2p.client; +/* + * 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. + * + */ + +/** + * Define a means for the router to asynchronously notify the client that a + * new message is available or the router is under attack. + * + * @author jrandom + */ +public interface I2PSessionListener { + /** Instruct the client that the given session has received a message with + * size # of bytes. + * @param session session to notify + * @param msgId message number available + * @param size size of the message + */ + void messageAvailable(I2PSession session, int msgId, long size); + + /** Instruct the client that the session specified seems to be under attack + * and that the client may wish to move its destination to another router. + * @param session session to report abuse to + * @param severity how bad the abuse is + */ + void reportAbuse(I2PSession session, int severity); + + /** + * Notify the client that the session has been terminated + * + */ + void disconnected(I2PSession session); + + /** + * Notify the client that some error occurred + * + */ + void errorOccurred(I2PSession session, String message, Throwable error); +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/MessagePayloadMessageHandler.java b/core/java/src/net/i2p/client/MessagePayloadMessageHandler.java new file mode 100644 index 000000000..4cfe85b6b --- /dev/null +++ b/core/java/src/net/i2p/client/MessagePayloadMessageHandler.java @@ -0,0 +1,62 @@ +package net.i2p.client; +/* + * 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 net.i2p.crypto.ElGamalAESEngine; +import net.i2p.data.DataFormatException; +import net.i2p.data.Payload; +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.MessagePayloadMessage; +import net.i2p.data.i2cp.ReceiveMessageEndMessage; + +/** + * Handle I2CP MessagePayloadMessages from the router delivering the contents + * of a message by accepting it, decrypting the payload, adding it to the set of + * recieved messages, and telling the router that it has been recieved correctly. + * + * @author jrandom + */ +class MessagePayloadMessageHandler extends HandlerImpl { + public MessagePayloadMessageHandler() { + super(MessagePayloadMessage.MESSAGE_TYPE); + } + public void handleMessage(I2CPMessage message, I2PSessionImpl session) { + _log.debug("Handle message " + message); + try { + MessagePayloadMessage msg = (MessagePayloadMessage)message; + MessageId id = msg.getMessageId(); + Payload payload = decryptPayload(msg, session); + session.addNewMessage(msg); + + ReceiveMessageEndMessage m = new ReceiveMessageEndMessage(); + m.setMessageId(id); + m.setSessionId(msg.getSessionId()); + session.sendMessage(m); + } catch (DataFormatException dfe) { + session.propogateError("Error handling a new payload message", dfe); + } catch (I2PSessionException ise) { + session.propogateError("Error handling a new payload message", ise); + } + } + + /** + * Decrypt the payload + */ + private Payload decryptPayload(MessagePayloadMessage msg, I2PSessionImpl session) throws DataFormatException { + Payload payload = msg.getPayload(); + byte[] data = ElGamalAESEngine.decrypt(payload.getEncryptedData(), session.getDecryptionKey()); + if (data == null) { + _log.error("Error decrypting the payload to public key " + session.getMyDestination().getPublicKey().toBase64() + "\nPayload: " + payload.calculateHash()); + throw new DataFormatException("Unable to decrypt the payload"); + } + payload.setUnencryptedData(data); + return payload; + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/MessageState.java b/core/java/src/net/i2p/client/MessageState.java new file mode 100644 index 000000000..8748fccec --- /dev/null +++ b/core/java/src/net/i2p/client/MessageState.java @@ -0,0 +1,208 @@ +package net.i2p.client; + +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.MessageStatusMessage; + +import net.i2p.data.SessionKey; +import net.i2p.data.Destination; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Contains the state of a payload message being sent to a peer + * + */ +class MessageState { + private final static Log _log = new Log(MessageState.class); + private long _nonce; + private MessageId _id; + private Set _receivedStatus; + private SessionKey _key; + private SessionKey _newKey; + private Set _tags; + private Destination _to; + private boolean _cancelled; + private long _created; + private Object _lock = new Object(); + public MessageState(long nonce) { + _nonce = nonce; + _id = null; + _receivedStatus = new HashSet(); + _cancelled = false; + _key = null; + _newKey = null; + _tags = null; + _to = null; + _created = Clock.getInstance().now(); + } + public void receive(int status) { + synchronized (_receivedStatus) { + _receivedStatus.add(new Integer(status)); + } + synchronized (_lock) { + _lock.notifyAll(); + } + } + + public void setMessageId(MessageId id) { _id = id; } + public MessageId getMessageId() { return _id; } + public long getNonce() { return _nonce; } + public void setKey(SessionKey key) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Setting key [" + _key + "] to [" + key + "]"); + _key = key; + } + public SessionKey getKey() { return _key; } + public void setNewKey(SessionKey key) { _newKey = key; } + public SessionKey getNewKey() { return _newKey; } + public void setTags(Set tags) { _tags = tags; } + public Set getTags() { return _tags; } + public void setTo(Destination dest) { _to = dest; } + public Destination getTo() { return _to; } + + public long getElapsed() { return Clock.getInstance().now() - _created; } + + public void waitFor(int status, long expiration) { + while (true) { + if (_cancelled) return; + long timeToWait = expiration - Clock.getInstance().now(); + if (timeToWait <= 0) { + if (_log.shouldLog(Log.WARN)) _log.warn("Expired waiting for the status [" + status + "]"); + return; + } + if (isSuccess(status) || isFailure(status)) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Received a confirm (one way or the other)"); + return; + } + if (timeToWait > 5000) { + timeToWait = 5000; + } + synchronized (_lock) { + try { + _lock.wait(timeToWait); + } catch (InterruptedException ie) {} + } + } + } + + private boolean isSuccess(int wantedStatus) { + List received = null; + synchronized (_receivedStatus) { + received = new ArrayList(_receivedStatus); + //_receivedStatus.clear(); + } + + boolean rv = false; + + if (_log.shouldLog(Log.DEBUG)) _log.debug("isSuccess(" + wantedStatus + "): " + received); + for (Iterator iter = received.iterator(); iter.hasNext(); ) { + Integer val = (Integer)iter.next(); + int recv = val.intValue(); + switch (recv) { + case MessageStatusMessage.STATUS_SEND_BEST_EFFORT_FAILURE: + if (_log.shouldLog(Log.WARN)) _log.warn("Received best effort failure after " + getElapsed() + " from " + this.toString()); + rv = false; + break; + case MessageStatusMessage.STATUS_SEND_GUARANTEED_FAILURE: + if (_log.shouldLog(Log.WARN)) _log.warn("Received guaranteed failure after " + getElapsed() + " from " + this.toString()); + rv = false; + break; + case MessageStatusMessage.STATUS_SEND_ACCEPTED: + if (wantedStatus == MessageStatusMessage.STATUS_SEND_ACCEPTED) { + return true; // if we're only looking for accepted, take it directly (don't let any GUARANTEED_* override it) + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Got accepted, but we're waiting for more from " + this.toString()); + continue; + // ignore accepted, as we want something better + } + case MessageStatusMessage.STATUS_SEND_BEST_EFFORT_SUCCESS: + if (_log.shouldLog(Log.DEBUG)) _log.debug("Received best effort success after " + getElapsed() + " from " + this.toString()); + if (wantedStatus == recv) { + rv = true; + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Not guaranteed success, but best effort after " + getElapsed() + " will do... from " + this.toString()); + rv = true; + } + break; + case MessageStatusMessage.STATUS_SEND_GUARANTEED_SUCCESS: + if (_log.shouldLog(Log.DEBUG)) _log.debug("Received guaranteed success after " + getElapsed() + " from " + this.toString()); + // even if we're waiting for best effort success, guaranteed is good enough + rv = true; + break; + case -1: + continue; + default: + if (_log.shouldLog(Log.DEBUG)) _log.debug("Received something else [" + recv + "]..."); + } + } + return rv; + } + private boolean isFailure(int wantedStatus) { + List received = null; + synchronized (_receivedStatus) { + received = new ArrayList(_receivedStatus); + //_receivedStatus.clear(); + } + boolean rv = false; + + if (_log.shouldLog(Log.DEBUG)) _log.debug("isFailure(" + wantedStatus + "): " + received); + for (Iterator iter = received.iterator(); iter.hasNext(); ) { + Integer val = (Integer)iter.next(); + int recv = val.intValue(); + switch (recv) { + case MessageStatusMessage.STATUS_SEND_BEST_EFFORT_FAILURE: + if (_log.shouldLog(Log.DEBUG)) _log.warn("Received best effort failure after " + getElapsed() + " from " + this.toString()); + rv = true; + break; + case MessageStatusMessage.STATUS_SEND_GUARANTEED_FAILURE: + if (_log.shouldLog(Log.DEBUG)) _log.warn("Received guaranteed failure after " + getElapsed() + " from " + this.toString()); + rv = true; + break; + case MessageStatusMessage.STATUS_SEND_ACCEPTED: + if (wantedStatus == MessageStatusMessage.STATUS_SEND_ACCEPTED) { + rv = false; + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Got accepted, but we're waiting for more from " + this.toString()); + continue; + // ignore accepted, as we want something better + } + break; + case MessageStatusMessage.STATUS_SEND_BEST_EFFORT_SUCCESS: + if (_log.shouldLog(Log.DEBUG)) _log.debug("Received best effort success after " + getElapsed() + " from " + this.toString()); + if (wantedStatus == recv) { + rv = false; + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Not guaranteed success, but best effort after " + getElapsed() + " will do... from " + this.toString()); + rv = false; + } + break; + case MessageStatusMessage.STATUS_SEND_GUARANTEED_SUCCESS: + if (_log.shouldLog(Log.DEBUG)) _log.debug("Received guaranteed success after " + getElapsed() + " from " + this.toString()); + // even if we're waiting for best effort success, guaranteed is good enough + rv = false; + break; + case -1: + continue; + default: + if (_log.shouldLog(Log.DEBUG)) _log.debug("Received something else [" + recv + "]..."); + } + } + return rv; + } + + /** true if the given status (or an equivilant) was received */ + public boolean received(int status) { + return isSuccess(status); + } + public void cancel() { + _cancelled = true; + synchronized (_lock) { + _lock.notifyAll(); + } + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/MessageStatusMessageHandler.java b/core/java/src/net/i2p/client/MessageStatusMessageHandler.java new file mode 100644 index 000000000..df9929f84 --- /dev/null +++ b/core/java/src/net/i2p/client/MessageStatusMessageHandler.java @@ -0,0 +1,62 @@ +package net.i2p.client; +/* + * 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 net.i2p.data.i2cp.*; + +/** + * Handle I2CP MessageStatusMessages from the router. This currently only takes + * into account status of available, automatically prefetching them as soon as + * possible + * + * @author jrandom + */ +class MessageStatusMessageHandler extends HandlerImpl { + public MessageStatusMessageHandler() { + super(MessageStatusMessage.MESSAGE_TYPE); + } + public void handleMessage(I2CPMessage message, I2PSessionImpl session) { + boolean skipStatus = true; + if (I2PClient.PROP_RELIABILITY_GUARANTEED.equals(session.getOptions().getProperty( + I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT))) + skipStatus = false; + _log.debug("Handle message " + message); + MessageStatusMessage msg = (MessageStatusMessage)message; + switch (msg.getStatus()) { + case MessageStatusMessage.STATUS_AVAILABLE: + ReceiveMessageBeginMessage m = new ReceiveMessageBeginMessage(); + m.setMessageId(msg.getMessageId()); + m.setSessionId(msg.getSessionId()); + try { + session.sendMessage(m); + } catch (I2PSessionException ise) { + _log.error("Error asking for the message", ise); + } + return; + case MessageStatusMessage.STATUS_SEND_ACCEPTED: + session.receiveStatus(msg.getMessageId().getMessageId(), msg.getNonce(), msg.getStatus()); + // noop + return; + case MessageStatusMessage.STATUS_SEND_BEST_EFFORT_SUCCESS: + case MessageStatusMessage.STATUS_SEND_GUARANTEED_SUCCESS: + _log.info("Message delivery succeeded for message " + msg.getMessageId()); + //if (!skipStatus) + session.receiveStatus(msg.getMessageId().getMessageId(), msg.getNonce(), msg.getStatus()); + return; + case MessageStatusMessage.STATUS_SEND_BEST_EFFORT_FAILURE: + case MessageStatusMessage.STATUS_SEND_GUARANTEED_FAILURE: + _log.info("Message delivery FAILED for message " + msg.getMessageId()); + //if (!skipStatus) + session.receiveStatus(msg.getMessageId().getMessageId(), msg.getNonce(), msg.getStatus()); + return; + default: + _log.error("Invalid message delivery status received: " + msg.getStatus()); + } + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java new file mode 100644 index 000000000..a10432f62 --- /dev/null +++ b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java @@ -0,0 +1,123 @@ +package net.i2p.client; +/* + * 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.util.HashMap; +import java.util.Map; + +import net.i2p.crypto.KeyGenerator; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Lease; +import net.i2p.data.LeaseSet; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.RequestLeaseSetMessage; +import net.i2p.util.Log; + +/** + * Handle I2CP RequestLeaseSetMessage from the router by granting all leases + * + * @author jrandom + */ +class RequestLeaseSetMessageHandler extends HandlerImpl { + private final static Log _log = new Log(RequestLeaseSetMessageHandler.class); + private Map _existingLeaseSets; + + public RequestLeaseSetMessageHandler() { + super(RequestLeaseSetMessage.MESSAGE_TYPE); + _existingLeaseSets = new HashMap(32); + } + public void handleMessage(I2CPMessage message, I2PSessionImpl session) { + _log.debug("Handle message " + message); + RequestLeaseSetMessage msg = (RequestLeaseSetMessage)message; + LeaseSet leaseSet = new LeaseSet(); + for (int i = 0; i < msg.getEndpoints(); i++) { + Lease lease = new Lease(); + lease.setRouterIdentity(msg.getRouter(i)); + lease.setTunnelId(msg.getTunnelId(i)); + lease.setEndDate(msg.getEndDate()); + //lease.setStartDate(msg.getStartDate()); + leaseSet.addLease(lease); + } + // also, if this session is connected to multiple routers, include other leases here + leaseSet.setDestination(session.getMyDestination()); + + // reuse the old keys for the client + LeaseInfo li = null; + synchronized (_existingLeaseSets) { + if (_existingLeaseSets.containsKey(session.getMyDestination())) + li = (LeaseInfo)_existingLeaseSets.get(session.getMyDestination()); + } + if (li == null) { + li = new LeaseInfo(session.getMyDestination()); + synchronized (_existingLeaseSets) { + _existingLeaseSets.put(session.getMyDestination(), li); + } + _log.debug("Creating new leaseInfo keys", new Exception("new leaseInfo keys")); + } else { + _log.debug("Caching the old leaseInfo keys", new Exception("cached! w00t")); + } + + leaseSet.setEncryptionKey(li.getPublicKey()); + leaseSet.setSigningKey(li.getSigningPublicKey()); + try { + leaseSet.sign(session.getPrivateKey()); + session.getProducer().createLeaseSet(session, leaseSet, li.getSigningPrivateKey(), li.getPrivateKey()); + session.setLeaseSet(leaseSet); + } catch (DataFormatException dfe) { + session.propogateError("Error signing the leaseSet", dfe); + } catch (I2PSessionException ise) { + session.propogateError("Error sending the signed leaseSet", ise); + } + } + + private static class LeaseInfo { + private PublicKey _pubKey; + private PrivateKey _privKey; + private SigningPublicKey _signingPubKey; + private SigningPrivateKey _signingPrivKey; + private Destination _dest; + + public LeaseInfo(Destination dest) { + _dest = dest; + Object encKeys[] = KeyGenerator.getInstance().generatePKIKeypair(); + Object signKeys[] = KeyGenerator.getInstance().generateSigningKeypair(); + _pubKey = (PublicKey)encKeys[0]; + _privKey = (PrivateKey)encKeys[1]; + _signingPubKey = (SigningPublicKey)signKeys[0]; + _signingPrivKey = (SigningPrivateKey)signKeys[1]; + } + public PublicKey getPublicKey() { return _pubKey; } + public PrivateKey getPrivateKey() { return _privKey; } + public SigningPublicKey getSigningPublicKey() { return _signingPubKey; } + public SigningPrivateKey getSigningPrivateKey() { return _signingPrivKey; } + + public int hashCode() { + return DataHelper.hashCode(_pubKey) + + 7*DataHelper.hashCode(_privKey) + + 7*7*DataHelper.hashCode(_signingPubKey) + + 7*7*7*DataHelper.hashCode(_signingPrivKey); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof LeaseInfo) ) + return false; + LeaseInfo li = (LeaseInfo)obj; + return DataHelper.eq(_pubKey, li.getPublicKey()) && + DataHelper.eq(_privKey, li.getPrivateKey()) && + DataHelper.eq(_signingPubKey, li.getSigningPublicKey()) && + DataHelper.eq(_signingPrivKey, li.getSigningPrivateKey()); + } + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/SessionStatusMessageHandler.java b/core/java/src/net/i2p/client/SessionStatusMessageHandler.java new file mode 100644 index 000000000..8f347beeb --- /dev/null +++ b/core/java/src/net/i2p/client/SessionStatusMessageHandler.java @@ -0,0 +1,46 @@ +package net.i2p.client; +/* + * 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 net.i2p.data.i2cp.*; + +/** + * Handle I2CP SessionStatusMessagese from the router, updating the session as + * necssary. + * + * @author jrandom + */ +class SessionStatusMessageHandler extends HandlerImpl { + public SessionStatusMessageHandler() { + super(SessionStatusMessage.MESSAGE_TYPE); + } + public void handleMessage(I2CPMessage message, I2PSessionImpl session) { + _log.debug("Handle message " + message); + SessionStatusMessage msg = (SessionStatusMessage)message; + session.setSessionId(msg.getSessionId()); + switch (msg.getStatus()) { + case SessionStatusMessage.STATUS_CREATED: + _log.info("Session created successfully"); + break; + case SessionStatusMessage.STATUS_DESTROYED: + _log.info("Session destroyed"); + session.destroySession(); + break; + case SessionStatusMessage.STATUS_INVALID: + session.destroySession(); + break; + case SessionStatusMessage.STATUS_UPDATED: + _log.info("Session status updated"); + break; + default: + _log.warn("Unknown session status sent: " + msg.getStatus()); + } + return; + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/SetDateMessageHandler.java b/core/java/src/net/i2p/client/SetDateMessageHandler.java new file mode 100644 index 000000000..11923c2a3 --- /dev/null +++ b/core/java/src/net/i2p/client/SetDateMessageHandler.java @@ -0,0 +1,29 @@ +package net.i2p.client; +/* + * 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 net.i2p.data.i2cp.*; +import net.i2p.util.Clock; + +/** + * Handle I2CP time messages from the router + * + * @author jrandom + */ +class SetDateMessageHandler extends HandlerImpl { + public SetDateMessageHandler() { + super(SetDateMessage.MESSAGE_TYPE); + } + public void handleMessage(I2CPMessage message, I2PSessionImpl session) { + _log.debug("Handle message " + message); + SetDateMessage msg = (SetDateMessage)message; + Clock.getInstance().setNow(msg.getDate().getTime()); + session.dateUpdated(); + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/TestClient.java b/core/java/src/net/i2p/client/TestClient.java new file mode 100644 index 000000000..ba32c050a --- /dev/null +++ b/core/java/src/net/i2p/client/TestClient.java @@ -0,0 +1,121 @@ +package net.i2p.client; +/* + * 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.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.Properties; + +import net.i2p.data.Destination; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Quick and dirty test harness for sending messages from one destination to another. + * This will print out some debugging information and containg the statement + * "Hello other side. I am dest1" if the router and the client libraries work. + * This class bootstraps itself each time - creating new keys and destinations + * + * @author jrandom + */ +public class TestClient implements I2PSessionListener { + private final static Log _log = new Log(TestClient.class); + private static Destination _dest1; + private static Destination _dest2; + private boolean _stillRunning; + + public void runTest(String keyfile, boolean isDest1) { + _stillRunning = true; + try { + I2PClient client = I2PClientFactory.createClient(); + File file = new File(keyfile); + Destination d = client.createDestination(new FileOutputStream(file)); + if (isDest1) + _dest1 = d; + else + _dest2 = d; + _log.debug("Destination written to " + file.getAbsolutePath()); + Properties options = new Properties(); + + if (System.getProperty(I2PClient.PROP_TCP_HOST) == null) + options.setProperty(I2PClient.PROP_TCP_HOST, "localhost"); + else + options.setProperty(I2PClient.PROP_TCP_HOST, System.getProperty(I2PClient.PROP_TCP_HOST)); + if (System.getProperty(I2PClient.PROP_TCP_PORT) == null) + options.setProperty(I2PClient.PROP_TCP_PORT, "7654"); + else + options.setProperty(I2PClient.PROP_TCP_PORT, System.getProperty(I2PClient.PROP_TCP_PORT)); + + I2PSession session = client.createSession(new FileInputStream(file), options); + session.setSessionListener(this); + _log.debug("Before connect..."); + session.connect(); + _log.debug("Connected"); + + // wait until the other one is connected + while ( (_dest1 == null) || (_dest2 == null) ) + try { Thread.sleep(500); } catch (InterruptedException ie) {} + + if (isDest1) { + Destination otherD = (isDest1 ? _dest2 : _dest1); + boolean accepted = session.sendMessage(otherD, ("Hello other side. I am" + (isDest1 ? "" : " NOT") + " dest1").getBytes()); + } else { + while (_stillRunning) { + try { + _log.debug("waiting for a message..."); + Thread.sleep(1000); + } catch (InterruptedException ie) {} + } + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + System.exit(0); + } + //session.destroySession(); + } catch (Exception e) { + _log.error("Error running the test for isDest1? " + isDest1, e); + } + } + + public static void main(String args[]) { + doTest(); + try { Thread.sleep(30*1000); } catch (InterruptedException ie) {} + } + static void doTest() { + Thread test1 = new I2PThread(new Runnable() { public void run() { (new TestClient()).runTest("test1.keyfile", true); } } ); + Thread test2 = new I2PThread(new Runnable() { public void run() { (new TestClient()).runTest("test2.keyfile", false); } } ); + test1.start(); + test2.start(); + _log.debug("Test threads started"); + } + + public void disconnected(I2PSession session) { + _log.debug("Disconnected"); + _stillRunning = false; + } + + public void errorOccurred(I2PSession session, String message, Throwable error) { + _log.debug("Error occurred: " + message, error); + } + + public void messageAvailable(I2PSession session, int msgId, long size) { + _log.debug("Message available for us! id = " + msgId + " of size " + size); + try { + byte msg[] = session.receiveMessage(msgId); + _log.debug("Content of message " + msgId+ ":\n"+new String(msg)); + _stillRunning = false; + } catch (I2PSessionException ise) { + _log.error("Error fetching available message", ise); + } + } + + public void reportAbuse(I2PSession session, int severity) { + _log.debug("Abuse reported of severity " + severity); + } + +} diff --git a/core/java/src/net/i2p/client/TestServer.java b/core/java/src/net/i2p/client/TestServer.java new file mode 100644 index 000000000..1c88e38a5 --- /dev/null +++ b/core/java/src/net/i2p/client/TestServer.java @@ -0,0 +1,84 @@ +package net.i2p.client; +/* + * 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.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Implement a local only router for testing purposes. This router is minimal + * in that it doesn't verify signatures, communicate with other routers, or handle + * failures very gracefully. It is simply a test harness to allow I2CP based + * applications to run. + * + * @author jrandom + */ +public class TestServer implements Runnable { + private final static Log _log = new Log(TestServer.class); + private ServerSocket _socket; + public static int LISTEN_PORT = 7654; + + protected void setPort(int port) { LISTEN_PORT = port; } + + /** + * Start up the socket listener, listens for connections, and + * fires those connections off via {@link #runConnection runConnection}. + * This only returns if the socket cannot be opened or there is a catastrophic + * failure. + * + */ + public void runServer() { + try { + _socket = new ServerSocket(LISTEN_PORT); + } catch (IOException ioe) { + _log.error("Error listening", ioe); + return; + } + while (true) { + try { + Socket socket = _socket.accept(); + runConnection(socket); + } catch (IOException ioe) { + _log.error("Server error accepting", ioe); + } + } + } + + /** + * Handle the connection by passing it off to a {@link ConnectionRunner ConnectionRunner} + * + */ + protected void runConnection(Socket socket) throws IOException { + ConnectionRunner runner = new ConnectionRunner(socket); + runner.doYourThing(); + } + + public void run() { runServer(); } + + /** + * Fire up the router + */ + public static void main(String args[]) { + if (args.length == 1) { + } else if (args.length == 2) { + try { + LISTEN_PORT = Integer.parseInt(args[1]); + } catch (NumberFormatException nfe) { + _log.error("Invalid port number specified (" + args[1] + "), using " + LISTEN_PORT, nfe); + } + } + TestServer server = new TestServer(); + Thread t = new I2PThread(server); + t.start(); + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/client/naming/DummyNamingService.java b/core/java/src/net/i2p/client/naming/DummyNamingService.java new file mode 100644 index 000000000..ea3bac13d --- /dev/null +++ b/core/java/src/net/i2p/client/naming/DummyNamingService.java @@ -0,0 +1,23 @@ +/* + * free (adj.): unencumbered; not under the control of others + * Written by mihi in 2004 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. + */ +package net.i2p.client.naming; + +import net.i2p.data.Destination; + +/** + * A Dummy naming service that can only handle base64 destinations. + */ +class DummyNamingService extends NamingService { + public Destination lookup(String hostname) { + return lookupBase64(hostname); + } + + public String reverseLookup(Destination dest) { + return null; + } +} diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java new file mode 100644 index 000000000..21e5f601c --- /dev/null +++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java @@ -0,0 +1,66 @@ +/* + * free (adj.): unencumbered; not under the control of others + * Written by mihi in 2004 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. + */ +package net.i2p.client.naming; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +import net.i2p.data.Destination; +import net.i2p.util.Log; + +/** + * A naming service based on the "hosts.txt" file. + */ +public class HostsTxtNamingService extends NamingService { + + /** + * If this system property is specified, the tunnel will read the + * given file for hostname=destKey values when resolving names + */ + public final static String PROP_HOSTS_FILE = "i2p.hostsfile"; + + /** default hosts.txt filename */ + public final static String DEFAULT_HOSTS_FILE = "hosts.txt"; + + private final static Log _log = new Log(HostsTxtNamingService.class); + + public Destination lookup(String hostname) { + // Try to look it up in hosts.txt + // Reload file each time to catch changes. + // (and it's easier :P + String hostsfile=System.getProperty(PROP_HOSTS_FILE, + DEFAULT_HOSTS_FILE ); + Properties hosts=new Properties(); + FileInputStream fis = null; + try { + File f = new File(hostsfile); + if(f.canRead()) { + fis = new FileInputStream(f); + hosts.load(fis); + } else { + _log.error("Hosts file " + hostsfile + " does not exist."); + } + } catch (Exception ioe) { + _log.error("Error loading hosts file " + hostsfile, ioe ); + } finally { + if (fis != null) try {fis.close();} catch (IOException ioe) {} + } + String res = hosts.getProperty(hostname); + // If we can't find name in hosts, assume it's a key. + if ((res == null) || (res.trim().length() == 0)) { + res = hostname; + } + return lookupBase64(res); + } + + public String reverseLookup(Destination dest) { + return null; + } +} diff --git a/core/java/src/net/i2p/client/naming/NamingService.java b/core/java/src/net/i2p/client/naming/NamingService.java new file mode 100644 index 000000000..2a9704569 --- /dev/null +++ b/core/java/src/net/i2p/client/naming/NamingService.java @@ -0,0 +1,78 @@ +/* + * free (adj.): unencumbered; not under the control of others + * Written by mihi in 2004 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. + */ +package net.i2p.client.naming; + +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.util.Log; + +/** + * Naming services create a subclass of this class. + */ +public abstract class NamingService { + + private final static Log _log = new Log(NamingService.class); + + private static final String PROP_IMPL = "i2p.naming.impl"; + private static final String DEFAULT_IMPL= + "net.i2p.client.naming.HostsTxtNamingService"; + + /** + * Look up a host name. + * @return the Destination for this host name, or + * null if name is unknown. + */ + public abstract Destination lookup(String hostname); + + /** + * Reverse look up a destination + * @return a host name for this Destination, or null + * if none is known. It is safe for subclasses to always return + * null if no reverse lookup is possible. + */ + public abstract String reverseLookup(Destination dest); + + /** + * Check if host name is valid Base64 encoded dest and return this + * dest in that case. Useful as a "fallback" in custom naming + * implementations. + */ + protected Destination lookupBase64(String hostname) { + try { + Destination result = new Destination(); + result.fromBase64(hostname); + return result; + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Error translating [" + hostname + "]", dfe); + return null; + } + } + + private static NamingService instance = null; + + /** + * Get a naming service instance. This method ensures that there + * will be only one naming service instance (singleton) as well as + * choose the implementation from the "i2p.naming.impl" system + * property. + */ + public static synchronized NamingService getInstance() { + if (instance == null) { + String impl = System.getProperty(PROP_IMPL, + DEFAULT_IMPL); + try { + instance = (NamingService) Class.forName(impl).newInstance(); + } catch (Exception ex) { + _log.error("Cannot loadNaming service implementation", ex); + instance = new DummyNamingService(); // fallback + } + } + return instance; + } +} diff --git a/core/java/src/net/i2p/crypto/AESEngine.java b/core/java/src/net/i2p/crypto/AESEngine.java new file mode 100644 index 000000000..627d24ef4 --- /dev/null +++ b/core/java/src/net/i2p/crypto/AESEngine.java @@ -0,0 +1,145 @@ +package net.i2p.crypto; +/* + * 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 net.i2p.data.DataFormatException; +import net.i2p.data.SessionKey; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; + +import java.io.IOException; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; + +/** + * Wrapper singleton for AES cypher operation. + * + * @author jrandom + * @license GPL + */ +public class AESEngine { + private final static Log _log = new Log(AESEngine.class); + private static AESEngine _engine; + static { + if ("off".equals(System.getProperty("i2p.encryption", "on"))) + _engine = new AESEngine(); + else + _engine = new CryptixAESEngine(); + } + public static AESEngine getInstance() { return _engine; } + + /** Encrypt the payload with the session key + * @param payload data to be encrypted + * @param sessionKey private esession key to encrypt to + * @param initializationVector IV for CBC + * @return encrypted data + */ + public byte[] encrypt(byte payload[], SessionKey sessionKey, byte initializationVector[]) { + if ( (initializationVector == null) || (payload == null) || (sessionKey == null) || (initializationVector.length != 16) ) + return null; + + byte cyphertext[] = new byte[payload.length+(16-(payload.length%16))]; + _log.warn("Warning: AES is disabled"); + System.arraycopy(payload, 0, cyphertext, 0, payload.length); + return cyphertext; + } + + public byte[] safeEncrypt(byte payload[], SessionKey sessionKey, byte iv[], int paddedSize) { + if ( (iv == null) || (payload == null) || (sessionKey == null) || (iv.length != 16) ) + return null; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(paddedSize+64); + Hash h = SHA256Generator.getInstance().calculateHash(sessionKey.getData()); + try { + h.writeBytes(baos); + DataHelper.writeLong(baos, 4, payload.length); + baos.write(payload); + byte tv[] = baos.toByteArray(); + baos.write(ElGamalAESEngine.getPadding(tv.length, paddedSize)); + } catch (IOException ioe) { + _log.error("Error writing data", ioe); + return null; + } catch (DataFormatException dfe) { + _log.error("Error writing data", dfe); + return null; + } + return encrypt(baos.toByteArray(), sessionKey, iv); + } + + public byte[] safeDecrypt(byte payload[], SessionKey sessionKey, byte iv[]) { + if ( (iv == null) || (payload == null) || (sessionKey == null) || (iv.length != 16) ) + return null; + + byte decr[] = decrypt(payload, sessionKey, iv); + if (decr == null) { + _log.error("Error decrypting the data - payload " + payload.length + " decrypted to null"); + return null; + } + ByteArrayInputStream bais = new ByteArrayInputStream(decr); + Hash h = SHA256Generator.getInstance().calculateHash(sessionKey.getData()); + try { + Hash rh = new Hash(); + rh.readBytes(bais); + if (!h.equals(rh)) { + _log.error("Hash does not match [key=" + sessionKey + " / iv =" + DataHelper.toString(iv, iv.length) + "]", new Exception("Hash error")); + return null; + } + long len = DataHelper.readLong(bais, 4); + byte data[] = new byte[(int)len]; + int read = bais.read(data); + if (read != len) { + _log.error("Not enough to read"); + return null; + } + return data; + } catch (IOException ioe) { + _log.error("Error writing data", ioe); + return null; + } catch (DataFormatException dfe) { + _log.error("Error writing data", dfe); + return null; + } + } + + + /** decrypt the data with the session key provided + * @param cyphertext encrypted data + * @param sessionKey private session key + * @param initializationVector IV for CBC + * @return unencrypted data + */ + public byte[] decrypt(byte cyphertext[], SessionKey sessionKey, byte initializationVector[]) { + if ( (initializationVector == null) || (cyphertext == null) || (sessionKey == null) || (initializationVector.length != 16) ) + return null; + + byte payload[] = new byte[cyphertext.length]; + _log.warn("Warning: AES is disabled"); + return cyphertext; + } + + public static void main(String args[]) { + SessionKey key = KeyGenerator.getInstance().generateSessionKey(); + byte iv[] = new byte[16]; + RandomSource.getInstance().nextBytes(iv); + + byte sbuf[] = new byte[16]; + RandomSource.getInstance().nextBytes(sbuf); + byte se[] = AESEngine.getInstance().encrypt(sbuf, key, iv); + byte sd[] = AESEngine.getInstance().decrypt(se, key, iv); + _log.debug("Short test: " + DataHelper.eq(sd, sbuf)); + + byte lbuf[] = new byte[1024]; + RandomSource.getInstance().nextBytes(sbuf); + byte le[] = AESEngine.getInstance().safeEncrypt(lbuf, key, iv, 2048); + byte ld[] = AESEngine.getInstance().safeDecrypt(le, key, iv); + _log.debug("Long test: " + DataHelper.eq(ld, lbuf)); + } +} diff --git a/core/java/src/net/i2p/crypto/AESInputStream.java b/core/java/src/net/i2p/crypto/AESInputStream.java new file mode 100644 index 000000000..dd95a132b --- /dev/null +++ b/core/java/src/net/i2p/crypto/AESInputStream.java @@ -0,0 +1,411 @@ +package net.i2p.crypto; +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; + +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import net.i2p.util.RandomSource; + +/** + * This reads an underlying stream as written by AESOutputStream - AES256 encrypted + * in CBC mode with PKCS#5 padding, with the padding on each and every block of + * 16 bytes. This minimizes the overhead when communication is intermittent, + * rather than when streams of large sets of data are sent (in which case, the + * padding would be on a larger size - say, 1k, though in the worst case that + * would have 1023 bytes of padding, while in the worst case here, we only have + * 15 bytes of padding). So we have an expansion factor of 6.25%. c'est la vie + * + */ +public class AESInputStream extends FilterInputStream { + private final static Log _log = new Log(AESInputStream.class); + private final static CryptixAESEngine _engine = new CryptixAESEngine(); + private SessionKey _key; + private byte[] _lastBlock; + private boolean _eofFound; + private long _cumulativeRead; // how many read from the source stream + private long _cumulativePrepared; // how many bytes decrypted and added to _readyBuf + private long _cumulativePaddingStripped; // how many bytes have been stripped + + private ByteArrayOutputStream _encryptedBuf; // read from the stream but not yet decrypted + private List _readyBuf; // list of Bytes ready to be consumed, where index 0 is the first + + private final static int BLOCK_SIZE = CryptixRijndael_Algorithm._BLOCK_SIZE; + private final static int READ_SIZE = BLOCK_SIZE; + private final static int DECRYPT_SIZE = BLOCK_SIZE-1; + + public AESInputStream(InputStream source, SessionKey key, byte iv[]) { + super(source); + _key = key; + _lastBlock = new byte[BLOCK_SIZE]; + System.arraycopy(iv, 0, _lastBlock, 0, BLOCK_SIZE); + _encryptedBuf = new ByteArrayOutputStream(BLOCK_SIZE); + _readyBuf = new LinkedList(); + _cumulativePaddingStripped = 0; + _eofFound = false; + } + + public int read() throws IOException { + while ( (!_eofFound) && (_readyBuf.size() <= 0) ) { + refill(READ_SIZE); + } + Integer nval = getNext(); + if (nval != null) { + return nval.intValue(); + } else { + //_log.debug("No byte available. eof? " + _eofFound); + if (_eofFound) + return -1; + else { + throw new IOException("Not EOF, but none available? " + _readyBuf.size() + "/" + _encryptedBuf.size() + "/" + _cumulativeRead + "... impossible"); + } + } + } + + public int read(byte dest[]) throws IOException { + for (int i = 0; i < dest.length; i++) { + int val = read(); + if (val == -1) { + // no more to read... can they expect more? + if (_eofFound && (i == 0)) + return -1; + else + return i; + } else { + dest[i] = (byte)val; + } + } + _log.debug("Read the full buffer of size " + dest.length); + return dest.length; + } + + public int read(byte dest[], int off, int len) throws IOException { + byte buf[] = new byte[len]; + int read = read(buf); + if (read == -1) + return -1; + System.arraycopy(buf, 0, dest, off, read); + return read; + } + public long skip(long numBytes) throws IOException { + for (long l = 0; l < numBytes; l++) { + int val = read(); + if (val == -1) + return l; + } + return numBytes; + } + + public int available() throws IOException { return _readyBuf.size(); } + public void close() throws IOException { + //_log.debug("We have " + _encryptedBuf.size() + " available to decrypt... doing so"); + //decrypt(); + //byte buf[] = new byte[_readyBuf.size()]; + //for (int i = 0; i < buf.length; i++) + // buf[i] = ((Integer)_readyBuf.get(i)).byteValue(); + //_log.debug("After decrypt: readyBuf.size: " + _readyBuf.size() + "\n val:\t" + Base64.encode(buf)); + int ready = _readyBuf.size(); + int encrypted = _readyBuf.size(); + _readyBuf.clear(); + _encryptedBuf.reset(); + in.close(); + _log.debug("Cumulative bytes read from source/decrypted/stripped: " + _cumulativeRead + "/"+_cumulativePrepared +"/" + _cumulativePaddingStripped + "] remaining [" + ready + " ready, " + encrypted + " still encrypted]"); + } + + public void mark(int readLimit) {} + public void reset() throws IOException { throw new IOException("Reset not supported"); } + public boolean markSupported() { return false; } + + /** + * Retrieve the next ready byte, or null if no bytes are ready. this does not refill or block + * + */ + private Integer getNext() { + if (_readyBuf.size() > 0) { + return (Integer)_readyBuf.remove(0); + } else { + return null; + } + } + + /** + * Read at least one new byte from the underlying stream, and up to max new bytes, + * but not necessarily enough for a new decrypted block. This blocks until at least + * one new byte is read from the stream + * + */ + private void refill(int max) throws IOException { + if (!_eofFound) { + byte buf[] = new byte[max]; + int read = in.read(buf); + if (read == -1) { + _eofFound = true; + } else if (read > 0) { + //_log.debug("Read from the source stream " + read + " bytes"); + _cumulativeRead += read; + _encryptedBuf.write(buf, 0, read); + } + } + if (false) return; // true to keep the data for decrypt/display on close + if (_encryptedBuf.size() > 0) { + if (_encryptedBuf.size() >= DECRYPT_SIZE) { + //_log.debug("We have " + _encryptedBuf.size() + " available to decrypt... doing so"); + decrypt(); + //if (_encryptedBuf.size() > 0) + // _log.debug("Bytes left in the encrypted buffer after decrypt: " + _encryptedBuf.size()); + } else { + if (_eofFound) { + //_log.debug("EOF and not enough bytes to decrypt [size = " + _encryptedBuf.size() + " totalCumulative: " + _cumulativeRead + "/"+_cumulativePrepared +"]!"); + } else { + //_log.debug("Not enough bytes to decrypt [size = " + _encryptedBuf.size() + " totalCumulative: " + _cumulativeRead + "/"+_cumulativePrepared +"]"); + } + } + } + } + + /** + * Take (n*BLOCK_SIZE) bytes off the _encryptedBuf, decrypt them, and place + * them on _readyBuf + * + */ + private void decrypt() throws IOException { + byte encrypted[] = _encryptedBuf.toByteArray(); + _encryptedBuf.reset(); + + if ( (encrypted == null) || (encrypted.length <= 0) ) + throw new IOException("Error decrypting - no data to decrypt"); + + int numBlocks = encrypted.length / BLOCK_SIZE; + if ( (encrypted.length % BLOCK_SIZE) != 0) { + // it was flushed / handled off the BLOCK_SIZE segments, so put the excess + // back into the _encryptedBuf for later handling + int trailing = encrypted.length % BLOCK_SIZE; + _encryptedBuf.write(encrypted, encrypted.length - trailing, trailing); + byte nencrypted[] = new byte[encrypted.length - trailing]; + System.arraycopy(encrypted, 0, nencrypted, 0, nencrypted.length); + encrypted = nencrypted; + _log.warn("Decrypt got odd segment - " + trailing + " bytes pushed back for later decryption - corrupted or slow data stream perhaps?"); + } else { + //_log.info(encrypted.length + " bytes makes up " + numBlocks + " blocks to decrypt normally"); + } + + byte block[] = new byte[BLOCK_SIZE]; + for (int i = 0; i < numBlocks; i++) { + System.arraycopy(encrypted, i*BLOCK_SIZE, block, 0, BLOCK_SIZE); + byte decrypted[] = _engine.decrypt(block, _key, _lastBlock); + byte data[] = CryptixAESEngine.xor(decrypted, _lastBlock); + int cleaned[] = stripPadding(data); + for (int j = 0; j < cleaned.length; j++) { + if ( ((int)cleaned[j]) <= 0) { + cleaned[j] += 256; + //_log.error("(modified: " + cleaned[j] + ")"); + } + _readyBuf.add(new Integer(cleaned[j])); + } + _cumulativePrepared += cleaned.length; + //_log.debug("Updating last block for inputStream"); + System.arraycopy(decrypted, 0, _lastBlock, 0, BLOCK_SIZE); + } + + int remaining = encrypted.length % BLOCK_SIZE; + if (remaining != 0) { + _encryptedBuf.write(encrypted, encrypted.length-remaining, remaining); + _log.debug("After pushing " + remaining + " bytes back onto the buffer, lets delay 1s our action so we don't fast busy until the net transfers data"); + try { Thread.sleep(1000); } catch (InterruptedException ie) {} + } else { + //_log.debug("No remaining encrypted bytes beyond the block size"); + } + } + + /** + * PKCS#5 specifies the padding for the block has the # of padding bytes + * located in the last byte of the block, and each of the padding bytes are + * equal to that value. + * e.g. in a 4 byte block: + * 0x0a padded would become + * 0x0a 0x03 0x03 0x03 + * e.g. in a 4 byte block: + * 0x01 0x02 padded would become + * 0x01 0x02 0x02 0x02 + * + * We use 16 byte blocks in this AES implementation + * + */ + private int[] stripPadding(byte data[]) throws IOException { + int numPadBytes = (int)data[data.length-1]; + if ( (numPadBytes >= data.length) || (numPadBytes <= 0) ) + throw new IOException("Invalid number of pad bytes"); + int rv[] = new int[data.length-numPadBytes]; + // optional, but a really good idea: verify the padding + if (true) { + for (int i = data.length - numPadBytes; i < data.length; i++) { + if (data[i] != (byte)numPadBytes) { + throw new IOException("Incorrect padding on decryption: data["+i+"] = " + data[i] + " not " + numPadBytes); + } + } + } + for (int i = 0; i < rv.length; i++) + rv[i] = data[i]; + _cumulativePaddingStripped += numPadBytes; + return rv; + } + + int remainingBytes() { return _encryptedBuf.size(); } + int readyBytes() { return _readyBuf.size(); } + + /** + * Test AESOutputStream/AESInputStream + */ + public static void main(String args[]) { + byte orig[] = new byte[1024*32]; + RandomSource.getInstance().nextBytes(orig); + //byte orig[] = "you are my sunshine, my only sunshine".getBytes(); + SessionKey key = KeyGenerator.getInstance().generateSessionKey(); + byte iv[] = "there once was a".getBytes(); + + for (int i = 0; i < 20; i++) { + runTest(orig, key, iv); + } + + _log.info("Done testing 32KB data"); + + orig = new byte[20]; + RandomSource.getInstance().nextBytes(orig); + for (int i = 0; i < 20; i++) { + runTest(orig, key, iv); + } + + _log.info("Done testing 20 byte data"); + + orig = new byte[3]; + RandomSource.getInstance().nextBytes(orig); + for (int i = 0; i < 20; i++) { + runTest(orig, key, iv); + } + + _log.info("Done testing 3 byte data"); + + orig = new byte[0]; + RandomSource.getInstance().nextBytes(orig); + for (int i = 0; i < 20; i++) { + runTest(orig, key, iv); + } + + _log.info("Done testing 0 byte data"); + + orig = new byte[32]; + RandomSource.getInstance().nextBytes(orig); + runOffsetTest(orig, key, iv); + + _log.info("Done testing offset test (it should have come back with a statement NOT EQUAL!)"); + + try { Thread.sleep(30*1000); } catch (InterruptedException ie) {} + } + + private static void runTest(byte orig[], SessionKey key, byte[] iv) { + try { + long start = Clock.getInstance().now(); + ByteArrayOutputStream origStream = new ByteArrayOutputStream(512); + AESOutputStream out = new AESOutputStream(origStream, key, iv); + out.write(orig); + out.close(); + + byte encrypted[] = origStream.toByteArray(); + long endE = Clock.getInstance().now(); + + ByteArrayInputStream encryptedStream = new ByteArrayInputStream(encrypted); + AESInputStream in = new AESInputStream(encryptedStream, key, iv); + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + byte buf[] = new byte[1024*32]; + int read = DataHelper.read(in, buf); + if (read > 0) + baos.write(buf, 0, read); + in.close(); + byte fin[] = baos.toByteArray(); + long end = Clock.getInstance().now(); + Hash origHash = SHA256Generator.getInstance().calculateHash(orig); + + Hash newHash = SHA256Generator.getInstance().calculateHash(fin); + boolean eq = origHash.equals(newHash); + if (eq) + _log.info("Equal hashes. hash: " + origHash); + else + _log.error("NOT EQUAL! \norig: \t" + Base64.encode(orig) + "\nnew : \t" + Base64.encode(fin)); + boolean ok = DataHelper.eq(orig, fin); + _log.debug("EQ data? " + ok + " origLen: " + orig.length + " fin.length: " + fin.length); + _log.debug("Time to D(E(" + orig.length + ")): " + (end - start) + "ms"); + _log.debug("Time to E(" + orig.length + "): " + (endE - start) + "ms"); + _log.debug("Time to D(" + orig.length + "): " + (end - endE) + "ms"); + + } catch (Throwable t) { + _log.error("ERROR transferring", t); + } + //try { Thread.sleep(5000); } catch (Throwable t) {} + } + + private static void runOffsetTest(byte orig[], SessionKey key, byte[] iv) { + try { + long start = Clock.getInstance().now(); + ByteArrayOutputStream origStream = new ByteArrayOutputStream(512); + AESOutputStream out = new AESOutputStream(origStream, key, iv); + out.write(orig); + out.close(); + + byte encrypted[] = origStream.toByteArray(); + long endE = Clock.getInstance().now(); + + _log.info("Encrypted segment length: " + encrypted.length); + byte encryptedSegment[] = new byte[40]; + System.arraycopy(encrypted, 0, encryptedSegment, 0, 40); + + ByteArrayInputStream encryptedStream = new ByteArrayInputStream(encryptedSegment); + AESInputStream in = new AESInputStream(encryptedStream, key, iv); + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + byte buf[] = new byte[1024*32]; + int read = DataHelper.read(in, buf); + int remaining = in.remainingBytes(); + int readyBytes = in.readyBytes(); + _log.info("Read: " + read); + if (read > 0) + baos.write(buf, 0, read); + in.close(); + byte fin[] = baos.toByteArray(); + _log.info("fin.length: " + fin.length + " remaining: " + remaining + " ready: " + readyBytes); + long end = Clock.getInstance().now(); + Hash origHash = SHA256Generator.getInstance().calculateHash(orig); + + Hash newHash = SHA256Generator.getInstance().calculateHash(fin); + boolean eq = origHash.equals(newHash); + if (eq) + _log.info("Equal hashes. hash: " + origHash); + else + _log.error("NOT EQUAL! \norig: \t" + Base64.encode(orig) + "\nnew : \t" + Base64.encode(fin)); + boolean ok = DataHelper.eq(orig, fin); + _log.debug("EQ data? " + ok + " origLen: " + orig.length + " fin.length: " + fin.length); + _log.debug("Time to D(E(" + orig.length + ")): " + (end - start) + "ms"); + _log.debug("Time to E(" + orig.length + "): " + (endE - start) + "ms"); + _log.debug("Time to D(" + orig.length + "): " + (end - endE) + "ms"); + + } catch (Throwable t) { + _log.error("ERROR transferring", t); + } + //try { Thread.sleep(5000); } catch (Throwable t) {} + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/crypto/AESOutputStream.java b/core/java/src/net/i2p/crypto/AESOutputStream.java new file mode 100644 index 000000000..8c62f36ce --- /dev/null +++ b/core/java/src/net/i2p/crypto/AESOutputStream.java @@ -0,0 +1,125 @@ +package net.i2p.crypto; +/* + * 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.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +import net.i2p.data.SessionKey; +import net.i2p.util.Log; + +/** + * This writes everything as CBC with PKCS#5 padding, but each block is padded + * so as soon as a block is received it can be decrypted (rather than wait for + * an arbitrary number of blocks to arrive). That means that each block sent + * will contain exactly one padding byte (unless it was flushed with + * numBytes % (BLOCK_SIZE-1) != 0, in which case that last block will be padded + * with up to 15 bytes). So we have an expansion factor of 6.25%. c'est la vie + * + */ +public class AESOutputStream extends FilterOutputStream { + private final static CryptixAESEngine _engine = new CryptixAESEngine(); + private final static Log _log = new Log(AESOutputStream.class); + private SessionKey _key; + private byte[] _lastBlock; + private ByteArrayOutputStream _inBuf; + private long _cumulativeProvided; // how many bytes provided to this stream + private long _cumulativeWritten; // how many bytes written to the underlying stream + private long _cumulativePadding; // how many bytes of padding written + + public final static float EXPANSION_FACTOR = 1.0625f; // 6% overhead w/ the padding + + private final static int BLOCK_SIZE = CryptixRijndael_Algorithm._BLOCK_SIZE; + private final static int MAX_BUF = 256; + + public AESOutputStream(OutputStream source, SessionKey key, byte[] iv) { + super(source); + _key = key; + _lastBlock = new byte[BLOCK_SIZE]; + System.arraycopy(iv, 0, _lastBlock, 0, BLOCK_SIZE); + _inBuf = new ByteArrayOutputStream(MAX_BUF); + } + + public void write(int val) throws IOException { + _cumulativeProvided++; + _inBuf.write(val); + if (_inBuf.size() > MAX_BUF) + doFlush(); + } + public void write(byte src[]) throws IOException { + _cumulativeProvided += src.length; + _inBuf.write(src); + if (_inBuf.size() > MAX_BUF) + doFlush(); + } + public void write(byte src[], int off, int len) throws IOException { + _cumulativeProvided += len; + _inBuf.write(src, off, len); + if (_inBuf.size() > MAX_BUF) + doFlush(); + } + + public void close() throws IOException { + flush(); + out.close(); + _inBuf.reset(); + _log.debug("Cumulative bytes provided to this stream / written out / padded: " + _cumulativeProvided + "/" + _cumulativeWritten + "/" + _cumulativePadding); + } + public void flush() throws IOException { doFlush(); out.flush(); } + + private void doFlush() throws IOException { + writeEncrypted(_inBuf.toByteArray()); + _inBuf.reset(); + } + + /** + * Encrypt an arbitrary size array with AES using CBC and PKCS#5 padding, + * write it to the stream, and set _lastBlock to the last encrypted + * block. This operation works by taking every (BLOCK_SIZE-1) bytes + * from the src, padding it with PKCS#5 (aka adding 0x01), and encrypting + * it. If the last block doesn't contain exactly (BLOCK_SIZE-1) bytes, it + * is padded with PKCS#5 as well (adding # padding bytes repeated that many + * times). + * + */ + private void writeEncrypted(byte src[]) throws IOException { + if ( (src == null) || (src.length == 0) ) return; + int numBlocks = src.length/(BLOCK_SIZE-1); + + byte block[] = new byte[BLOCK_SIZE]; + block[BLOCK_SIZE-1] = 0x01; // the padding byte for "full" blocks + for (int i = 0; i < numBlocks; i++) { + System.arraycopy(src, i*15, block, 0, 15); + byte data[] = _engine.xor(block, _lastBlock); + byte encrypted[] = _engine.encrypt(data, _key, _lastBlock); + _cumulativeWritten += encrypted.length; + out.write(encrypted); + System.arraycopy(encrypted, encrypted.length-BLOCK_SIZE, _lastBlock, 0, BLOCK_SIZE); + _cumulativePadding++; + } + + if (src.length % 15 != 0) { + // we need to do non trivial padding + int remainingBytes = src.length-numBlocks*15; + int paddingBytes = BLOCK_SIZE - remainingBytes; + System.arraycopy(src, numBlocks*15, block, 0, remainingBytes); + Arrays.fill(block, remainingBytes, BLOCK_SIZE, (byte)paddingBytes); + byte data[] = _engine.xor(block, _lastBlock); + byte encrypted[] = _engine.encrypt(data, _key, _lastBlock); + out.write(encrypted); + System.arraycopy(encrypted, encrypted.length-BLOCK_SIZE, _lastBlock, 0, BLOCK_SIZE); + _cumulativePadding += paddingBytes; + _cumulativeWritten += encrypted.length; + } + } + +} \ No newline at end of file diff --git a/core/java/src/net/i2p/crypto/CryptixAESEngine.java b/core/java/src/net/i2p/crypto/CryptixAESEngine.java new file mode 100644 index 000000000..b44feaf2e --- /dev/null +++ b/core/java/src/net/i2p/crypto/CryptixAESEngine.java @@ -0,0 +1,150 @@ +package net.i2p.crypto; +/* + * 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.InvalidKeyException; + +import net.i2p.data.SessionKey; +import net.i2p.util.Log; + +/** + * Wrapper for AES cypher operation using Cryptix's Rijndael implementation. Implements + * CBC with a 16 byte IV. + * Problems: + * Only supports data of size mod 16 bytes - no inherent padding. + * + * @author jrandom, thecrypto + * @license GPL + */ +public class CryptixAESEngine extends AESEngine { + private final static Log _log = new Log(CryptixAESEngine.class); + private final static CryptixRijndael_Algorithm _algo = new CryptixRijndael_Algorithm(); + private final static boolean USE_FAKE_CRYPTO = false; + private final static byte FAKE_KEY = 0x2A; + + public byte[] encrypt(byte payload[], SessionKey sessionKey, byte initializationVector[]) { + if ( (initializationVector == null) || (payload == null) || (payload.length <= 0) || (sessionKey == null) || (initializationVector.length != 16) ) + return null; + + if (USE_FAKE_CRYPTO) { + _log.warn("AES Crypto disabled! Using trivial XOR"); + byte rv[] = new byte[payload.length]; + for (int i = 0; i < rv.length; i++) + rv[i] = (byte)(payload[i] ^ FAKE_KEY); + return rv; + } + + int numblock = payload.length/16; + if (payload.length % 16 != 0) + numblock++; + byte[][] plain = new byte[numblock][16]; + for (int x = 0; x < numblock; x++) { + for (int y = 0; y < 16; y++) { + plain[x][y] = payload[x*16+y]; + } + } + + byte[][] cipher = new byte[numblock][16]; + cipher[0] = encrypt(xor(initializationVector, plain[0]), sessionKey); + for (int x = 1; x < numblock; x++) { + cipher[x] = encrypt(xor(cipher[x-1], plain[x]), sessionKey); + } + + byte[] ret = new byte[numblock * 16]; + for (int x = 0; x < numblock; x++) { + for (int y = 0; y < 16; y++) { + ret[x*16+y] = cipher[x][y]; + } + } + + return ret; + } + + public byte[] decrypt(byte payload[], SessionKey sessionKey, byte initializationVector[]) { + if ( (initializationVector == null) || (payload == null) || (payload.length <= 0) || (sessionKey == null) || (initializationVector.length != 16) ) + return null; + + if (USE_FAKE_CRYPTO) { + _log.warn("AES Crypto disabled! Using trivial XOR"); + byte rv[] = new byte[payload.length]; + for (int i = 0; i < rv.length; i++) + rv[i] = (byte)(payload[i] ^ FAKE_KEY); + return rv; + } + + int numblock = payload.length/16; + if (payload.length % 16 != 0) + numblock++; + byte[][] cipher = new byte[numblock][16]; + for (int x = 0; x < numblock; x++) { + for (int y = 0; y < 16; y++) { + cipher[x][y] = payload[x*16+y]; + } + } + + byte[][] plain = new byte[numblock][16]; + plain[0] = xor(decrypt(cipher[0], sessionKey), initializationVector); + for (int x = 1; x < numblock; x++) { + plain[x] = xor(decrypt(cipher[x], sessionKey), cipher[x-1]); + } + + byte[] ret = new byte[numblock * 16]; + for (int x = 0; x < numblock; x++) { + for (int y = 0; y < 16; y++) { + ret[x*16+y] = plain[x][y]; + } + } + + return ret; + } + + + final static byte[] xor (byte[] a, byte[] b) { + if ( (a == null) || (b == null) || (a.length != b.length) ) + return null; + byte[] ret = new byte[a.length]; + for (int x = 0; x < a.length; x++) { + ret[x] = (byte)(a[x] ^ b[x]); + } + return ret; + } + + /** Encrypt the payload with the session key + * @param payload data to be encrypted + * @param sessionKey private esession key to encrypt to + * @return encrypted data + */ + final static byte[] encrypt(byte payload[], SessionKey sessionKey) { + try { + Object key = CryptixRijndael_Algorithm.makeKey(sessionKey.getData(), 16); + byte rv[] = new byte[payload.length]; + CryptixRijndael_Algorithm.blockEncrypt(payload, rv, 0, key, 16); + return rv; + } catch (InvalidKeyException ike) { + _log.error("Invalid key", ike); + return null; + } + } + /** decrypt the data with the session key provided + * @param payload encrypted data + * @param sessionKey private session key + * @return unencrypted data + */ + final static byte[] decrypt(byte payload[], SessionKey sessionKey) { + try { + Object key = CryptixRijndael_Algorithm.makeKey(sessionKey.getData(), 16); + byte rv[] = new byte[payload.length]; + CryptixRijndael_Algorithm.blockDecrypt(payload, rv, 0, key, 16); + return rv; + } catch (InvalidKeyException ike) { + _log.error("Invalid key", ike); + return null; + } + } +} diff --git a/core/java/src/net/i2p/crypto/CryptixRijndael_Algorithm.java b/core/java/src/net/i2p/crypto/CryptixRijndael_Algorithm.java new file mode 100644 index 000000000..2f22578da --- /dev/null +++ b/core/java/src/net/i2p/crypto/CryptixRijndael_Algorithm.java @@ -0,0 +1,874 @@ +/* + * Copyright (c) 1997, 1998 Systemics Ltd on behalf of + * the Cryptix Development Team. All rights reserved. + */ +package net.i2p.crypto; + +import net.i2p.util.Clock; +import java.io.PrintWriter; +import java.security.InvalidKeyException; + +//........................................................................... +/** + * Rijndael --pronounced Reindaal-- is a variable block-size (128-, 192- and + * 256-bit), variable key-size (128-, 192- and 256-bit) symmetric cipher.

+ * + * Rijndael was written by Vincent + * Rijmen and Joan Daemen.

+ * + * Portions of this code are Copyright © 1997, 1998 + * Systemics Ltd on behalf of the + * Cryptix Development Team. + *
All rights reserved.

+ * + * @author Raif S. Naffah + * @author Paulo S. L. M. Barreto + * + * License is apparently available from http://www.cryptix.org/docs/license.html + */ +public final class CryptixRijndael_Algorithm // implicit no-argument constructor +{ +// Debugging methods and variables +//........................................................................... + + static final String _NAME = "Rijndael_Algorithm"; + static final boolean _IN = true, _OUT = false; + + static final boolean _RDEBUG = false; + static final int _debuglevel = 0; // RDEBUG ? Rijndael_Properties.getLevel(NAME): 0; +// static final PrintWriter err = RDEBUG ? Rijndael_Properties.getOutput() : null; + static final PrintWriter _err = new PrintWriter(new java.io.OutputStreamWriter(System.err)); + + static final boolean _TRACE = false; // Rijndael_Properties.isTraceable(NAME); + + static void debug (String s) { _err.println(">>> "+_NAME+": "+s); } + static void trace (boolean in, String s) { + if (_TRACE) _err.println((in?"==> ":"<== ")+_NAME+"."+s); + } + static void trace (String s) { if (_TRACE) _err.println("<=> "+_NAME+"."+s); } + + +// Constants and variables +//........................................................................... + + static final int _BLOCK_SIZE = 16; // default block size in bytes + + static final int[] _alog = new int[256]; + static final int[] _log = new int[256]; + + static final byte[] _S = new byte[256]; + static final byte[] _Si = new byte[256]; + static final int[] _T1 = new int[256]; + static final int[] _T2 = new int[256]; + static final int[] _T3 = new int[256]; + static final int[] _T4 = new int[256]; + static final int[] _T5 = new int[256]; + static final int[] _T6 = new int[256]; + static final int[] _T7 = new int[256]; + static final int[] _T8 = new int[256]; + static final int[] _U1 = new int[256]; + static final int[] _U2 = new int[256]; + static final int[] _U3 = new int[256]; + static final int[] _U4 = new int[256]; + static final byte[] _rcon = new byte[30]; + + static final int[][][] _shifts = new int[][][] { + { {0, 0}, {1, 3}, {2, 2}, {3, 1} }, + { {0, 0}, {1, 5}, {2, 4}, {3, 3} }, + { {0, 0}, {1, 7}, {3, 5}, {4, 4} } + }; + + private static final char[] _HEX_DIGITS = { + '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' + }; + + +// Static code - to intialise S-boxes and T-boxes +//........................................................................... + + static { + long time = Clock.getInstance().now(); + +if (_RDEBUG && _debuglevel > 6) { +System.out.println("Algorithm Name: Rijndael ver 0.1"); +System.out.println("Electronic Codebook (ECB) Mode"); +System.out.println(); +} + int ROOT = 0x11B; + int i, j = 0; + + // + // produce log and alog tables, needed for multiplying in the + // field GF(2^m) (generator = 3) + // + _alog[0] = 1; + for (i = 1; i < 256; i++) { + j = (_alog[i-1] << 1) ^ _alog[i-1]; + if ((j & 0x100) != 0) j ^= ROOT; + _alog[i] = j; + } + for (i = 1; i < 255; i++) _log[_alog[i]] = i; + byte[][] A = new byte[][] { + {1, 1, 1, 1, 1, 0, 0, 0}, + {0, 1, 1, 1, 1, 1, 0, 0}, + {0, 0, 1, 1, 1, 1, 1, 0}, + {0, 0, 0, 1, 1, 1, 1, 1}, + {1, 0, 0, 0, 1, 1, 1, 1}, + {1, 1, 0, 0, 0, 1, 1, 1}, + {1, 1, 1, 0, 0, 0, 1, 1}, + {1, 1, 1, 1, 0, 0, 0, 1} + }; + byte[] B = new byte[] { 0, 1, 1, 0, 0, 0, 1, 1}; + + // + // substitution box based on F^{-1}(x) + // + int t; + byte[][] box = new byte[256][8]; + box[1][7] = 1; + for (i = 2; i < 256; i++) { + j = _alog[255 - _log[i]]; + for (t = 0; t < 8; t++) + box[i][t] = (byte)((j >>> (7 - t)) & 0x01); + } + // + // affine transform: box[i] <- B + A*box[i] + // + byte[][] cox = new byte[256][8]; + for (i = 0; i < 256; i++) + for (t = 0; t < 8; t++) { + cox[i][t] = B[t]; + for (j = 0; j < 8; j++) + cox[i][t] ^= A[t][j] * box[i][j]; + } + // + // S-boxes and inverse S-boxes + // + for (i = 0; i < 256; i++) { + _S[i] = (byte)(cox[i][0] << 7); + for (t = 1; t < 8; t++) + _S[i] ^= cox[i][t] << (7-t); + _Si[_S[i] & 0xFF] = (byte) i; + } + // + // T-boxes + // + byte[][] G = new byte[][] { + {2, 1, 1, 3}, + {3, 2, 1, 1}, + {1, 3, 2, 1}, + {1, 1, 3, 2} + }; + byte[][] AA = new byte[4][8]; + for (i = 0; i < 4; i++) { + for (j = 0; j < 4; j++) AA[i][j] = G[i][j]; + AA[i][i+4] = 1; + } + byte pivot, tmp; + byte[][] iG = new byte[4][4]; + for (i = 0; i < 4; i++) { + pivot = AA[i][i]; + if (pivot == 0) { + t = i + 1; + while ((AA[t][i] == 0) && (t < 4)) + t++; + if (t == 4) + throw new RuntimeException("G matrix is not invertible"); + else { + for (j = 0; j < 8; j++) { + tmp = AA[i][j]; + AA[i][j] = AA[t][j]; + AA[t][j] = (byte) tmp; + } + pivot = AA[i][i]; + } + } + for (j = 0; j < 8; j++) + if (AA[i][j] != 0) + AA[i][j] = (byte) + _alog[(255 + _log[AA[i][j] & 0xFF] - _log[pivot & 0xFF]) % 255]; + for (t = 0; t < 4; t++) + if (i != t) { + for (j = i+1; j < 8; j++) + AA[t][j] ^= mul(AA[i][j], AA[t][i]); + AA[t][i] = 0; + } + } + for (i = 0; i < 4; i++) + for (j = 0; j < 4; j++) iG[i][j] = AA[i][j + 4]; + + int s; + for (t = 0; t < 256; t++) { + s = _S[t]; + _T1[t] = mul4(s, G[0]); + _T2[t] = mul4(s, G[1]); + _T3[t] = mul4(s, G[2]); + _T4[t] = mul4(s, G[3]); + + s = _Si[t]; + _T5[t] = mul4(s, iG[0]); + _T6[t] = mul4(s, iG[1]); + _T7[t] = mul4(s, iG[2]); + _T8[t] = mul4(s, iG[3]); + + _U1[t] = mul4(t, iG[0]); + _U2[t] = mul4(t, iG[1]); + _U3[t] = mul4(t, iG[2]); + _U4[t] = mul4(t, iG[3]); + } + // + // round constants + // + _rcon[0] = 1; + int r = 1; + for (t = 1; t < 30; ) _rcon[t++] = (byte)(r = mul(2, r)); + + time = Clock.getInstance().now() - time; + +if (_RDEBUG && _debuglevel > 8) { +System.out.println("=========="); +System.out.println(); +System.out.println("Static Data"); +System.out.println(); +System.out.println("S[]:"); for(i=0;i<16;i++) { for(j=0;j<16;j++) System.out.print("0x"+byteToString(_S[i*16+j])+", "); System.out.println();} +System.out.println(); +System.out.println("Si[]:"); for(i=0;i<16;i++) { for(j=0;j<16;j++) System.out.print("0x"+byteToString(_Si[i*16+j])+", "); System.out.println();} + +System.out.println(); +System.out.println("iG[]:"); for(i=0;i<4;i++){for(j=0;j<4;j++) System.out.print("0x"+byteToString(iG[i][j])+", "); System.out.println();} + +System.out.println(); +System.out.println("T1[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_T1[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("T2[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_T2[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("T3[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_T3[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("T4[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_T4[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("T5[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_T5[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("T6[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_T6[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("T7[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_T7[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("T8[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_T8[i*4+j])+", "); System.out.println();} + +System.out.println(); +System.out.println("U1[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_U1[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("U2[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_U2[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("U3[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_U3[i*4+j])+", "); System.out.println();} +System.out.println(); +System.out.println("U4[]:"); for(i=0;i<64;i++){for(j=0;j<4;j++) System.out.print("0x"+intToString(_U4[i*4+j])+", "); System.out.println();} + +System.out.println(); +System.out.println("rcon[]:"); for(i=0;i<5;i++){for(j=0;j<6;j++) System.out.print("0x"+byteToString(_rcon[i*6+j])+", "); System.out.println();} + +System.out.println(); +System.out.println("Total initialization time: "+time+" ms."); +System.out.println(); +} + } + + // multiply two elements of GF(2^m) + static final int mul (int a, int b) { + return (a != 0 && b != 0) ? + _alog[(_log[a & 0xFF] + _log[b & 0xFF]) % 255] : + 0; + } + + // convenience method used in generating Transposition boxes + static final int mul4 (int a, byte[] b) { + if (a == 0) return 0; + a = _log[a & 0xFF]; + int a0 = (b[0] != 0) ? _alog[(a + _log[b[0] & 0xFF]) % 255] & 0xFF : 0; + int a1 = (b[1] != 0) ? _alog[(a + _log[b[1] & 0xFF]) % 255] & 0xFF : 0; + int a2 = (b[2] != 0) ? _alog[(a + _log[b[2] & 0xFF]) % 255] & 0xFF : 0; + int a3 = (b[3] != 0) ? _alog[(a + _log[b[3] & 0xFF]) % 255] & 0xFF : 0; + return a0 << 24 | a1 << 16 | a2 << 8 | a3; + } + + +// Basic API methods +//........................................................................... + + /** + * Convenience method to expand a user-supplied key material into a + * session key, assuming Rijndael's default block size (128-bit). + * + * @param k The 128/192/256-bit user-key to use. + * @exception InvalidKeyException If the key is invalid. + */ + public static final Object makeKey (byte[] k) throws InvalidKeyException { + return makeKey(k, _BLOCK_SIZE); + } + + /** + * Convenience method to encrypt exactly one block of plaintext, assuming + * Rijndael's default block size (128-bit). + * + * @param in The plaintext. + * @param result The resulting ciphertext. + * @param inOffset Index of in from which to start considering data. + * @param sessionKey The session key to use for encryption. + * @return The ciphertext generated from a plaintext using the session key. + */ + public static final void + blockEncrypt (byte[] in, byte[] result, int inOffset, Object sessionKey) { +if (_RDEBUG) trace(_IN, "blockEncrypt("+in+", "+inOffset+", "+sessionKey+")"); + int[][] Ke = (int[][]) ((Object[]) sessionKey)[0]; // extract encryption round keys + int ROUNDS = Ke.length - 1; + int[] Ker = Ke[0]; + + // plaintext to ints + key + int t0 = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Ker[0]; + int t1 = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Ker[1]; + int t2 = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Ker[2]; + int t3 = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Ker[3]; + + int a0, a1, a2, a3; + for (int r = 1; r < ROUNDS; r++) { // apply round transforms + Ker = Ke[r]; + a0 = (_T1[(t0 >>> 24) & 0xFF] ^ + _T2[(t1 >>> 16) & 0xFF] ^ + _T3[(t2 >>> 8) & 0xFF] ^ + _T4[ t3 & 0xFF] ) ^ Ker[0]; + a1 = (_T1[(t1 >>> 24) & 0xFF] ^ + _T2[(t2 >>> 16) & 0xFF] ^ + _T3[(t3 >>> 8) & 0xFF] ^ + _T4[ t0 & 0xFF] ) ^ Ker[1]; + a2 = (_T1[(t2 >>> 24) & 0xFF] ^ + _T2[(t3 >>> 16) & 0xFF] ^ + _T3[(t0 >>> 8) & 0xFF] ^ + _T4[ t1 & 0xFF] ) ^ Ker[2]; + a3 = (_T1[(t3 >>> 24) & 0xFF] ^ + _T2[(t0 >>> 16) & 0xFF] ^ + _T3[(t1 >>> 8) & 0xFF] ^ + _T4[ t2 & 0xFF] ) ^ Ker[3]; + t0 = a0; + t1 = a1; + t2 = a2; + t3 = a3; +if (_RDEBUG && _debuglevel > 6) System.out.println("CT"+r+"="+intToString(t0)+intToString(t1)+intToString(t2)+intToString(t3)); + } + + // last round is special + Ker = Ke[ROUNDS]; + int tt = Ker[0]; + result[ 0] = (byte)(_S[(t0 >>> 24) & 0xFF] ^ (tt >>> 24)); + result[ 1] = (byte)(_S[(t1 >>> 16) & 0xFF] ^ (tt >>> 16)); + result[ 2] = (byte)(_S[(t2 >>> 8) & 0xFF] ^ (tt >>> 8)); + result[ 3] = (byte)(_S[ t3 & 0xFF] ^ tt ); + tt = Ker[1]; + result[ 4] = (byte)(_S[(t1 >>> 24) & 0xFF] ^ (tt >>> 24)); + result[ 5] = (byte)(_S[(t2 >>> 16) & 0xFF] ^ (tt >>> 16)); + result[ 6] = (byte)(_S[(t3 >>> 8) & 0xFF] ^ (tt >>> 8)); + result[ 7] = (byte)(_S[ t0 & 0xFF] ^ tt ); + tt = Ker[2]; + result[ 8] = (byte)(_S[(t2 >>> 24) & 0xFF] ^ (tt >>> 24)); + result[ 9] = (byte)(_S[(t3 >>> 16) & 0xFF] ^ (tt >>> 16)); + result[10] = (byte)(_S[(t0 >>> 8) & 0xFF] ^ (tt >>> 8)); + result[11] = (byte)(_S[ t1 & 0xFF] ^ tt ); + tt = Ker[3]; + result[12] = (byte)(_S[(t3 >>> 24) & 0xFF] ^ (tt >>> 24)); + result[13] = (byte)(_S[(t0 >>> 16) & 0xFF] ^ (tt >>> 16)); + result[14] = (byte)(_S[(t1 >>> 8) & 0xFF] ^ (tt >>> 8)); + result[15] = (byte)(_S[ t2 & 0xFF] ^ tt ); +if (_RDEBUG && _debuglevel > 6) { +System.out.println("CT="+toString(result)); +System.out.println(); +} +if (_RDEBUG) trace(_OUT, "blockEncrypt()"); + } + + /** + * Convenience method to decrypt exactly one block of plaintext, assuming + * Rijndael's default block size (128-bit). + * + * @param in The ciphertext. + * @param result the resulting ciphertext + * @param inOffset Index of in from which to start considering data. + * @param sessionKey The session key to use for decryption. + * @return The plaintext generated from a ciphertext using the session key. + */ + public static final void + blockDecrypt (byte[] in, byte[] result, int inOffset, Object sessionKey) { +if (_RDEBUG) trace(_IN, "blockDecrypt("+in+", "+inOffset+", "+sessionKey+")"); + int[][] Kd = (int[][]) ((Object[]) sessionKey)[1]; // extract decryption round keys + int ROUNDS = Kd.length - 1; + int[] Kdr = Kd[0]; + + // ciphertext to ints + key + int t0 = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Kdr[0]; + int t1 = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Kdr[1]; + int t2 = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Kdr[2]; + int t3 = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Kdr[3]; + + int a0, a1, a2, a3; + for (int r = 1; r < ROUNDS; r++) { // apply round transforms + Kdr = Kd[r]; + a0 = (_T5[(t0 >>> 24) & 0xFF] ^ + _T6[(t3 >>> 16) & 0xFF] ^ + _T7[(t2 >>> 8) & 0xFF] ^ + _T8[ t1 & 0xFF] ) ^ Kdr[0]; + a1 = (_T5[(t1 >>> 24) & 0xFF] ^ + _T6[(t0 >>> 16) & 0xFF] ^ + _T7[(t3 >>> 8) & 0xFF] ^ + _T8[ t2 & 0xFF] ) ^ Kdr[1]; + a2 = (_T5[(t2 >>> 24) & 0xFF] ^ + _T6[(t1 >>> 16) & 0xFF] ^ + _T7[(t0 >>> 8) & 0xFF] ^ + _T8[ t3 & 0xFF] ) ^ Kdr[2]; + a3 = (_T5[(t3 >>> 24) & 0xFF] ^ + _T6[(t2 >>> 16) & 0xFF] ^ + _T7[(t1 >>> 8) & 0xFF] ^ + _T8[ t0 & 0xFF] ) ^ Kdr[3]; + t0 = a0; + t1 = a1; + t2 = a2; + t3 = a3; +if (_RDEBUG && _debuglevel > 6) System.out.println("PT"+r+"="+intToString(t0)+intToString(t1)+intToString(t2)+intToString(t3)); + } + + // last round is special + Kdr = Kd[ROUNDS]; + int tt = Kdr[0]; + result[ 0] = (byte)(_Si[(t0 >>> 24) & 0xFF] ^ (tt >>> 24)); + result[ 1] = (byte)(_Si[(t3 >>> 16) & 0xFF] ^ (tt >>> 16)); + result[ 2] = (byte)(_Si[(t2 >>> 8) & 0xFF] ^ (tt >>> 8)); + result[ 3] = (byte)(_Si[ t1 & 0xFF] ^ tt ); + tt = Kdr[1]; + result[ 4] = (byte)(_Si[(t1 >>> 24) & 0xFF] ^ (tt >>> 24)); + result[ 5] = (byte)(_Si[(t0 >>> 16) & 0xFF] ^ (tt >>> 16)); + result[ 6] = (byte)(_Si[(t3 >>> 8) & 0xFF] ^ (tt >>> 8)); + result[ 7] = (byte)(_Si[ t2 & 0xFF] ^ tt ); + tt = Kdr[2]; + result[ 8] = (byte)(_Si[(t2 >>> 24) & 0xFF] ^ (tt >>> 24)); + result[ 9] = (byte)(_Si[(t1 >>> 16) & 0xFF] ^ (tt >>> 16)); + result[10] = (byte)(_Si[(t0 >>> 8) & 0xFF] ^ (tt >>> 8)); + result[11] = (byte)(_Si[ t3 & 0xFF] ^ tt ); + tt = Kdr[3]; + result[12] = (byte)(_Si[(t3 >>> 24) & 0xFF] ^ (tt >>> 24)); + result[13] = (byte)(_Si[(t2 >>> 16) & 0xFF] ^ (tt >>> 16)); + result[14] = (byte)(_Si[(t1 >>> 8) & 0xFF] ^ (tt >>> 8)); + result[15] = (byte)(_Si[ t0 & 0xFF] ^ tt ); +if (_RDEBUG && _debuglevel > 6) { +System.out.println("PT="+toString(result)); +System.out.println(); +} +if (_RDEBUG) trace(_OUT, "blockDecrypt()"); + } + + /** A basic symmetric encryption/decryption test. */ + public static boolean self_test() { return self_test(_BLOCK_SIZE); } + + +// Rijndael own methods +//........................................................................... + + /** @return The default length in bytes of the Algorithm input block. */ + public static final int blockSize() { return _BLOCK_SIZE; } + + /** + * Expand a user-supplied key material into a session key. + * + * @param k The 128/192/256-bit user-key to use. + * @param blockSize The block size in bytes of this Rijndael. + * @exception InvalidKeyException If the key is invalid. + */ + public static final /* synchronized */ Object makeKey (byte[] k, int blockSize) + throws InvalidKeyException { +if (_RDEBUG) trace(_IN, "makeKey("+k+", "+blockSize+")"); + if (k == null) + throw new InvalidKeyException("Empty key"); + if (!(k.length == 16 || k.length == 24 || k.length == 32)) + throw new InvalidKeyException("Incorrect key length"); + int ROUNDS = getRounds(k.length, blockSize); + int BC = blockSize / 4; + int[][] Ke = new int[ROUNDS + 1][BC]; // encryption round keys + int[][] Kd = new int[ROUNDS + 1][BC]; // decryption round keys + int ROUND_KEY_COUNT = (ROUNDS + 1) * BC; + int KC = k.length / 4; + int[] tk = new int[KC]; + int i, j; + + // copy user material bytes into temporary ints + for (i = 0, j = 0; i < KC; ) + tk[i++] = (k[j++] & 0xFF) << 24 | + (k[j++] & 0xFF) << 16 | + (k[j++] & 0xFF) << 8 | + (k[j++] & 0xFF); + // copy values into round key arrays + int t = 0; + for (j = 0; (j < KC) && (t < ROUND_KEY_COUNT); j++, t++) { + Ke[t / BC][t % BC] = tk[j]; + Kd[ROUNDS - (t / BC)][t % BC] = tk[j]; + } + int tt, rconpointer = 0; + while (t < ROUND_KEY_COUNT) { + // extrapolate using phi (the round key evolution function) + tt = tk[KC - 1]; + tk[0] ^= (_S[(tt >>> 16) & 0xFF] & 0xFF) << 24 ^ + (_S[(tt >>> 8) & 0xFF] & 0xFF) << 16 ^ + (_S[ tt & 0xFF] & 0xFF) << 8 ^ + (_S[(tt >>> 24) & 0xFF] & 0xFF) ^ + (_rcon[rconpointer++] & 0xFF) << 24; + if (KC != 8) + for (i = 1, j = 0; i < KC; ) { + //tk[i++] ^= tk[j++]; + // The above line replaced with the code below in order to work around + // a bug in the kjc-1.4F java compiler (which has been reported). + tk[i] ^= tk[j++]; + i++; + } + else { + for (i = 1, j = 0; i < KC / 2; ) { + //tk[i++] ^= tk[j++]; + // The above line replaced with the code below in order to work around + // a bug in the kjc-1.4F java compiler (which has been reported). + tk[i] ^= tk[j++]; + i++; + } + tt = tk[KC / 2 - 1]; + tk[KC / 2] ^= (_S[ tt & 0xFF] & 0xFF) ^ + (_S[(tt >>> 8) & 0xFF] & 0xFF) << 8 ^ + (_S[(tt >>> 16) & 0xFF] & 0xFF) << 16 ^ + (_S[(tt >>> 24) & 0xFF] & 0xFF) << 24; + for (j = KC / 2, i = j + 1; i < KC; ) { + //tk[i++] ^= tk[j++]; + // The above line replaced with the code below in order to work around + // a bug in the kjc-1.4F java compiler (which has been reported). + tk[i] ^= tk[j++]; + i++; + } + } + // copy values into round key arrays + for (j = 0; (j < KC) && (t < ROUND_KEY_COUNT); j++, t++) { + Ke[t / BC][t % BC] = tk[j]; + Kd[ROUNDS - (t / BC)][t % BC] = tk[j]; + } + } + for (int r = 1; r < ROUNDS; r++) // inverse MixColumn where needed + for (j = 0; j < BC; j++) { + tt = Kd[r][j]; + Kd[r][j] = _U1[(tt >>> 24) & 0xFF] ^ + _U2[(tt >>> 16) & 0xFF] ^ + _U3[(tt >>> 8) & 0xFF] ^ + _U4[ tt & 0xFF]; + } + // assemble the encryption (Ke) and decryption (Kd) round keys into + // one sessionKey object + Object[] sessionKey = new Object[] {Ke, Kd}; +if (_RDEBUG) trace(_OUT, "makeKey()"); + return sessionKey; + } + + /** + * Encrypt exactly one block of plaintext. + * + * @param in The plaintext. + * @param result The resulting ciphertext. + * @param inOffset Index of in from which to start considering data. + * @param sessionKey The session key to use for encryption. + * @param blockSize The block size in bytes of this Rijndael. + * @return The ciphertext generated from a plaintext using the session key. + */ + public static final void + blockEncrypt (byte[] in, byte[] result, int inOffset, Object sessionKey, int blockSize) { + if (blockSize == _BLOCK_SIZE) { + blockEncrypt(in, result, inOffset, sessionKey); + return; + } +if (_RDEBUG) trace(_IN, "blockEncrypt("+in+", "+inOffset+", "+sessionKey+", "+blockSize+")"); + Object[] sKey = (Object[]) sessionKey; // extract encryption round keys + int[][] Ke = (int[][]) sKey[0]; + + int BC = blockSize / 4; + int ROUNDS = Ke.length - 1; + int SC = BC == 4 ? 0 : (BC == 6 ? 1 : 2); + int s1 = _shifts[SC][1][0]; + int s2 = _shifts[SC][2][0]; + int s3 = _shifts[SC][3][0]; + int[] a = new int[BC]; + int[] t = new int[BC]; // temporary work array + int i; + int j = 0, tt; + + for (i = 0; i < BC; i++) // plaintext to ints + key + t[i] = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Ke[0][i]; + for (int r = 1; r < ROUNDS; r++) { // apply round transforms + for (i = 0; i < BC; i++) + a[i] = (_T1[(t[ i ] >>> 24) & 0xFF] ^ + _T2[(t[(i + s1) % BC] >>> 16) & 0xFF] ^ + _T3[(t[(i + s2) % BC] >>> 8) & 0xFF] ^ + _T4[ t[(i + s3) % BC] & 0xFF] ) ^ Ke[r][i]; + System.arraycopy(a, 0, t, 0, BC); +if (_RDEBUG && _debuglevel > 6) System.out.println("CT"+r+"="+toString(t)); + } + for (i = 0; i < BC; i++) { // last round is special + tt = Ke[ROUNDS][i]; + result[j++] = (byte)(_S[(t[ i ] >>> 24) & 0xFF] ^ (tt >>> 24)); + result[j++] = (byte)(_S[(t[(i + s1) % BC] >>> 16) & 0xFF] ^ (tt >>> 16)); + result[j++] = (byte)(_S[(t[(i + s2) % BC] >>> 8) & 0xFF] ^ (tt >>> 8)); + result[j++] = (byte)(_S[ t[(i + s3) % BC] & 0xFF] ^ tt); + } +if (_RDEBUG && _debuglevel > 6) { +System.out.println("CT="+toString(result)); +System.out.println(); +} +if (_RDEBUG) trace(_OUT, "blockEncrypt()"); + } + + /** + * Decrypt exactly one block of ciphertext. + * + * @param in The ciphertext. + * @param result The resulting ciphertext. + * @param inOffset Index of in from which to start considering data. + * @param sessionKey The session key to use for decryption. + * @param blockSize The block size in bytes of this Rijndael. + * @return The plaintext generated from a ciphertext using the session key. + */ + public static final void + blockDecrypt (byte[] in, byte[] result, int inOffset, Object sessionKey, int blockSize) { + if (blockSize == _BLOCK_SIZE) { + blockDecrypt(in, result, inOffset, sessionKey); + return; + } + +if (_RDEBUG) trace(_IN, "blockDecrypt("+in+", "+inOffset+", "+sessionKey+", "+blockSize+")"); + Object[] sKey = (Object[]) sessionKey; // extract decryption round keys + int[][] Kd = (int[][]) sKey[1]; + + int BC = blockSize / 4; + int ROUNDS = Kd.length - 1; + int SC = BC == 4 ? 0 : (BC == 6 ? 1 : 2); + int s1 = _shifts[SC][1][1]; + int s2 = _shifts[SC][2][1]; + int s3 = _shifts[SC][3][1]; + int[] a = new int[BC]; + int[] t = new int[BC]; // temporary work array + int i; + int j = 0, tt; + + for (i = 0; i < BC; i++) // ciphertext to ints + key + t[i] = ((in[inOffset++] & 0xFF) << 24 | + (in[inOffset++] & 0xFF) << 16 | + (in[inOffset++] & 0xFF) << 8 | + (in[inOffset++] & 0xFF) ) ^ Kd[0][i]; + for (int r = 1; r < ROUNDS; r++) { // apply round transforms + for (i = 0; i < BC; i++) + a[i] = (_T5[(t[ i ] >>> 24) & 0xFF] ^ + _T6[(t[(i + s1) % BC] >>> 16) & 0xFF] ^ + _T7[(t[(i + s2) % BC] >>> 8) & 0xFF] ^ + _T8[ t[(i + s3) % BC] & 0xFF] ) ^ Kd[r][i]; + System.arraycopy(a, 0, t, 0, BC); +if (_RDEBUG && _debuglevel > 6) System.out.println("PT"+r+"="+toString(t)); + } + for (i = 0; i < BC; i++) { // last round is special + tt = Kd[ROUNDS][i]; + result[j++] = (byte)(_Si[(t[ i ] >>> 24) & 0xFF] ^ (tt >>> 24)); + result[j++] = (byte)(_Si[(t[(i + s1) % BC] >>> 16) & 0xFF] ^ (tt >>> 16)); + result[j++] = (byte)(_Si[(t[(i + s2) % BC] >>> 8) & 0xFF] ^ (tt >>> 8)); + result[j++] = (byte)(_Si[ t[(i + s3) % BC] & 0xFF] ^ tt); + } +if (_RDEBUG && _debuglevel > 6) { +System.out.println("PT="+toString(result)); +System.out.println(); +} +if (_RDEBUG) trace(_OUT, "blockDecrypt()"); + } + + /** A basic symmetric encryption/decryption test for a given key size. */ + private static boolean self_test (int keysize) { +if (_RDEBUG) trace(_IN, "self_test("+keysize+")"); + boolean ok = false; + try { + byte[] kb = new byte[keysize]; + byte[] pt = new byte[_BLOCK_SIZE]; + int i; + + for (i = 0; i < keysize; i++) + kb[i] = (byte) i; + for (i = 0; i < _BLOCK_SIZE; i++) + pt[i] = (byte) i; + +if (_RDEBUG && _debuglevel > 6) { +System.out.println("=========="); +System.out.println(); +System.out.println("KEYSIZE="+(8*keysize)); +System.out.println("KEY="+toString(kb)); +System.out.println(); +} + Object key = makeKey(kb, _BLOCK_SIZE); + +if (_RDEBUG && _debuglevel > 6) { +System.out.println("Intermediate Ciphertext Values (Encryption)"); +System.out.println(); +System.out.println("PT="+toString(pt)); +} + byte[] ct = new byte[_BLOCK_SIZE]; + blockEncrypt(pt, ct, 0, key, _BLOCK_SIZE); + +if (_RDEBUG && _debuglevel > 6) { +System.out.println("Intermediate Plaintext Values (Decryption)"); +System.out.println(); +System.out.println("CT="+toString(ct)); +} + byte[] cpt = new byte[_BLOCK_SIZE]; + blockDecrypt(ct, cpt, 0, key, _BLOCK_SIZE); + + ok = areEqual(pt, cpt); + if (!ok) + throw new RuntimeException("Symmetric operation failed"); + } + catch (Exception x) { +if (_RDEBUG && _debuglevel > 0) { + debug("Exception encountered during self-test: " + x.getMessage()); + x.printStackTrace(); +} + } +if (_RDEBUG && _debuglevel > 0) debug("Self-test OK? " + ok); +if (_RDEBUG) trace(_OUT, "self_test()"); + return ok; + } + + /** + * Return The number of rounds for a given Rijndael's key and block sizes. + * + * @param keySize The size of the user key material in bytes. + * @param blockSize The desired block size in bytes. + * @return The number of rounds for a given Rijndael's key and + * block sizes. + */ + public static final int getRounds (int keySize, int blockSize) { + switch (keySize) { + case 16: + return blockSize == 16 ? 10 : (blockSize == 24 ? 12 : 14); + case 24: + return blockSize != 32 ? 12 : 14; + default: // 32 bytes = 256 bits + return 14; + } + } + + +// utility static methods (from cryptix.util.core ArrayUtil and Hex classes) +//........................................................................... + + /** + * Compares two byte arrays for equality. + * + * @return true if the arrays have identical contents + */ + private static final boolean areEqual (byte[] a, byte[] b) { + int aLength = a.length; + if (aLength != b.length) + return false; + for (int i = 0; i < aLength; i++) + if (a[i] != b[i]) + return false; + return true; + } + + /** + * Returns a string of 2 hexadecimal digits (most significant + * digit first) corresponding to the lowest 8 bits of n. + */ + private static final String byteToString (int n) { + char[] buf = { + _HEX_DIGITS[(n >>> 4) & 0x0F], + _HEX_DIGITS[ n & 0x0F] + }; + return new String(buf); + } + + /** + * Returns a string of 8 hexadecimal digits (most significant + * digit first) corresponding to the integer n, which is + * treated as unsigned. + */ + private static final String intToString (int n) { + char[] buf = new char[8]; + for (int i = 7; i >= 0; i--) { + buf[i] = _HEX_DIGITS[n & 0x0F]; + n >>>= 4; + } + return new String(buf); + } + + /** + * Returns a string of hexadecimal digits from a byte array. Each + * byte is converted to 2 hex symbols. + */ + private static final String toString (byte[] ba) { + int length = ba.length; + char[] buf = new char[length * 2]; + for (int i = 0, j = 0, k; i < length; ) { + k = ba[i++]; + buf[j++] = _HEX_DIGITS[(k >>> 4) & 0x0F]; + buf[j++] = _HEX_DIGITS[ k & 0x0F]; + } + return new String(buf); + } + + /** + * Returns a string of hexadecimal digits from an integer array. Each + * int is converted to 4 hex symbols. + */ + private static final String toString (int[] ia) { + int length = ia.length; + char[] buf = new char[length * 8]; + for (int i = 0, j = 0, k; i < length; i++) { + k = ia[i]; + buf[j++] = _HEX_DIGITS[(k >>> 28) & 0x0F]; + buf[j++] = _HEX_DIGITS[(k >>> 24) & 0x0F]; + buf[j++] = _HEX_DIGITS[(k >>> 20) & 0x0F]; + buf[j++] = _HEX_DIGITS[(k >>> 16) & 0x0F]; + buf[j++] = _HEX_DIGITS[(k >>> 12) & 0x0F]; + buf[j++] = _HEX_DIGITS[(k >>> 8) & 0x0F]; + buf[j++] = _HEX_DIGITS[(k >>> 4) & 0x0F]; + buf[j++] = _HEX_DIGITS[ k & 0x0F]; + } + return new String(buf); + } + + +// main(): use to generate the Intermediate Values KAT +//........................................................................... + + public static void main (String[] args) { + self_test(16); + self_test(24); + self_test(32); + } +} diff --git a/core/java/src/net/i2p/crypto/CryptoConstants.java b/core/java/src/net/i2p/crypto/CryptoConstants.java new file mode 100644 index 000000000..78b2a7d13 --- /dev/null +++ b/core/java/src/net/i2p/crypto/CryptoConstants.java @@ -0,0 +1,64 @@ +package net.i2p.crypto; +/* + * Copyright (c) 2003, TheCrypto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the TheCrypto may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +import java.math.BigInteger; +import net.i2p.util.NativeBigInteger; + +/** + * Primes for ElGamal and DSA from + * http://www.ietf.org/proceedings/03mar/I-D/draft-ietf-ipsec-ike-modp-groups-05.txt + */ +public class CryptoConstants { + public static final BigInteger dsap = new NativeBigInteger( + "9c05b2aa960d9b97b8931963c9cc9e8c3026e9b8ed92fad0a69cc886d5bf8015fcadae31"+ + "a0ad18fab3f01b00a358de237655c4964afaa2b337e96ad316b9fb1cc564b5aec5b69a9f"+ + "f6c3e4548707fef8503d91dd8602e867e6d35d2235c1869ce2479c3b9d5401de04e0727f"+ + "b33d6511285d4cf29538d9e3b6051f5b22cc1c93", 16); + public static final BigInteger dsaq = new NativeBigInteger( + "a5dfc28fef4ca1e286744cd8eed9d29d684046b7", 16); + public static final BigInteger dsag = new NativeBigInteger( + "c1f4d27d40093b429e962d7223824e0bbc47e7c832a39236fc683af84889581075ff9082"+ + "ed32353d4374d7301cda1d23c431f4698599dda02451824ff369752593647cc3ddc197de"+ + "985e43d136cdcfc6bd5409cd2f450821142a5e6f8eb1c3ab5d0484b8129fcf17bce4f7f3"+ + "3321c3cb3dbb14a905e7b2b3e93be4708cbcc82", 16); + public static final BigInteger elgp = new NativeBigInteger( + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"+ + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"+ + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"+ + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"+ + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"+ + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"+ + "83655D23DCA3AD961C62F356208552BB9ED529077096966D"+ + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"+ + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"+ + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"+ + "15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16); + public static final BigInteger elgg = new NativeBigInteger("2"); +} diff --git a/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java b/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java new file mode 100644 index 000000000..8ec981e26 --- /dev/null +++ b/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java @@ -0,0 +1,312 @@ +package net.i2p.crypto; +/* + * 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 net.i2p.data.SessionKey; +import net.i2p.data.ByteArray; +import net.i2p.util.RandomSource; +import net.i2p.util.Log; +import net.i2p.util.NativeBigInteger; +import net.i2p.util.Clock; +import java.math.BigInteger; + +import java.util.ArrayList; +import java.util.List; + +/** + * Generate a new session key through a diffie hellman exchange. This uses the + * constants defined in CryptoConstants, which causes the exchange to create a + * 256 bit session key. + * + * This class precalcs a set of values on its own thread, using those transparently + * when a new instance is created. By default, the minimum threshold for creating + * new values for the pool is 5, and the max pool size is 10. Whenever the pool has + * less than the minimum, it fills it up again to the max. There is a delay after + * each precalculation so that the CPU isn't hosed during startup (defaulting to 1 second). + * These three parameters are controlled by java environmental variables and + * can be adjusted via: + * -Dcrypto.dh.precalc.min=40 -Dcrypto.dh.precalc.max=100 -Dcrypto.dh.precalc.delay=60000 + * + * (delay is milliseconds) + * + * To disable precalculation, set min to 0 + * + * @author jrandom + */ +public class DHSessionKeyBuilder { + private final static Log _log = new Log(DHSessionKeyBuilder.class); + private static int MIN_NUM_BUILDERS = -1; + private static int MAX_NUM_BUILDERS = -1; + private static int CALC_DELAY = -1; + private static volatile List _builders = new ArrayList(50); + private static Thread _precalcThread = null; + private BigInteger _myPrivateValue; + private BigInteger _myPublicValue; + private BigInteger _peerValue; + private SessionKey _sessionKey; + private ByteArray _extraExchangedBytes; // bytes after the session key from the DH exchange + + public final static String PROP_DH_PRECALC_MIN = "crypto.dh.precalc.min"; + public final static String PROP_DH_PRECALC_MAX = "crypto.dh.precalc.max"; + public final static String PROP_DH_PRECALC_DELAY = "crypto.dh.precalc.delay"; + public final static String DEFAULT_DH_PRECALC_MIN = "5"; + public final static String DEFAULT_DH_PRECALC_MAX = "10"; + public final static String DEFAULT_DH_PRECALC_DELAY = "1000"; + + static { + try { + int val = Integer.parseInt(System.getProperty(PROP_DH_PRECALC_MIN, DEFAULT_DH_PRECALC_MIN)); + MIN_NUM_BUILDERS = val; + } catch (Throwable t) { + int val = Integer.parseInt(DEFAULT_DH_PRECALC_MIN); + MIN_NUM_BUILDERS = val; + } + try { + int val = Integer.parseInt(System.getProperty(PROP_DH_PRECALC_MAX, DEFAULT_DH_PRECALC_MAX)); + MAX_NUM_BUILDERS = val; + } catch (Throwable t) { + int val = Integer.parseInt(DEFAULT_DH_PRECALC_MAX); + MAX_NUM_BUILDERS = val; + } + try { + int val = Integer.parseInt(System.getProperty(PROP_DH_PRECALC_DELAY, DEFAULT_DH_PRECALC_DELAY)); + CALC_DELAY = val; + } catch (Throwable t) { + int val = Integer.parseInt(DEFAULT_DH_PRECALC_DELAY); + CALC_DELAY = val; + } + + if (_log.shouldLog(Log.DEBUG)) _log.debug("DH Precalc (minimum: " + MIN_NUM_BUILDERS + " max: " + MAX_NUM_BUILDERS + ", delay: " + CALC_DELAY + ")"); + + _precalcThread = new Thread(new DHSessionKeyBuilderPrecalcRunner(MIN_NUM_BUILDERS, MAX_NUM_BUILDERS)); + _precalcThread.setName("DH Precalc"); + _precalcThread.setDaemon(true); + _precalcThread.setPriority(Thread.MIN_PRIORITY); + _precalcThread.start(); + } + /** + * Construct a new DH key builder + * + */ + public DHSessionKeyBuilder() { + this(false); + DHSessionKeyBuilder builder = null; + synchronized (_builders) { + if (_builders.size() > 0) { + builder = (DHSessionKeyBuilder)_builders.remove(0); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Removing a builder. # left = " + _builders.size()); + } else { + if (_log.shouldLog(Log.WARN)) _log.warn("NO MORE BUILDERS! creating one now"); + } + } + if (builder != null) { + _myPrivateValue = builder._myPrivateValue; + _myPublicValue = builder._myPublicValue; + _peerValue = builder._peerValue; + _sessionKey = builder._sessionKey; + _extraExchangedBytes = builder._extraExchangedBytes; + } else { + _myPrivateValue = null; + _myPublicValue = null; + _peerValue = null; + _sessionKey = null; + _myPublicValue = generateMyValue(); + _extraExchangedBytes = new ByteArray(); + } + } + + public DHSessionKeyBuilder(boolean usePool) { + _myPrivateValue = null; + _myPublicValue = null; + _peerValue = null; + _sessionKey = null; + _extraExchangedBytes = new ByteArray(); + } + + private static final int getSize() { synchronized (_builders) { return _builders.size(); } } + private static final int addBuilder(DHSessionKeyBuilder builder) { + int sz = 0; + synchronized (_builders) { + _builders.add(builder); + sz = _builders.size(); + } + return sz; + } + /** + * Create a new private value for the DH exchange, and return the number to + * be exchanged, leaving the actual private value accessible through getMyPrivateValue() + * + */ + public BigInteger generateMyValue() { + long start = Clock.getInstance().now(); + _myPrivateValue = new NativeBigInteger(2048, RandomSource.getInstance()); + BigInteger myValue = CryptoConstants.elgg.modPow(_myPrivateValue, CryptoConstants.elgp); + long end = Clock.getInstance().now(); + long diff = end - start; + if (diff > 1000) { + if (_log.shouldLog(Log.WARN)) _log.warn("Took more than a second (" + diff + "ms) to generate local DH value"); + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Took " + diff + "ms to generate local DH value"); + } + return myValue; + } + + /** + * Retrieve the private value used by the local participant in the DH exchange + */ + public BigInteger getMyPrivateValue() { return _myPrivateValue; } + /** + * Retrieve the public value used by the local participant in the DH exchange, + * generating it if necessary + */ + public BigInteger getMyPublicValue() { + if (_myPublicValue == null) + _myPublicValue = generateMyValue(); + return _myPublicValue; + } + /** + * Specify the value given by the peer for use in the session key negotiation + * + */ + public void setPeerPublicValue(BigInteger peerVal) { _peerValue = peerVal; } + public BigInteger getPeerPublicValue() { return _peerValue; } + + /** + * Retrieve the session key, calculating it if necessary (and if possible). + * + * @return session key exchanged, or null if the exchange is not complete + */ + public SessionKey getSessionKey() { + if (_sessionKey != null) return _sessionKey; + if (_peerValue != null) { + if (_myPrivateValue == null) generateMyValue(); + _sessionKey = calculateSessionKey(_myPrivateValue, _peerValue); + } else { + System.err.println("Not ready yet.. privateValue and peerValue must be set (" + (_myPrivateValue != null ? "set":"null") + "," + (_peerValue != null ? "set":"null") + ")"); + } + return _sessionKey; + } + + /** + * Retrieve the extra bytes beyond the session key resulting from the DH exchange. + * If there aren't enough bytes (with all of them being consumed by the 32 byte key), + * the SHA256 of the key itself is used. + * + */ + public ByteArray getExtraBytes() { return _extraExchangedBytes; } + + /** + * Calculate a session key based on the private value and the public peer value + * + */ + private final SessionKey calculateSessionKey(BigInteger myPrivateValue, BigInteger publicPeerValue) { + long start = Clock.getInstance().now(); + SessionKey key = new SessionKey(); + BigInteger exchangedKey = publicPeerValue.modPow(myPrivateValue, CryptoConstants.elgp); + byte buf[] = exchangedKey.toByteArray(); + byte val[] = new byte[32]; + if (buf.length < val.length) { + System.arraycopy(buf, 0, val, 0, buf.length); + byte remaining[] = SHA256Generator.getInstance().calculateHash(val).getData(); + _extraExchangedBytes.setData(remaining); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Storing " + remaining.length + " bytes from the DH exchange by SHA256 the session key"); + } else { // (buf.length >= val.length) + System.arraycopy(buf, 0, val, 0, val.length); + byte remaining[] = new byte[buf.length - val.length]; + System.arraycopy(buf, val.length, remaining, 0, remaining.length); + _extraExchangedBytes.setData(remaining); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Storing " + remaining.length + " bytes from the end of the DH exchange"); + } + key.setData(val); + long end = Clock.getInstance().now(); + long diff = end - start; + if (diff > 1000) { + if (_log.shouldLog(Log.WARN)) _log.warn("Generating session key took too long ("+ diff +" ms"); + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Generating session key "+ diff +" ms"); + } + return key; + } + + public static void main(String args[]) { + RandomSource.getInstance().nextBoolean(); // warm it up + try { Thread.sleep(20*1000); } catch (InterruptedException ie) {} + _log.debug("\n\n\n\nBegin test\n"); + long negTime = 0; + for (int i = 0; i < 5; i++) { + long startNeg = Clock.getInstance().now(); + DHSessionKeyBuilder builder1 = new DHSessionKeyBuilder(); + DHSessionKeyBuilder builder2 = new DHSessionKeyBuilder(); + BigInteger pub1 = builder1.getMyPublicValue(); + builder2.setPeerPublicValue(pub1); + BigInteger pub2 = builder2.getMyPublicValue(); + builder1.setPeerPublicValue(pub2); + SessionKey key1 = builder1.getSessionKey(); + SessionKey key2 = builder2.getSessionKey(); + long endNeg = Clock.getInstance().now(); + negTime += endNeg - startNeg; + + if (!key1.equals(key2)) + _log.error("**ERROR: Keys do not match"); + else + _log.debug("**Success: Keys match"); + + byte iv[] = new byte[16]; + RandomSource.getInstance().nextBytes(iv); + String origVal = "1234567890123456"; // 16 bytes max using AESEngine + byte enc[] = AESEngine.getInstance().encrypt(origVal.getBytes(), key1, iv); + byte dec[] = AESEngine.getInstance().decrypt(enc, key2, iv); + String tranVal = new String(dec); + if (origVal.equals(tranVal)) + _log.debug("**Success: D(E(val)) == val"); + else + _log.error("**ERROR: D(E(val)) != val [val=(" + tranVal + "), origVal=(" + origVal + ")"); + } + _log.debug("Negotiation time for 5 runs: " + negTime + " @ " + negTime/5l + "ms each"); + try { Thread.sleep(2000); } catch (InterruptedException ie) {} + } + + private static class DHSessionKeyBuilderPrecalcRunner implements Runnable { + private int _minSize; + private int _maxSize; + private DHSessionKeyBuilderPrecalcRunner(int minSize, int maxSize) { + _minSize = minSize; + _maxSize = maxSize; + } + public void run() { + while (true) { + + int curSize = 0; + long start = Clock.getInstance().now(); + int startSize = getSize(); + curSize = startSize; + while (curSize < _minSize) { + while (curSize < _maxSize) { + curSize = addBuilder(precalc(curSize)); + // for some relief... + try { Thread.sleep(CALC_DELAY); } catch (InterruptedException ie) {} + } + } + long end = Clock.getInstance().now(); + int numCalc = curSize - startSize; + if (numCalc > 0) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Precalced " + numCalc + " to " + curSize + " in " + (end-start-CALC_DELAY*numCalc) + "ms (not counting " + (CALC_DELAY*numCalc) + "ms relief). now sleeping"); + } + try { Thread.sleep(30*1000); } catch (InterruptedException ie) {} + } + } + + private DHSessionKeyBuilder precalc(int i) { + DHSessionKeyBuilder builder = new DHSessionKeyBuilder(false); + builder.getMyPublicValue(); + //_log.debug("Precalc " + i + " complete"); + return builder; + } + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/crypto/DSAEngine.java b/core/java/src/net/i2p/crypto/DSAEngine.java new file mode 100644 index 000000000..1b7ff8bac --- /dev/null +++ b/core/java/src/net/i2p/crypto/DSAEngine.java @@ -0,0 +1,281 @@ +package net.i2p.crypto; +/* + * Copyright (c) 2003, TheCrypto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the TheCrypto may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +import net.i2p.data.Signature; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.data.Hash; +import net.i2p.crypto.CryptoConstants; +import net.i2p.util.NativeBigInteger; +import net.i2p.util.RandomSource; +import net.i2p.util.Clock; + +import net.i2p.util.Log; + +import java.math.BigInteger; + +public class DSAEngine { + private final static Log _log = new Log(DSAEngine.class); + private static DSAEngine _instance = new DSAEngine(); + public static DSAEngine getInstance() { return _instance; } + + public boolean verifySignature(Signature signature, byte signedData[], SigningPublicKey verifyingKey) { + long start = Clock.getInstance().now(); + + byte[] sigbytes = signature.getData(); + byte rbytes[] = new byte[20]; + byte sbytes[] = new byte[20]; + for (int x = 0; x < 40; x++) { + if (x < 20) { + rbytes[x] = sigbytes[x]; + } else { + sbytes[x-20] = sigbytes[x]; + } + } + BigInteger s = new NativeBigInteger(1, sbytes); + BigInteger r = new NativeBigInteger(1, rbytes); + BigInteger y = new NativeBigInteger(1, verifyingKey.getData()); + BigInteger w = s.modInverse(CryptoConstants.dsaq); + BigInteger u1 = ((new NativeBigInteger(1, calculateHash(signedData).getData())).multiply(w)).mod(CryptoConstants.dsaq); + BigInteger u2 = r.multiply(w).mod(CryptoConstants.dsaq); + BigInteger v = ((CryptoConstants.dsag.modPow(u1, CryptoConstants.dsap)).multiply(y.modPow(u2, CryptoConstants.dsap))).mod(CryptoConstants.dsap).mod(CryptoConstants.dsaq); + + boolean ok = v.compareTo(r) == 0; + + long diff = Clock.getInstance().now() - start; + if (diff > 1000) { + if (_log.shouldLog(Log.WARN)) _log.warn("Took too long to verify the signature ("+ diff + "ms)"); + } + + return ok; + } + + public Signature sign(byte data[], SigningPrivateKey signingKey) { + if ( (signingKey == null) || (data == null) || (data.length <= 0) ) return null; + long start = Clock.getInstance().now(); + + Signature sig = new Signature(); + BigInteger k; + + do { + k = new BigInteger(160, RandomSource.getInstance()); + } while (k.compareTo(CryptoConstants.dsaq) != 1); + + BigInteger r = CryptoConstants.dsag.modPow(k, CryptoConstants.dsap).mod(CryptoConstants.dsaq); + BigInteger kinv = k.modInverse(CryptoConstants.dsaq); + Hash h = calculateHash(data); + + if (h == null) return null; + + BigInteger M = new NativeBigInteger(1, h.getData()); + BigInteger x = new NativeBigInteger(1, signingKey.getData()); + BigInteger s = (kinv.multiply(M.add(x.multiply(r)))).mod(CryptoConstants.dsaq); + + byte[] rbytes = r.toByteArray(); + byte[] sbytes = s.toByteArray(); + byte[] out = new byte[40]; + + if (rbytes.length == 20) { + for (int i = 0; i < 20; i++) { + out[i] = rbytes[i]; + } + } else if (rbytes.length == 21) { + for (int i = 0; i < 20; i++) { + out[i] = rbytes[i+1]; + } + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Using short rbytes.length [" + rbytes.length + "]"); + for (int i = 0; i < rbytes.length; i++) + out[i+20-rbytes.length] = rbytes[i]; + } + if (sbytes.length == 20) { + for (int i = 0; i < 20; i++) { + out[i+20] = sbytes[i]; + } + } else if (sbytes.length == 21) { + for (int i = 0; i < 20; i++) { + out[i+20] = sbytes[i+1]; + } + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Using short sbytes.length [" + sbytes.length + "]"); + for (int i = 0; i< sbytes.length; i++) + out[i+20+20-sbytes.length] = sbytes[i]; + } + sig.setData(out); + + long diff = Clock.getInstance().now() - start; + if (diff > 1000) { + if (_log.shouldLog(Log.WARN)) _log.warn("Took too long to sign (" + diff + "ms)"); + } + + return sig; + } + + private int[] H0 = { + 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 + }; + + private Hash calculateHash(byte[]source) { + long length = source.length * 8; + int k = 448 - (int) ((length + 1) % 512); + if (k < 0) { + k += 512; + } + int padbytes = k / 8; + int wordlength = (int) (source.length / 4 + padbytes / 4 + 3); + int[] M0 = new int[wordlength]; + int wordcount = 0; + int x = 0; + for (x = 0; x < (source.length / 4) * 4; x += 4) { + M0[wordcount] = source[ x ] << 24 >>> 24 << 24; + M0[wordcount] |= source[x + 1] << 24 >>> 24 << 16; + M0[wordcount] |= source[x + 2] << 24 >>> 24 << 8; + M0[wordcount] |= source[x + 3] << 24 >>> 24 << 0; + wordcount++; + } + + switch (source.length - (wordcount + 1) * 4 + 4) { + case 0: + M0[wordcount] |= 0x80000000; + break; + case 1: + M0[wordcount] = source[x] << 24 >>> 24 << 24; + M0[wordcount] |= 0x00800000; + break; + case 2: + M0[wordcount] = source[ x ] << 24 >>> 24 << 24; + M0[wordcount] |= source[x + 1] << 24 >>> 24 << 16; + M0[wordcount] |= 0x00008000; + break; + case 3: + M0[wordcount] = source[ x ] << 24 >>> 24 << 24; + M0[wordcount] |= source[x + 1] << 24 >>> 24 << 16; + M0[wordcount] |= source[x + 2] << 24 >>> 24 << 8; + M0[wordcount] |= 0x00000080; + break; + } + M0[wordlength - 2] = (int) (length >>> 32); + M0[wordlength - 1] = (int) (length); + int[] H = new int[5]; + for (x = 0; x < 5; x++) { + H[x] = H0[x]; + } + int blocks = M0.length / 16; + for (int bl = 0; bl < blocks; bl++) { + int a = H[0]; + int b = H[1]; + int c = H[2]; + int d = H[3]; + int e = H[4]; + + int[] W = new int[80]; + + for (x = 0; x < 80; x++) { + if (x < 16) { + W[x] = M0[bl * 16 + x]; + } else { + W[x] = ROTL(1, W[x - 3] ^ W[x - 8] ^ W[x - 14] ^ W[x - 16]); + } + } + + for (x = 0; x < 80; x++) { + int T = add(ROTL(5, a), add(f(x, b, c, d), add(e, add(k(x), W[x])))); + e = d; + d = c; + c = ROTL(30, b); + b = a; + a = T; + } + + H[0] = add(a, H[0]); + H[1] = add(b, H[1]); + H[2] = add(c, H[2]); + H[3] = add(d, H[3]); + H[4] = add(e, H[4]); + } + + byte[] hashbytes = new byte[20]; + for (x = 0; x < 5; x++) { + hashbytes[ x * 4 ] = (byte) (H[x] << 0 >>> 24); + hashbytes[x * 4 + 1] = (byte) (H[x] << 8 >>> 24); + hashbytes[x * 4 + 2] = (byte) (H[x] << 16 >>> 24); + hashbytes[x * 4 + 3] = (byte) (H[x] << 24 >>> 24); + } + Hash hash = new Hash(); + hash.setData(hashbytes); + return hash; + } + + private int k(int t) { + if (t > -1 && t < 20) { + return 0x5a827999; + } else if (t > 19 && t < 40) { + return 0x6ed9eba1; + } else if (t > 39 && t < 60) { + return 0x8f1bbcdc; + } else if (t > 59 && t < 80) { + return 0xca62c1d6; + } + return 0x00000000; + } + + private int f(int t, int x, int y, int z) { + if (t > -1 && t < 20) { + return Ch(x, y, z); + } else if (t > 19 && t < 40) { + return Parity(x, y, z); + } else if (t > 39 && t < 60) { + return Maj(x, y, z); + } else if (t > 59 && t < 80) { + return Parity(x, y, z); + } + return 0x00000000; + } + + private int Ch(int x, int y, int z) { + return (x & y) ^ (~x & z); + } + + private int Parity(int x, int y, int z) { + return x ^ y ^ z; + } + + private int Maj(int x, int y, int z) { + return (x & y) ^ (x & z) ^ (y & z); + } + + private int ROTL(int n, int x) { + return (x << n) | (x >>> 32 - n); + } + + private int add(int x, int y) { + return x + y; + } +} diff --git a/core/java/src/net/i2p/crypto/DummyElGamalEngine.java b/core/java/src/net/i2p/crypto/DummyElGamalEngine.java new file mode 100644 index 000000000..b9445d173 --- /dev/null +++ b/core/java/src/net/i2p/crypto/DummyElGamalEngine.java @@ -0,0 +1,92 @@ +package net.i2p.crypto; +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.util.Log; + +/** + * Fake ElG E and D, useful for when performance isn't being tested + * + * @author jrandom + * @license GPL + */ +public class DummyElGamalEngine extends ElGamalEngine { + private final static Log _log = new Log(DummyElGamalEngine.class); + + public DummyElGamalEngine() { + _log.log(Log.CRIT, "Dummy ElGamal engine in use! NO DATA SECURITY. Danger Will Robinson, Danger!", new Exception("I really hope you know what you're doing")); + } + + /** encrypt the data to the public key + * @return encrypted data + * @param publicKey public key encrypt to + * @param data data to encrypt + */ + public byte[] encrypt(byte data[], PublicKey publicKey) { + if ( (data == null) || (data.length >= 223) ) throw new IllegalArgumentException("Data to encrypt must be < 223 bytes at the moment"); + if (publicKey == null) throw new IllegalArgumentException("Null public key specified"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(256); + try { + baos.write(0xFF); + Hash hash = SHA256Generator.getInstance().calculateHash(data); + hash.writeBytes(baos); + baos.write(data); + baos.flush(); + } catch (Exception e) { + _log.error("Internal error writing to buffer", e); + return null; + } + byte d2[] = baos.toByteArray(); + byte[] out = new byte[514]; + System.arraycopy(d2, 0, out, (d2.length < 257 ? 257 - d2.length : 0), (d2.length > 257 ? 257 : d2.length)); + return out; + } + + /** Decrypt the data + * @param encrypted encrypted data + * @param privateKey private key to decrypt with + * @return unencrypted data + */ + public byte[] decrypt(byte encrypted[], PrivateKey privateKey) { + if ( (encrypted == null) || (encrypted.length > 514) ) throw new IllegalArgumentException("Data to decrypt must be <= 514 bytes at the moment"); + byte val[] = new byte[257]; + System.arraycopy(encrypted, 0, val, 0, val.length); + int i = 0; + for (i = 0; i < val.length; i++) + if (val[i] != (byte)0x00) + break; + ByteArrayInputStream bais = new ByteArrayInputStream(val, i, val.length - i); + Hash hash = new Hash(); + byte rv[] = null; + try { + bais.read(); // skip first byte + hash.readBytes(bais); + rv = new byte[val.length - i - 1 - 32]; + bais.read(rv); + } catch (Exception e) { + _log.error("Internal error reading value", e); + return null; + } + Hash calcHash = SHA256Generator.getInstance().calculateHash(rv); + if (calcHash.equals(hash)) { + _log.debug("Hash matches: " + DataHelper.toString(hash.getData(), hash.getData().length)); + return rv; + } else { + _log.debug("Doesn't match hash [calc=" + calcHash + " sent hash=" + hash + "]\ndata = " + new String(rv), new Exception("Doesn't match")); + return null; + } + } +} diff --git a/core/java/src/net/i2p/crypto/ElGamalAESEngine.java b/core/java/src/net/i2p/crypto/ElGamalAESEngine.java new file mode 100644 index 000000000..d2b7d7174 --- /dev/null +++ b/core/java/src/net/i2p/crypto/ElGamalAESEngine.java @@ -0,0 +1,520 @@ +package net.i2p.crypto; +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.util.Log; +import net.i2p.util.RandomSource; +import net.i2p.util.Clock; +import net.i2p.stat.StatManager; + +/** + * Handles the actual ElGamal+AES encryption and decryption scenarios using the + * supplied keys and data. + */ +public class ElGamalAESEngine { + private final static Log _log = new Log(ElGamalAESEngine.class); + private final static int MIN_ENCRYPTED_SIZE = 80; // smallest possible resulting size + + static { + StatManager.getInstance().createFrequencyStat("crypto.elGamalAES.encryptNewSession", + "how frequently we encrypt to a new ElGamal/AES+SessionTag session?", "Encryption", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l } ); + StatManager.getInstance().createFrequencyStat("crypto.elGamalAES.encryptExistingSession", + "how frequently we encrypt to an existing ElGamal/AES+SessionTag session?", "Encryption", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l } ); + StatManager.getInstance().createFrequencyStat("crypto.elGamalAES.decryptNewSession", + "how frequently we decrypt with a new ElGamal/AES+SessionTag session?", "Encryption", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l } ); + StatManager.getInstance().createFrequencyStat("crypto.elGamalAES.decryptExistingSession", + "how frequently we decrypt with an existing ElGamal/AES+SessionTag session?", "Encryption", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l } ); + StatManager.getInstance().createFrequencyStat("crypto.elGamalAES.decryptFail", + "how frequently we fail to decrypt with ElGamal/AES+SessionTag?", "Encryption", new long[] { 60*60*1000l, 24*60*60*1000l } ); + } + + /** + * Decrypt the message using the given private key. This works according to the + * ElGamal+AES algorithm in the data structure spec. + * + */ + public static byte[] decrypt(byte data[], PrivateKey targetPrivateKey) throws DataFormatException { + if (data == null) { + if (_log.shouldLog(Log.WARN)) _log.warn("Null data being decrypted?"); + return null; + } else if (data.length < MIN_ENCRYPTED_SIZE) { + if (_log.shouldLog(Log.WARN)) _log.warn("Data is less than the minimum size (" + data.length +" < " + MIN_ENCRYPTED_SIZE + ")"); + return null; + } + + byte tag[] = new byte[32]; + System.arraycopy(data, 0, tag, 0, tag.length); + SessionTag st = new SessionTag(tag); + SessionKey key = SessionKeyManager.getInstance().consumeTag(st); + SessionKey foundKey = new SessionKey(); + foundKey.setData(null); + SessionKey usedKey = new SessionKey(); + Set foundTags = new HashSet(); + byte decrypted[] = null; + if (key != null) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Key is known for tag " + st); + usedKey.setData(key.getData()); + decrypted = decryptExistingSession(data, key, targetPrivateKey, foundTags, usedKey, foundKey); + if (decrypted != null) + StatManager.getInstance().updateFrequency("crypto.elGamalAES.decryptExistingSession"); + else + StatManager.getInstance().updateFrequency("crypto.elGamalAES.decryptFailed"); + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Key is NOT known for tag " + st); + decrypted = decryptNewSession(data, targetPrivateKey, foundTags, usedKey, foundKey); + if (decrypted != null) + StatManager.getInstance().updateFrequency("crypto.elGamalAES.decryptNewSession"); + else + StatManager.getInstance().updateFrequency("crypto.elGamalAES.decryptFailed"); + } + + if ( (key == null) && (decrypted == null) ) { + //_log.debug("Unable to decrypt the data starting with tag [" + st + "] - did the tag expire recently?", new Exception("Decrypt failure")); + } + + if (foundTags.size() > 0) { + if (foundKey.getData() != null) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Found key: " + foundKey); + SessionKeyManager.getInstance().tagsReceived(foundKey, foundTags); + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Used key: " + usedKey); + SessionKeyManager.getInstance().tagsReceived(usedKey, foundTags); + } + } + return decrypted; + } + + /** + * scenario 1: + * Begin with 222 bytes, ElG encrypted, containing: + * - 32 byte SessionKey + * - 32 byte pre-IV for the AES + * - 158 bytes of random padding + * Then encrypt with AES using that session key and the first 16 bytes of the SHA256 of the pre-IV, using + * the decryptAESBlock method & structure. + * + * @param foundTags set which is filled with any sessionTags found during decryption + * @param foundKey session key which may be filled with a new sessionKey found during decryption + * + * @return null if decryption fails + */ + static byte[] decryptNewSession(byte data[], PrivateKey targetPrivateKey, Set foundTags, SessionKey usedKey, SessionKey foundKey) throws DataFormatException { + if (data == null) { + if (_log.shouldLog(Log.WARN)) _log.warn("Data is null, unable to decrypt new session"); + return null; + } else if (data.length < 514) { + if (_log.shouldLog(Log.WARN)) _log.warn("Data length is too small ("+ data.length + ")"); + return null; + } + byte elgEncr[] = new byte[514]; + if (data.length > 514) { + System.arraycopy(data, 0, elgEncr, 0, 514); + } else { + System.arraycopy(data, 0, elgEncr, 514-data.length, data.length); + } + byte elgDecr[] = ElGamalEngine.getInstance().decrypt(elgEncr, targetPrivateKey); + if (elgDecr == null) + return null; + + ByteArrayInputStream bais = new ByteArrayInputStream(elgDecr); + byte preIV[] = null; + + try { + usedKey.readBytes(bais); + preIV = new byte[32]; + int read = bais.read(preIV); + if (read != preIV.length) { + // hmm, this can't really happen... + throw new DataFormatException("Somehow ElGamal broke and 256 bytes is less than 32 bytes..."); + } + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) _log.error("Error decrypting the new session", ioe); + return null; + } + // ignore the next 192 bytes + byte aesEncr[] = new byte[data.length - 514]; + System.arraycopy(data, 514, aesEncr, 0, aesEncr.length); + + //_log.debug("Pre IV for decryptNewSession: " + DataHelper.toString(preIV, 32)); + //_log.debug("SessionKey for decryptNewSession: " + DataHelper.toString(key.getData(), 32)); + Hash ivHash = SHA256Generator.getInstance().calculateHash(preIV); + byte iv[] = new byte[16]; + System.arraycopy(ivHash.getData(), 0, iv, 0, 16); + + byte aesDecr[] = decryptAESBlock(aesEncr, usedKey, iv, null, foundTags, foundKey); + + if (_log.shouldLog(Log.DEBUG)) _log.debug("Decrypt with a NEW session successfull: # tags read = " + foundTags.size(), new Exception("Decrypted by")); + return aesDecr; + } + + /** + * scenario 2: + * The data begins with 32 byte session tag, which also serves as the preIV. + * Then decrypt with AES using that session key and the first 16 bytes of the SHA256 of the pre-IV: + * - 2 byte integer specifying the # of session tags + * - that many 32 byte session tags + * - 4 byte integer specifying data.length + * - SHA256 of data + * - 1 byte flag that, if == 1, is followed by a new SessionKey + * - data + * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 + * + * If anything doesn't match up in decryption, it falls back to decryptNewSession + * + * @param foundTags set which is filled with any sessionTags found during decryption + * @param foundKey session key which may be filled with a new sessionKey found during decryption + * + */ + static byte[] decryptExistingSession(byte data[], SessionKey key, PrivateKey targetPrivateKey, Set foundTags, SessionKey usedKey, SessionKey foundKey) throws DataFormatException { + byte preIV[] = new byte[32]; + System.arraycopy(data, 0, preIV, 0, preIV.length); + byte encr[] = new byte[data.length-32]; + System.arraycopy(data, 32, encr, 0, encr.length); + Hash ivHash = SHA256Generator.getInstance().calculateHash(preIV); + byte iv[] = new byte[16]; + System.arraycopy(ivHash.getData(), 0, iv, 0, 16); + + usedKey.setData(key.getData()); + + //_log.debug("Pre IV for decryptExistingSession: " + DataHelper.toString(preIV, 32)); + //_log.debug("SessionKey for decryptNewSession: " + DataHelper.toString(key.getData(), 32)); + byte decrypted[] = decryptAESBlock(encr, key, iv, preIV, foundTags, foundKey); + if (decrypted == null) { + // it begins with a valid session tag, but thats just a coincidence. + if (_log.shouldLog(Log.DEBUG)) _log.debug("Decrypt with a non session tag, but tags read: " + foundTags.size()); + return decryptNewSession(data, targetPrivateKey, foundTags, usedKey, foundKey); + } else { + // existing session decrypted successfully! + if (_log.shouldLog(Log.DEBUG)) _log.debug("Decrypt with an EXISTING session tag successfull, # tags read: " + foundTags.size(), new Exception("Decrypted by")); + return decrypted; + } + } + + /** + * Decrypt the AES data with the session key and IV. The result should be: + * - 2 byte integer specifying the # of session tags + * - that many 32 byte session tags + * - 4 byte integer specifying data.length + * - SHA256 of data + * - 1 byte flag that, if == 1, is followed by a new SessionKey + * - data + * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 + * + * If anything doesn't match up in decryption, return null. Otherwise, return + * the decrypted data and update the session as necessary. If the sentTag is not null, + * consume it, but if it is null, record the keys, etc as part of a new session. + * + * @param foundTags set which is filled with any sessionTags found during decryption + * @param foundKey session key which may be filled with a new sessionKey found during decryption + */ + static byte[] decryptAESBlock(byte encrypted[], SessionKey key, byte iv[], byte sentTag[], Set foundTags, SessionKey foundKey) throws DataFormatException { + //_log.debug("iv for decryption: " + DataHelper.toString(iv, 16)); + //_log.debug("decrypting AES block. encr.length = " + (encrypted == null? -1 : encrypted.length) + " sentTag: " + DataHelper.toString(sentTag, 32)); + byte decrypted[] = AESEngine.getInstance().decrypt(encrypted, key, iv); + Hash h = SHA256Generator.getInstance().calculateHash(decrypted); + //_log.debug("Hash of entire aes block after decryption: \n" + DataHelper.toString(h.getData(), 32)); + try { + SessionKey newKey = null; + Hash readHash = null; + List tags = new ArrayList(); + + ByteArrayInputStream bais = new ByteArrayInputStream(decrypted); + long numTags = DataHelper.readLong(bais, 2); + //_log.debug("# tags: " + numTags); + if ( (numTags < 0) || (numTags > 65535) ) + throw new Exception("Invalid number of session tags"); + for (int i = 0; i < numTags; i++) { + byte tag[] = new byte[32]; + int read = bais.read(tag); + if (read != 32) + throw new Exception("Invalid session tag - # tags: " + numTags + " curTag #: " + i + " read: " + read); + tags.add(new SessionTag(tag)); + } + long len = DataHelper.readLong(bais, 4); + //_log.debug("len: " + len); + if ( (len < 0) || (len > encrypted.length) ) + throw new Exception("Invalid size of payload"); + byte hashval[] = new byte[32]; + int read = bais.read(hashval); + if (read != hashval.length) + throw new Exception("Invalid size of hash"); + readHash = new Hash(); + readHash.setData(hashval); + byte flag = (byte)bais.read(); + if (flag == 0x01) { + byte rekeyVal[] = new byte[32]; + read = bais.read(rekeyVal); + if (read != rekeyVal.length) + throw new Exception("Invalid size of the rekeyed session key"); + newKey = new SessionKey(); + newKey.setData(rekeyVal); + } + byte unencrData[] = new byte[(int)len]; + read = bais.read(unencrData); + if (read != unencrData.length) + throw new Exception("Invalid size of the data read"); + Hash calcHash = SHA256Generator.getInstance().calculateHash(unencrData); + if (calcHash.equals(readHash)) { + // everything matches. w00t. + foundTags.addAll(tags); + if (newKey != null) + foundKey.setData(newKey.getData()); + return unencrData; + } else { + throw new Exception("Hash does not match"); + } + } catch (Exception e) { + if (_log.shouldLog(Log.WARN)) _log.warn("Unable to decrypt AES block", e); + return null; + } + } + + + /** + * Encrypt the unencrypted data to the target. The total size returned will be + * no less than the paddedSize parameter, but may be more. This method uses the + * ElGamal+AES algorithm in the data structure spec. + * + * @param target public key to which the data should be encrypted. + * @param key session key to use during encryption + * @param tagsForDelivery session tags to be associated with the key (or newKey if specified), or null + * @param currentTag sessionTag to use, or null if it should use ElG + * @param newKey key to be delivered to the target, with which the tagsForDelivery should be associated + * @param paddedSize minimum size in bytes of the body after padding it (if less than the + * body's real size, no bytes are appended but the body is not truncated) + */ + public static byte[] encrypt(byte data[], PublicKey target, SessionKey key, Set tagsForDelivery, SessionTag currentTag, SessionKey newKey, long paddedSize) { + if (currentTag == null) { + if (_log.shouldLog(Log.INFO)) _log.info("Current tag is null, encrypting as new session", new Exception("encrypt new")); + StatManager.getInstance().updateFrequency("crypto.elGamalAES.encryptNewSession"); + return encryptNewSession(data, target, key, tagsForDelivery, newKey, paddedSize); + } else { + if (_log.shouldLog(Log.INFO)) _log.info("Current tag is NOT null, encrypting as existing session", new Exception("encrypt existing")); + StatManager.getInstance().updateFrequency("crypto.elGamalAES.encryptExistingSession"); + return encryptExistingSession(data, target, key, tagsForDelivery, currentTag, newKey, paddedSize); + } + } + + /** + * Encrypt the data to the target using the given key and deliver the specified tags + */ + public static byte[] encrypt(byte data[], PublicKey target, SessionKey key, Set tagsForDelivery, SessionTag currentTag, long paddedSize) { + return encrypt(data, target, key, tagsForDelivery, currentTag, null, paddedSize); + } + + /** + * Encrypt the data to the target using the given key and deliver the specified tags + */ + public static byte[] encrypt(byte data[], PublicKey target, SessionKey key, Set tagsForDelivery, long paddedSize) { + return encrypt(data, target, key, tagsForDelivery, null, null, paddedSize); + } + + /** + * Encrypt the data to the target using the given key delivering no tags + */ + public static byte[] encrypt(byte data[], PublicKey target, SessionKey key, long paddedSize) { + return encrypt(data, target, key, null, null, null, paddedSize); + } + + /** + * scenario 1: + * Begin with 222 bytes, ElG encrypted, containing: + * - 32 byte SessionKey + * - 32 byte pre-IV for the AES + * - 158 bytes of random padding + * Then encrypt with AES using that session key and the first 16 bytes of the SHA256 of the pre-IV: + * - 2 byte integer specifying the # of session tags + * - that many 32 byte session tags + * - 4 byte integer specifying data.length + * - SHA256 of data + * - 1 byte flag that, if == 1, is followed by a new SessionKey + * - data + * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 + * + */ + static byte[] encryptNewSession(byte data[], PublicKey target, SessionKey key, Set tagsForDelivery, SessionKey newKey, long paddedSize) { + //_log.debug("Encrypting to a NEW session"); + try { + ByteArrayOutputStream elgSrc = new ByteArrayOutputStream(64); + key.writeBytes(elgSrc); + byte preIV[] = new byte[32]; + RandomSource.getInstance().nextBytes(preIV); + elgSrc.write(preIV); + byte rnd[] = new byte[158]; + RandomSource.getInstance().nextBytes(rnd); + elgSrc.write(rnd); + elgSrc.flush(); + + //_log.debug("Pre IV for encryptNewSession: " + DataHelper.toString(preIV, 32)); + //_log.debug("SessionKey for encryptNewSession: " + DataHelper.toString(key.getData(), 32)); + long before = Clock.getInstance().now(); + byte elgEncr[] = ElGamalEngine.getInstance().encrypt(elgSrc.toByteArray(), target); + long after = Clock.getInstance().now(); + if (_log.shouldLog(Log.INFO)) _log.info("elgEngine.encrypt of the session key took " + (after-before) + "ms"); + if (elgEncr.length < 514) { + byte elg[] = new byte[514]; + int diff = elg.length - elgEncr.length; + if (_log.shouldLog(Log.DEBUG)) _log.debug("Difference in size: " + diff); + System.arraycopy(elgEncr, 0, elg, diff, elgEncr.length); + elgEncr = elg; + } + //_log.debug("ElGamal encrypted length: " + elgEncr.length + " elGamal source length: " + elgSrc.toByteArray().length); + + Hash ivHash = SHA256Generator.getInstance().calculateHash(preIV); + byte iv[] = new byte[16]; + System.arraycopy(ivHash.getData(), 0, iv, 0, 16); + byte aesEncr[] = encryptAESBlock(data, key, iv, tagsForDelivery, newKey, paddedSize); + //_log.debug("AES encrypted length: " + aesEncr.length); + + byte rv[] = new byte[elgEncr.length + aesEncr.length]; + System.arraycopy(elgEncr, 0, rv, 0, elgEncr.length); + System.arraycopy(aesEncr, 0, rv, elgEncr.length, aesEncr.length); + //_log.debug("Return length: " + rv.length); + long finish = Clock.getInstance().now(); + if (_log.shouldLog(Log.DEBUG)) _log.debug("after the elgEngine.encrypt took a total of " + (finish-after) +"ms"); + return rv; + } catch (IOException ioe) { + _log.error("Error encrypting the new session", ioe); + return null; + } catch (DataFormatException dfe) { + _log.error("Error writing out the bytes for the new session", dfe); + return null; + } + } + + /** + * scenario 2: + * Begin with 32 byte session tag, which also serves as the preIV. + * Then encrypt with AES using that session key and the first 16 bytes of the SHA256 of the pre-IV: + * - 2 byte integer specifying the # of session tags + * - that many 32 byte session tags + * - 4 byte integer specifying data.length + * - SHA256 of data + * - 1 byte flag that, if == 1, is followed by a new SessionKey + * - data + * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 + * + */ + static byte[] encryptExistingSession(byte data[], PublicKey target, SessionKey key, Set tagsForDelivery, SessionTag currentTag, SessionKey newKey, long paddedSize) { + //_log.debug("Encrypting to an EXISTING session"); + byte rawTag[] = currentTag.getData(); + + //_log.debug("Pre IV for encryptExistingSession (aka tag): " + currentTag.toString()); + //_log.debug("SessionKey for encryptNewSession: " + DataHelper.toString(key.getData(), 32)); + Hash ivHash = SHA256Generator.getInstance().calculateHash(rawTag); + byte iv[] = new byte[16]; + System.arraycopy(ivHash.getData(), 0, iv, 0, 16); + + byte aesEncr[] = encryptAESBlock(data, key, iv, tagsForDelivery, newKey, paddedSize); + byte rv[] = new byte[rawTag.length + aesEncr.length]; + System.arraycopy(rawTag, 0, rv, 0, rawTag.length); + System.arraycopy(aesEncr, 0, rv, rawTag.length, aesEncr.length); + return rv; + } + + private final static Set EMPTY_SET = new HashSet(); + + /** + * For both scenarios, this method encrypts the AES area using the given key, iv + * and making sure the resulting data is at least as long as the paddedSize and + * also mod 16 bytes. The contents of the encrypted data is: + * - 2 byte integer specifying the # of session tags + * - that many 32 byte session tags + * - 4 byte integer specifying data.length + * - SHA256 of data + * - 1 byte flag that, if == 1, is followed by a new SessionKey + * - data + * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 + * + */ + final static byte[] encryptAESBlock(byte data[], SessionKey key, byte[] iv, Set tagsForDelivery, SessionKey newKey, long paddedSize) { + //_log.debug("iv for encryption: " + DataHelper.toString(iv, 16)); + //_log.debug("Encrypting AES"); + try { + ByteArrayOutputStream aesSrc = new ByteArrayOutputStream((int)paddedSize); + if (tagsForDelivery == null) tagsForDelivery = EMPTY_SET; + DataHelper.writeLong(aesSrc, 2, tagsForDelivery.size()); + for (Iterator iter = tagsForDelivery.iterator(); iter.hasNext(); ) { + SessionTag tag = (SessionTag)iter.next(); + aesSrc.write(tag.getData()); + } + //_log.debug("# tags created, registered, and written: " + tags.size()); + DataHelper.writeLong(aesSrc, 4, data.length); + //_log.debug("data length: " + data.length); + Hash hash = SHA256Generator.getInstance().calculateHash(data); + hash.writeBytes(aesSrc); + //_log.debug("hash of data: " + DataHelper.toString(hash.getData(), 32)); + if (newKey == null) { + byte flag = 0x00; // don't rekey + aesSrc.write(flag); + //_log.debug("flag written"); + } else { + byte flag = 0x01; // rekey + aesSrc.write(flag); + aesSrc.write(newKey.getData()); + } + aesSrc.write(data); + int len = aesSrc.toByteArray().length; + //_log.debug("raw data written: " + len); + byte padding[] = getPadding(len, paddedSize); + //_log.debug("padding length: " + padding.length); + aesSrc.write(padding); + + byte aesUnencr[] = aesSrc.toByteArray(); + Hash h = SHA256Generator.getInstance().calculateHash(aesUnencr); + //_log.debug("Hash of entire aes block before encryption: (len=" + aesUnencr.length + ")\n" + DataHelper.toString(h.getData(), 32)); + byte aesEncr[] = AESEngine.getInstance().encrypt(aesUnencr, key, iv); + //_log.debug("Encrypted length: " + aesEncr.length); + return aesEncr; + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) _log.error("Error encrypting AES chunk", ioe); + return null; + } catch (DataFormatException dfe) { + if (_log.shouldLog(Log.ERROR)) _log.error("Error formatting the bytes to write the AES chunk", dfe); + return null; + } + } + + + /** + * Return random bytes for padding the data to a mod 16 size so that it is + * at least minPaddedSize + * + */ + final static byte[] getPadding(int curSize, long minPaddedSize) { + int diff = 0; + if (curSize < minPaddedSize) { + diff = (int)minPaddedSize - curSize; + } + + int numPadding = diff; + if (((curSize + diff) % 16) != 0) + numPadding += (16-((curSize + diff) % 16)); + byte rv[] = new byte[numPadding]; + RandomSource.getInstance().nextBytes(rv); + return rv; + } + +} \ No newline at end of file diff --git a/core/java/src/net/i2p/crypto/ElGamalEngine.java b/core/java/src/net/i2p/crypto/ElGamalEngine.java new file mode 100644 index 000000000..5c7839fd0 --- /dev/null +++ b/core/java/src/net/i2p/crypto/ElGamalEngine.java @@ -0,0 +1,258 @@ +package net.i2p.crypto; +/* + * Copyright (c) 2003, TheCrypto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the TheCrypto may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; + +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.util.NativeBigInteger; +import net.i2p.util.RandomSource; +import net.i2p.stat.StatManager; + +/** + * Wrapper for ElGamal encryption/signature schemes. + * + * Does all of Elgamal now for data sizes of 223 bytes and less. The data to be + * encrypted is first prepended with a random nonzero byte, then the 32 bytes + * making up the SHA256 of the data, then the data itself. The random byte and + * the SHA256 hash is stripped on decrypt so the original data is returned. + * + * @author thecrypto, jrandom + */ + +public class ElGamalEngine { + private final static Log _log = new Log(ElGamalEngine.class); + private static ElGamalEngine _engine; + static { + if ("off".equals(System.getProperty("i2p.encryption", "on"))) + _engine = new DummyElGamalEngine(); + else + _engine = new ElGamalEngine(); + + StatManager.getInstance().createRateStat("crypto.elGamal.encrypt", + "how long does it take to do a full ElGamal encryption", "Encryption", new long[] { 60*1000, 60*60*1000, 24*60*60*1000 } ); + StatManager.getInstance().createRateStat("crypto.elGamal.decrypt", + "how long does it take to do a full ElGamal decryption", "Encryption", new long[] { 60*1000, 60*60*1000, 24*60*60*1000 } ); + } + public static ElGamalEngine getInstance() { return _engine; } + private final static BigInteger _two = new NativeBigInteger(1, new byte[] { 0x02 } ); + + private BigInteger[] getNextYK() { return YKGenerator.getNextYK(); } + + /** encrypt the data to the public key + * @return encrypted data + * @param publicKey public key encrypt to + * @param data data to encrypt + */ + public byte[] encrypt(byte data[], PublicKey publicKey) { + if ( (data == null) || (data.length >= 223) ) throw new IllegalArgumentException("Data to encrypt must be < 223 bytes at the moment"); + if (publicKey == null) throw new IllegalArgumentException("Null public key specified"); + + long start = Clock.getInstance().now(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(256); + try { + baos.write(0xFF); + Hash hash = SHA256Generator.getInstance().calculateHash(data); + hash.writeBytes(baos); + baos.write(data); + baos.flush(); + } catch (Exception e) { + if (_log.shouldLog(Log.ERROR)) _log.error("Internal error writing to buffer", e); + return null; + } + + byte d2[] = baos.toByteArray(); + long t0 = Clock.getInstance().now(); + BigInteger m = new NativeBigInteger(1, d2); + long t1 = Clock.getInstance().now(); + if (m.compareTo(CryptoConstants.elgp) >= 0) + throw new IllegalArgumentException("ARGH. Data cannot be larger than the ElGamal prime. FIXME"); + long t2 = Clock.getInstance().now(); + BigInteger aalpha = new NativeBigInteger(1, publicKey.getData()); + long t3 = Clock.getInstance().now(); + BigInteger yk[] = getNextYK(); + BigInteger k = yk[1]; + BigInteger y = yk[0]; + + long t7 = Clock.getInstance().now(); + BigInteger d = aalpha.modPow(k, CryptoConstants.elgp); + long t8 = Clock.getInstance().now(); + d = d.multiply(m); + long t9 = Clock.getInstance().now(); + d = d.mod(CryptoConstants.elgp); + long t10 = Clock.getInstance().now(); + + byte[] ybytes = y.toByteArray(); + byte[] dbytes = d.toByteArray(); + byte[] out = new byte[514]; + System.arraycopy(ybytes, 0, out, (ybytes.length < 257 ? 257 - ybytes.length : 0), (ybytes.length > 257 ? 257 : ybytes.length)); + System.arraycopy(dbytes, 0, out, (dbytes.length < 257 ? 514 - dbytes.length : 257), (dbytes.length > 257 ? 257 : dbytes.length)); + StringBuffer buf = new StringBuffer(1024); + buf.append("Timing\n"); + buf.append("0-1: ").append(t1-t0).append('\n'); + buf.append("1-2: ").append(t2-t1).append('\n'); + buf.append("2-3: ").append(t3-t2).append('\n'); + //buf.append("3-4: ").append(t4-t3).append('\n'); + //buf.append("4-5: ").append(t5-t4).append('\n'); + //buf.append("5-6: ").append(t6-t5).append('\n'); + //buf.append("6-7: ").append(t7-t6).append('\n'); + buf.append("7-8: ").append(t8-t7).append('\n'); + buf.append("8-9: ").append(t9-t8).append('\n'); + buf.append("9-10: ").append(t10-t9).append('\n'); + //_log.debug(buf.toString()); + long end = Clock.getInstance().now(); + + long diff = end - start; + if (diff > 1000) { + if (_log.shouldLog(Log.WARN)) _log.warn("Took too long to encrypt ElGamal block (" + diff + "ms)"); + } + + StatManager.getInstance().addRateData("crypto.elGamal.encrypt", diff, diff); + return out; + } + + /** Decrypt the data + * @param encrypted encrypted data + * @param privateKey private key to decrypt with + * @return unencrypted data + */ + public byte[] decrypt(byte encrypted[], PrivateKey privateKey) { + if ( (encrypted == null) || (encrypted.length > 514) ) throw new IllegalArgumentException("Data to decrypt must be <= 514 bytes at the moment"); + long start = Clock.getInstance().now(); + + byte[] ybytes = new byte[257]; + byte[] dbytes = new byte[257]; + System.arraycopy(encrypted, 0, ybytes, 0, 257); + System.arraycopy(encrypted, 257, dbytes, 0, 257); + BigInteger y = new NativeBigInteger(1, ybytes); + BigInteger d = new NativeBigInteger(1, dbytes); + BigInteger a = new NativeBigInteger(1, privateKey.getData()); + BigInteger y1p = CryptoConstants.elgp.subtract(BigInteger.ONE).subtract(a); + BigInteger ya = y.modPow(y1p, CryptoConstants.elgp); + BigInteger m = ya.multiply(d); m = m.mod(CryptoConstants.elgp); + byte val[] = m.toByteArray(); + int i = 0; + for (i = 0; i < val.length; i++) + if (val[i] != (byte)0x00) + break; + + ByteArrayInputStream bais = new ByteArrayInputStream(val, i, val.length - i); + Hash hash = new Hash(); + byte rv[] = null; + try { + bais.read(); // skip first byte + hash.readBytes(bais); + rv = new byte[val.length - i - 1 - 32]; + bais.read(rv); + } catch (Exception e) { + if (_log.shouldLog(Log.ERROR)) _log.error("Internal error reading value", e); + return null; + } + + + Hash calcHash = SHA256Generator.getInstance().calculateHash(rv); + boolean ok = calcHash.equals(hash); + + long end = Clock.getInstance().now(); + + long diff = end - start; + if (diff > 1000) { + if (_log.shouldLog(Log.WARN)) _log.warn("Took too long to decrypt and verify ElGamal block (" + diff + "ms)"); + } + + StatManager.getInstance().addRateData("crypto.elGamal.decrypt", diff, diff); + + if (ok) { + //_log.debug("Hash matches: " + DataHelper.toString(hash.getData(), hash.getData().length)); + return rv; + } else { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Doesn't match hash [calc=" + calcHash + " sent hash=" + hash + "]\ndata = " + Base64.encode(rv), new Exception("Doesn't match")); + return null; + } + } + + public static void main(String args[]) { + long eTime = 0; + long dTime = 0; + long gTime = 0; + int numRuns = 100; + if (args.length > 0) + try { + numRuns = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) {} + + try { Thread.sleep(30*1000); } catch (InterruptedException ie) {} + + RandomSource.getInstance().nextBoolean(); + + System.out.println("Running " + numRuns + " times"); + + for (int i = 0; i < numRuns; i++) { + long startG = Clock.getInstance().now(); + Object pair[] = KeyGenerator.getInstance().generatePKIKeypair(); + long endG = Clock.getInstance().now(); + + PublicKey pubkey = (PublicKey)pair[0]; + PrivateKey privkey = (PrivateKey)pair[1]; + byte buf[] = new byte[128]; + RandomSource.getInstance().nextBytes(buf); + long startE = Clock.getInstance().now(); + byte encr[] = ElGamalEngine.getInstance().encrypt(buf, pubkey); + long endE = Clock.getInstance().now(); + byte decr[] = ElGamalEngine.getInstance().decrypt(encr, privkey); + long endD = Clock.getInstance().now(); + eTime += endE - startE; + dTime += endD - endE; + gTime += endG - startG; + + if (!DataHelper.eq(decr, buf)) { + System.out.println("PublicKey : " + DataHelper.toString(pubkey.getData(), pubkey.getData().length)); + System.out.println("PrivateKey : " + DataHelper.toString(privkey.getData(), privkey.getData().length)); + System.out.println("orig : " + DataHelper.toString(buf, buf.length)); + System.out.println("d(e(orig) : " + DataHelper.toString(decr, decr.length)); + System.out.println("orig.len : " + buf.length); + System.out.println("d(e(orig).len : " + decr.length); + System.out.println("Not equal!"); + System.exit(0); + } else { + System.out.println("*Run " +i+" is successful, with encr.length = " + encr.length + " [E: " + (endE-startE) + " D: " + (endD-endE) + " G: " + (endG - startG) + "]\n"); + } + } + System.out.println("\n\nAll "+numRuns+" tests successful, average encryption time: " + (eTime/numRuns) + " average decryption time: " + (dTime / numRuns) + " average key generation time: " + (gTime / numRuns)); + } +} diff --git a/core/java/src/net/i2p/crypto/HMACSHA256Generator.java b/core/java/src/net/i2p/crypto/HMACSHA256Generator.java new file mode 100644 index 000000000..99d908feb --- /dev/null +++ b/core/java/src/net/i2p/crypto/HMACSHA256Generator.java @@ -0,0 +1,33 @@ +package net.i2p.crypto; + +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; +import net.i2p.data.DataHelper; + +/** + * Calculate the HMAC-SHA256 of a key+message. Currently FAKE - returns a stupid + * kludgy hash: H(H(key) XOR H(data)). Fix me! + * + */ +public abstract class HMACSHA256Generator { + private static HMACSHA256Generator _generator = new DummyHMACSHA256Generator(); + public static HMACSHA256Generator getInstance() { return _generator; } + + public abstract Hash calculate(SessionKey key, byte data[]); +} + +/** + * jrandom smells. + * + */ +class DummyHMACSHA256Generator extends HMACSHA256Generator { + public Hash calculate(SessionKey key, byte data[]) { + if ( (key == null) || (key.getData() == null) || (data == null) ) + throw new NullPointerException("Null arguments for HMAC"); + + Hash hkey = SHA256Generator.getInstance().calculateHash(key.getData()); + Hash hdata = SHA256Generator.getInstance().calculateHash(data); + return SHA256Generator.getInstance().calculateHash(DataHelper.xor(hkey.getData(), hdata.getData())); + } +} + \ No newline at end of file diff --git a/core/java/src/net/i2p/crypto/KeyGenerator.java b/core/java/src/net/i2p/crypto/KeyGenerator.java new file mode 100644 index 000000000..ea59b28b6 --- /dev/null +++ b/core/java/src/net/i2p/crypto/KeyGenerator.java @@ -0,0 +1,158 @@ +package net.i2p.crypto; +/* + * 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.math.BigInteger; + +import net.i2p.data.DataHelper; +import net.i2p.data.PrivateKey; +import net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.Signature; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.util.NativeBigInteger; +import net.i2p.util.RandomSource; + +/** Define a way of generating asymetrical key pairs as well as symetrical keys + * @author jrandom + */ +public class KeyGenerator { + private final static Log _log = new Log(KeyGenerator.class); + private static final RandomSource _random = RandomSource.getInstance(); + private static KeyGenerator _generator = new KeyGenerator(); + public static KeyGenerator getInstance() { return _generator; } + + /** Generate a private 256 bit session key + * @return session key + */ + public SessionKey generateSessionKey() { + // 256bit random # as a session key + SessionKey key = new SessionKey(); + byte data[] = new byte[SessionKey.KEYSIZE_BYTES]; + _random.nextBytes(data); + key.setData(data); + return key; + } + + /** Generate a pair of keys, where index 0 is a PublicKey, and + * index 1 is a PrivateKey + * @return pair of keys + */ + public Object[] generatePKIKeypair() { + BigInteger a = new NativeBigInteger(2048, _random); + BigInteger aalpha = CryptoConstants.elgg.modPow(a, CryptoConstants.elgp); + + Object[] keys = new Object[2]; + keys[0] = new PublicKey(); + keys[1] = new PrivateKey(); + byte[] k0 = aalpha.toByteArray(); + byte[] k1 = a.toByteArray(); + + // bigInteger.toByteArray returns SIGNED integers, but since they'return positive, + // signed two's complement is the same as unsigned + + ((PublicKey)keys[0]).setData(padBuffer(k0, PublicKey.KEYSIZE_BYTES)); + ((PrivateKey)keys[1]).setData(padBuffer(k1, PrivateKey.KEYSIZE_BYTES)); + + return keys; + } + + /** Generate a pair of DSA keys, where index 0 is a SigningPublicKey, and + * index 1 is a SigningPrivateKey + * @return pair of keys + */ + public Object[] generateSigningKeypair() { + Object[] keys = new Object[2]; + BigInteger x = null; + + // make sure the random key is less than the DSA q + do { + x = new NativeBigInteger(160, _random); + } while (x.compareTo(CryptoConstants.dsaq) >= 0); + + BigInteger y = CryptoConstants.dsag.modPow(x, CryptoConstants.dsap); + keys[0] = new SigningPublicKey(); + keys[1] = new SigningPrivateKey(); + byte k0[] = padBuffer(y.toByteArray(), SigningPublicKey.KEYSIZE_BYTES); + byte k1[] = padBuffer(x.toByteArray(), SigningPrivateKey.KEYSIZE_BYTES); + + ((SigningPublicKey)keys[0]).setData(k0); + ((SigningPrivateKey)keys[1]).setData(k1); + return keys; + } + + + /** + * Pad the buffer w/ leading 0s or trim off leading bits so the result is the + * given length. + */ + private final static byte[] padBuffer(byte src[], int length) { + byte buf[] = new byte[length]; + + if (src.length > buf.length) // extra bits, chop leading bits + System.arraycopy(src, src.length - buf.length, buf, 0, buf.length); + else if (src.length < buf.length) // short bits, padd w/ 0s + System.arraycopy(src, 0, buf, buf.length - src.length, src.length); + else // eq + System.arraycopy(src, 0, buf, 0, buf.length); + + return buf; + } + + public static void main(String args[]) { + Log log = new Log("keygenTest"); + RandomSource.getInstance().nextBoolean(); + byte src[] = new byte[200]; + RandomSource.getInstance().nextBytes(src); + + long time = 0; + for (int i = 0; i < 10; i++) { + long start = Clock.getInstance().now(); + Object keys[] = KeyGenerator.getInstance().generatePKIKeypair(); + long end = Clock.getInstance().now(); + byte ctext[] = ElGamalEngine.getInstance().encrypt(src, (PublicKey)keys[0]); + byte ptext[] = ElGamalEngine.getInstance().decrypt(ctext, (PrivateKey)keys[1]); + time += end - start; + if (DataHelper.eq(ptext, src)) + log.debug("D(E(data)) == data"); + else + log.error("D(E(data)) != data!!!!!!"); + } + log.info("Keygen 10 times: " + time + "ms"); + + Object obj[] = KeyGenerator.getInstance().generateSigningKeypair(); + SigningPublicKey fake = (SigningPublicKey)obj[0]; + time = 0; + for (int i = 0; i < 10; i++) { + long start = Clock.getInstance().now(); + Object keys[] = KeyGenerator.getInstance().generateSigningKeypair(); + long end = Clock.getInstance().now(); + Signature sig = DSAEngine.getInstance().sign(src, (SigningPrivateKey)keys[1]); + boolean ok = DSAEngine.getInstance().verifySignature(sig, src, (SigningPublicKey)keys[0]); + boolean fakeOk = DSAEngine.getInstance().verifySignature(sig, src, fake); + time += end - start; + log.debug("V(S(data)) == " + ok + " fake verify correctly failed? " + (fakeOk == false)); + } + log.info("Signing Keygen 10 times: " + time + "ms"); + + time = 0; + for (int i = 0; i < 1000; i++) { + long start = Clock.getInstance().now(); + KeyGenerator.getInstance().generateSessionKey(); + long end = Clock.getInstance().now(); + time += end - start; + } + log.info("Session keygen 1000 times: " + time + "ms"); + + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + } +} diff --git a/core/java/src/net/i2p/crypto/PersistentSessionKeyManager.java b/core/java/src/net/i2p/crypto/PersistentSessionKeyManager.java new file mode 100644 index 000000000..bdeba3e20 --- /dev/null +++ b/core/java/src/net/i2p/crypto/PersistentSessionKeyManager.java @@ -0,0 +1,164 @@ +package net.i2p.crypto; +/* + * 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.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.util.Log; + +/** + * Expose the functionality to allow people to write out and read in the + * session key and session tag information via streams. This implementation + * does not write anywhere except where its told. + * + */ +public class PersistentSessionKeyManager extends TransientSessionKeyManager { + private final static Log _log = new Log(PersistentSessionKeyManager.class); + + private Object _yk = YKGenerator.class; + + /** + * Write the session key data to the given stream + * + */ + public void saveState(OutputStream out) throws IOException, DataFormatException { + Set tagSets = getInboundTagSets(); + Set sessions = getOutboundSessions(); + _log.info("Saving state with " + tagSets.size() + " inbound tagSets and " + sessions.size() + " outbound sessions"); + + DataHelper.writeLong(out, 4, tagSets.size()); + for (Iterator iter = tagSets.iterator(); iter.hasNext(); ) { + TagSet ts = (TagSet)iter.next(); + writeTagSet(out, ts); + } + DataHelper.writeLong(out, 4, sessions.size()); + for (Iterator iter = sessions.iterator(); iter.hasNext(); ) { + OutboundSession sess = (OutboundSession)iter.next(); + writeOutboundSession(out, sess); + } + } + + + /** + * Load the session key data from the given stream + * + */ + public void loadState(InputStream in) throws IOException, DataFormatException { + int inboundSets = (int)DataHelper.readLong(in, 4); + Set tagSets = new HashSet(inboundSets); + for (int i = 0; i < inboundSets; i++) { + TagSet ts = readTagSet(in); + tagSets.add(ts); + } + int outboundSessions = (int)DataHelper.readLong(in, 4); + Set sessions = new HashSet(outboundSessions); + for (int i = 0; i < outboundSessions; i++) { + OutboundSession sess = readOutboundSession(in); + sessions.add(sess); + } + + _log.info("Loading state with " + tagSets.size() + " inbound tagSets and " + sessions.size() + " outbound sessions"); + setData(tagSets, sessions); + } + + private void writeOutboundSession(OutputStream out, OutboundSession sess) throws IOException, DataFormatException { + sess.getTarget().writeBytes(out); + sess.getCurrentKey().writeBytes(out); + DataHelper.writeDate(out, new Date(sess.getEstablishedDate())); + DataHelper.writeDate(out, new Date(sess.getLastUsedDate())); + List sets = sess.getTagSets(); + DataHelper.writeLong(out, 2, sets.size()); + for (Iterator iter = sets.iterator(); iter.hasNext(); ) { + TagSet set = (TagSet)iter.next(); + writeTagSet(out, set); + } + } + + private void writeTagSet(OutputStream out, TagSet ts) throws IOException, DataFormatException { + ts.getAssociatedKey().writeBytes(out); + DataHelper.writeDate(out, new Date(ts.getDate())); + DataHelper.writeLong(out, 2, ts.getTags().size()); + for (Iterator iter = ts.getTags().iterator(); iter.hasNext(); ) { + SessionTag tag = (SessionTag)iter.next(); + out.write(tag.getData()); + } + } + + private OutboundSession readOutboundSession(InputStream in) throws IOException, DataFormatException { + PublicKey key = new PublicKey(); + key.readBytes(in); + SessionKey skey = new SessionKey(); + skey.readBytes(in); + Date established = DataHelper.readDate(in); + Date lastUsed = DataHelper.readDate(in); + int tagSets = (int)DataHelper.readLong(in, 2); + ArrayList sets = new ArrayList(tagSets); + for (int i = 0; i < tagSets; i++) { + TagSet ts = readTagSet(in); + sets.add(ts); + } + + return new OutboundSession(key, skey, established.getTime(), lastUsed.getTime(), sets); + } + + private TagSet readTagSet(InputStream in) throws IOException, DataFormatException { + SessionKey key = new SessionKey(); + key.readBytes(in); + Date date = DataHelper.readDate(in); + int numTags = (int)DataHelper.readLong(in, 2); + Set tags = new HashSet(numTags); + for (int i = 0; i < numTags; i++) { + SessionTag tag = new SessionTag(); + byte val[] = new byte[SessionTag.BYTE_LENGTH]; + int read = DataHelper.read(in, val); + if (read != SessionTag.BYTE_LENGTH) + throw new IOException("Unable to fully read a session tag [" + read + " not " + SessionTag.BYTE_LENGTH + ")"); + tag.setData(val); + tags.add(tag); + } + TagSet ts = new TagSet(tags, key); + ts.setDate(date.getTime()); + return ts; + } + + public static void main(String args[]) { + PersistentSessionKeyManager mgr = new PersistentSessionKeyManager(); + try { + mgr.loadState(new FileInputStream("sessionKeys.dat")); + String state = mgr.renderStatusHTML(); + FileOutputStream fos = new FileOutputStream("sessionKeysBeforeExpire.html"); + fos.write(state.getBytes()); + fos.close(); + int expired = mgr.aggressiveExpire(); + _log.error("Expired: " + expired); + String stateAfter = mgr.renderStatusHTML(); + FileOutputStream fos2 = new FileOutputStream("sessionKeysAfterExpire.html"); + fos2.write(stateAfter.getBytes()); + fos2.close(); + } catch (Throwable t) { + _log.error("Error loading/storing sessionKeys", t); + } + try { Thread.sleep(3000); } catch (Throwable t) {} + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/crypto/SHA256Generator.java b/core/java/src/net/i2p/crypto/SHA256Generator.java new file mode 100644 index 000000000..e4966f9b7 --- /dev/null +++ b/core/java/src/net/i2p/crypto/SHA256Generator.java @@ -0,0 +1,173 @@ +package net.i2p.crypto; +/* + * Copyright (c) 2003, TheCrypto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the TheCrypto may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +import net.i2p.data.Hash; + +/** Defines a wrapper for SHA-256 operation + * + * This is done. Takes data of any size and hashes it. + * + * @author thecrypto,jrandom + */ +public class SHA256Generator { + private static SHA256Generator _generator = new SHA256Generator(); + public static SHA256Generator getInstance() { return _generator; } + + static int[] K = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + }; + + static int[] H0 = { + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + }; + + /** Calculate the SHA-256 has of the source + * @param source what to hash + * @return hash of the source + */ + public Hash calculateHash(byte[] source) { + long length = source.length * 8; + int k = 448 - (int)((length+1) % 512); + if (k < 0) { + k += 512; + } + int padbytes = k/8; + int wordlength = (int)(source.length/4 + padbytes/4 + 3); + int[] M0 = new int[wordlength]; + int wordcount = 0; + int x = 0; + for (x = 0; x < (source.length / 4) * 4; x += 4) { + M0[wordcount] = source[ x ] << 24 >>> 24 << 24; + M0[wordcount] |= source[x + 1] << 24 >>> 24 << 16; + M0[wordcount] |= source[x + 2] << 24 >>> 24 << 8; + M0[wordcount] |= source[x + 3] << 24 >>> 24 << 0; + wordcount++; + } + switch (source.length - (wordcount + 1) * 4 + 4) { + case 0: + M0[wordcount] |= 0x80000000; + break; + case 1: + M0[wordcount] = source[ x ] << 24 >>> 24 << 24; + M0[wordcount] |= 0x00800000; + break; + case 2: + M0[wordcount] = source[ x ] << 24 >>> 24 << 24; + M0[wordcount] |= source[x + 1] << 24 >>> 24 << 16; + M0[wordcount] |= 0x00008000; + break; + case 3: + M0[wordcount] = source[ x ] << 24 >>> 24 << 24; + M0[wordcount] |= source[x + 1] << 24 >>> 24 << 16; + M0[wordcount] |= source[x + 2] << 24 >>> 24 << 8; + M0[wordcount] |= 0x00000080; + break; + } + M0[wordlength - 2] = (int)(length >>> 32); + M0[wordlength - 1] = (int)(length); + int[] H = new int[8]; + for (x = 0; x < 8; x++) { + H[x] = H0[x]; + } + int blocks = M0.length/16; + for (int bl = 0; bl < blocks; bl++) { + int a = H[0]; int b = H[1]; int c = H[2]; int d = H[3]; + int e = H[4]; int f = H[5]; int g = H[6]; int h = H[7]; + int[] W = new int[64]; + for (x = 0; x < 64; x++) { + if (x < 16) { + W[x] = M0[bl*16 + x]; + } else { + W[x] = add(o1(W[x-2]), add(W[x-7], add(o0(W[x-15]), W[x-16]))); + } + } + for (x = 0; x < 64; x++) { + int T1 = add(h, add(e1(e), add(Ch(e, f, g), add(K[x], W[x])))); + int T2 = add(e0(a), Maj(a, b, c)); + h = g; g = f; f = e; e = add(d, T1); d = c; c = b; b = a; a = add(T1, T2); + } + H[0] = add(a, H[0]); H[1] = add(b, H[1]); H[2] = add(c, H[2]); H[3] = add(d, H[3]); + H[4] = add(e, H[4]); H[5] = add(f, H[5]); H[6] = add(g, H[6]); H[7] = add(h, H[7]); + } + byte[] hashbytes = new byte[32]; + for (x = 0; x < 8; x++) { + hashbytes[ x * 4 ] = (byte)(H[x] << 0 >>> 24); + hashbytes[x * 4 + 1] = (byte)(H[x] << 8 >>> 24); + hashbytes[x * 4 + 2] = (byte)(H[x] << 16 >>> 24); + hashbytes[x * 4 + 3] = (byte)(H[x] << 24 >>> 24); + } + Hash hash = new Hash(); + hash.setData(hashbytes); + return hash; + } + + private static int Ch(int x, int y, int z) { + return (x & y) ^ (~x & z); + } + + private static int Maj(int x, int y, int z) { + return (x & y) ^ (x & z) ^ (y & z); + } + + private static int ROTR (int x, int n) { + return (x >>> n) | (x << 32 - n); + } + + private static int e0(int x) { + return ROTR(x, 2) ^ ROTR (x, 13) ^ ROTR(x, 22); + } + + private static int e1(int x) { + return ROTR(x, 6) ^ ROTR (x, 11) ^ ROTR(x, 25); + } + + private static int SHR(int x, int n) { + return x >>> n; + } + + private static int o0(int x) { + return ROTR(x, 7) ^ ROTR (x, 18) ^ SHR(x, 3); + } + + private static int o1(int x) { + return ROTR(x, 17) ^ ROTR (x, 19) ^ SHR(x, 10); + } + + private static int add (int x, int y) { + return x+y; + } +} diff --git a/core/java/src/net/i2p/crypto/SessionKeyManager.java b/core/java/src/net/i2p/crypto/SessionKeyManager.java new file mode 100644 index 000000000..ac2aaf756 --- /dev/null +++ b/core/java/src/net/i2p/crypto/SessionKeyManager.java @@ -0,0 +1,111 @@ +package net.i2p.crypto; +/* + * 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 net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; + +import java.util.Set; + +/** + * Manage the session keys and session tags used for encryption and decryption. + * This base implementation simply ignores sessions and acts as if everything is + * unknown (and hence always forces a full ElGamal encryption for each message). + * A more intelligent subclass should manage and persist keys and tags. + * + */ +public class SessionKeyManager { + private final static SessionKeyManager _instance = new PersistentSessionKeyManager(); // new TransientSessionKeyManager(); // SessionKeyManager(); + public final static SessionKeyManager getInstance() { return _instance; } + + /** + * Retrieve the session key currently associated with encryption to the target, + * or null if a new session key should be generated. + * + */ + public SessionKey getCurrentKey(PublicKey target) { return null; } + + /** + * Associate a new session key with the specified target. Metrics to determine + * when to expire that key begin with this call. + * + */ + public void createSession(PublicKey target, SessionKey key) { } + + /** + * Generate a new session key and associate it with the specified target. + * + */ + public SessionKey createSession(PublicKey target) { + SessionKey key = KeyGenerator.getInstance().generateSessionKey(); + createSession(target, key); + return key; + } + + /** + * Retrieve the next available session tag for identifying the use of the given + * key when communicating with the target. If this returns null, no tags are + * available so ElG should be used with the given key (a new sessionKey should + * NOT be used) + * + */ + public SessionTag consumeNextAvailableTag(PublicKey target, SessionKey key) { return null; } + + /** + * Determine (approximately) how many available session tags for the current target + * have been confirmed and are available + * + */ + public int getAvailableTags(PublicKey target, SessionKey key) { return 0; } + + /** + * Determine how long the available tags will be available for before expiring, in + * milliseconds + */ + public long getAvailableTimeLeft(PublicKey target, SessionKey key) { return 0; } + + /** + * Take note of the fact that the given sessionTags associated with the key for + * encryption to the target have definitely been received at the target (aka call this + * method after receiving an ack to a message delivering them) + * + */ + public void tagsDelivered(PublicKey target, SessionKey key, Set sessionTags) { } + + /** + * Mark all of the tags delivered to the target up to this point as invalid, since the peer + * has failed to respond when they should have. This call essentially lets the system recover + * from corrupted tag sets and crashes + * + */ + public void failTags(PublicKey target) {} + + /** + * Accept the given tags and associate them with the given key for decryption + * + */ + public void tagsReceived(SessionKey key, Set sessionTags) { } + + /** + * Determine if we have received a session key associated with the given session tag, + * and if so, discard it (but keep track for frequent dups) and return the decryption + * key it was received with (via tagsReceived(...)). returns null if no session key + * matches + * + */ + public SessionKey consumeTag(SessionTag tag) { return null; } + + /** + * Called when the system is closing down, instructing the session key manager to take + * whatever precautions are necessary (saving state, etc) + * + */ + public void shutdown() {} +} \ No newline at end of file diff --git a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java new file mode 100644 index 000000000..a37aef819 --- /dev/null +++ b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java @@ -0,0 +1,527 @@ +package net.i2p.crypto; +/* + * 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 net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.data.DataHelper; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Date; + +/** + * Implement the session key management, but keep everything in memory (don't write + * to disk). However, this being java, we cannot guarantee that the keys aren't swapped + * out to disk so this should not be considered secure in that sense. + * + */ +class TransientSessionKeyManager extends SessionKeyManager { + private final static Log _log = new Log(TransientSessionKeyManager.class); + private Map _outboundSessions; // PublicKey --> OutboundSession + private Map _inboundTagSets; // SessionTag --> TagSet + + /** + * Let session tags sit around for 10 minutes before expiring them. We can now have such a large + * value since there is the persistent session key manager. This value is for outbound tags - + * inbound tags are managed by SESSION_LIFETIME_MAX_MS + * + */ + public final static long SESSION_TAG_DURATION_MS = 10*60*1000; + /** + * Keep unused inbound session tags around for up to 15 minutes (5 minutes longer than + * session tags are used on the outbound side so that no reasonable network lag + * can cause failed decrypts) + * + */ + public final static long SESSION_LIFETIME_MAX_MS = SESSION_TAG_DURATION_MS + 5*60*1000; + public final static int MAX_INBOUND_SESSION_TAGS = 100*1000; // this will consume at most 3.2M + + public TransientSessionKeyManager() { + super(); + _outboundSessions = new HashMap(64); + _inboundTagSets = new HashMap(1024); + } + + /** TagSet */ + protected Set getInboundTagSets() { + synchronized (_inboundTagSets) { + return new HashSet(_inboundTagSets.values()); + } + } + /** OutboundSession */ + protected Set getOutboundSessions() { + synchronized (_outboundSessions) { + return new HashSet(_outboundSessions.values()); + } + } + protected void setData(Set inboundTagSets, Set outboundSessions) { + _log.info("Loading " + inboundTagSets.size() + " inbound tag sets, and " + outboundSessions.size() + " outbound sessions"); + Map tagSets = new HashMap(inboundTagSets.size()); + for (Iterator iter = inboundTagSets.iterator(); iter.hasNext(); ) { + TagSet ts = (TagSet)iter.next(); + for (Iterator tsIter = ts.getTags().iterator(); tsIter.hasNext(); ) { + SessionTag tag = (SessionTag)tsIter.next(); + tagSets.put(tag, ts); + } + } + synchronized (_inboundTagSets) { + _inboundTagSets.clear(); + _inboundTagSets.putAll(tagSets); + } + Map sessions = new HashMap(outboundSessions.size()); + for (Iterator iter = outboundSessions.iterator(); iter.hasNext(); ) { + OutboundSession sess = (OutboundSession)iter.next(); + sessions.put(sess.getTarget(), sess); + } + synchronized (_outboundSessions) { + _outboundSessions.clear(); + _outboundSessions.putAll(sessions); + } + } + + /** + * Retrieve the session key currently associated with encryption to the target, + * or null if a new session key should be generated. + * + */ + public SessionKey getCurrentKey(PublicKey target) { + OutboundSession sess = getSession(target); + if (sess == null) return null; + long now = Clock.getInstance().now(); + if (sess.getEstablishedDate() < now - SESSION_LIFETIME_MAX_MS) { + _log.info("Expiring old session key established on " + new Date(sess.getEstablishedDate()) + " with target " + target); + return null; + } else { + return sess.getCurrentKey(); + } + } + + /** + * Associate a new session key with the specified target. Metrics to determine + * when to expire that key begin with this call. + * + */ + public void createSession(PublicKey target, SessionKey key) { + OutboundSession sess = new OutboundSession(target); + sess.setCurrentKey(key); + addSession(sess); + } + + /** + * Retrieve the next available session tag for identifying the use of the given + * key when communicating with the target. If this returns null, no tags are + * available so ElG should be used with the given key (a new sessionKey should + * NOT be used) + * + */ + public SessionTag consumeNextAvailableTag(PublicKey target, SessionKey key) { + OutboundSession sess = getSession(target); + if (sess == null) { + _log.debug("No session for " + target); + return null; + } + if (sess.getCurrentKey().equals(key)) { + SessionTag nxt = sess.consumeNext(); + _log.debug("Tag consumed: " + nxt); + return nxt; + } else { + _log.debug("Key does not match existing key, no tag"); + return null; + } + } + + /** + * Determine (approximately) how many available session tags for the current target + * have been confirmed and are available + * + */ + public int getAvailableTags(PublicKey target, SessionKey key) { + OutboundSession sess = getSession(target); + if (sess == null) { + return 0; + } + if (sess.getCurrentKey().equals(key)) { + return sess.availableTags(); + } else { + return 0; + } + } + + + /** + * Determine how long the available tags will be available for before expiring, in + * milliseconds + */ + public long getAvailableTimeLeft(PublicKey target, SessionKey key) { + OutboundSession sess = getSession(target); + if (sess == null) { + return 0; + } + if (sess.getCurrentKey().equals(key)) { + return (sess.getLastExpirationDate() + SESSION_TAG_DURATION_MS) - Clock.getInstance().now(); + } else { + return 0; + } + } + + /** + * Take note of the fact that the given sessionTags associated with the key for + * encryption to the target have definitely been received at the target (aka call this + * method after receiving an ack to a message delivering them) + * + */ + public void tagsDelivered(PublicKey target, SessionKey key, Set sessionTags) { + OutboundSession sess = getSession(target); + if (sess == null) { + createSession(target, key); + sess = getSession(target); + } + sess.setCurrentKey(key); + TagSet set = new TagSet(sessionTags, key); + sess.addTags(set); + _log.debug("Tags delivered to set " + set + " on session " + sess); + } + + /** + * Mark all of the tags delivered to the target up to this point as invalid, since the peer + * has failed to respond when they should have. This call essentially lets the system recover + * from corrupted tag sets and crashes + * + */ + public void failTags(PublicKey target) { + removeSession(target); + } + + + /** + * Accept the given tags and associate them with the given key for decryption + * + */ + public void tagsReceived(SessionKey key, Set sessionTags) { + TagSet tagSet = new TagSet(sessionTags, key); + for (Iterator iter = sessionTags.iterator(); iter.hasNext(); ) { + SessionTag tag = (SessionTag)iter.next(); + _log.debug("Receiving tag " + tag + " for key " + key); + synchronized (_inboundTagSets) { + _inboundTagSets.put(tag, tagSet); + } + } + synchronized (_inboundTagSets) { + // todo: make this limit the tags by sessionKey and actually enforce the limit! + int overage = _inboundTagSets.size() - MAX_INBOUND_SESSION_TAGS; + if (overage > 0) { + _log.error("TOO MANY SESSION TAGS! " + (_inboundTagSets.size())); + } + } + + if (sessionTags.size() <= 0) + _log.debug("Received 0 tags for key " + key); + } + + /** + * Determine if we have received a session key associated with the given session tag, + * and if so, discard it (but keep track for frequent dups) and return the decryption + * key it was received with (via tagsReceived(...)). returns null if no session key + * matches + * + */ + public SessionKey consumeTag(SessionTag tag) { + synchronized (_inboundTagSets) { + TagSet tagSet = (TagSet)_inboundTagSets.remove(tag); + if (tagSet == null) { + _log.debug("Cannot consume tag " + tag + " as it is not known"); + return null; + } else { + tagSet.consume(tag); + } + SessionKey key = tagSet.getAssociatedKey(); + _log.debug("Consuming tag " + tag + " for sessionKey " + key); + return key; + } + } + + private OutboundSession getSession(PublicKey target) { + synchronized (_outboundSessions) { + return (OutboundSession)_outboundSessions.get(target); + } + } + + private void addSession(OutboundSession sess) { + synchronized (_outboundSessions) { + _outboundSessions.put(sess.getTarget(), sess); + } + } + + private void removeSession(PublicKey target) { + if (target == null) return; + synchronized (_outboundSessions) { + _outboundSessions.remove(target); + } + } + + /** + * Aggressively expire inbound tag sets and outbound sessions + * + * @return number of tag sets expired + */ + public int aggressiveExpire() { + int removed = 0; + long now = Clock.getInstance().now(); + Set tagsToDrop = new HashSet(64); + synchronized (_inboundTagSets) { + for (Iterator iter = _inboundTagSets.keySet().iterator(); iter.hasNext(); ) { + SessionTag tag = (SessionTag)iter.next(); + TagSet ts = (TagSet)_inboundTagSets.get(tag); + if (ts.getDate() < now - SESSION_LIFETIME_MAX_MS) { + tagsToDrop.add(tag); + } + } + removed += tagsToDrop.size(); + for (Iterator iter = tagsToDrop.iterator(); iter.hasNext(); ) + _inboundTagSets.remove(iter.next()); + } + //_log.warn("Expiring tags: [" + tagsToDrop + "]"); + + synchronized (_outboundSessions) { + Set sessionsToDrop = new HashSet(64); + for (Iterator iter = _outboundSessions.keySet().iterator(); iter.hasNext(); ) { + PublicKey key = (PublicKey)iter.next(); + OutboundSession sess = (OutboundSession)_outboundSessions.get(key); + removed += sess.expireTags(); + if (sess.getTagSets().size() <= 0) + sessionsToDrop.add(key); + } + for (Iterator iter = sessionsToDrop.iterator(); iter.hasNext(); ) + _outboundSessions.remove(iter.next()); + } + return removed; + } + + public String renderStatusHTML() { + StringBuffer buf = new StringBuffer(1024); + buf.append("

Inbound sessions

"); + buf.append(""); + Set inbound = getInboundTagSets(); + Map inboundSets = new HashMap(inbound.size()); + for (Iterator iter = inbound.iterator(); iter.hasNext(); ) { + TagSet ts = (TagSet)iter.next(); + if (!inboundSets.containsKey(ts.getAssociatedKey())) + inboundSets.put(ts.getAssociatedKey(), new HashSet()); + Set sets = (Set)inboundSets.get(ts.getAssociatedKey()); + sets.add(ts); + } + for (Iterator iter = inboundSets.keySet().iterator(); iter.hasNext(); ) { + SessionKey skey = (SessionKey)iter.next(); + Set sets = (Set)inboundSets.get(skey); + buf.append(""); + buf.append(""); + buf.append(""); + } + buf.append("
Session key: ").append(skey.toBase64()).append("# Sets: ").append(sets.size()).append("
    "); + for (Iterator siter = sets.iterator(); siter.hasNext(); ) { + TagSet ts = (TagSet)siter.next(); + buf.append("
  • Received on: ").append(new Date(ts.getDate())).append(" with ").append(ts.getTags().size()).append(" tags remaining
  • "); + } + buf.append("
"); + + buf.append("

Outbound sessions

"); + + buf.append(""); + Set outbound = getOutboundSessions(); + for (Iterator iter = outbound.iterator(); iter.hasNext(); ) { + OutboundSession sess = (OutboundSession)iter.next(); + buf.append(""); + buf.append(""); + buf.append(""); + } + buf.append("
Target key: ").append(sess.getTarget().toString()).append("
"); + buf.append("Established: ").append(new Date(sess.getEstablishedDate())).append("
"); + buf.append("Last Used: ").append(new Date(sess.getLastUsedDate())).append("
"); + buf.append("# Sets: ").append(sess.getTagSets().size()).append("
Session key: ").append(sess.getCurrentKey().toBase64()).append("
    "); + for (Iterator siter = sess.getTagSets().iterator(); siter.hasNext(); ) { + TagSet ts = (TagSet)siter.next(); + buf.append("
  • Sent on: ").append(new Date(ts.getDate())).append(" with ").append(ts.getTags().size()).append(" tags remaining
  • "); + } + buf.append("
"); + + return buf.toString(); + } + + static class OutboundSession { + private PublicKey _target; + private SessionKey _currentKey; + private long _established; + private long _lastUsed; + private List _tagSets; + + public OutboundSession(PublicKey target) { + this(target, null, Clock.getInstance().now(), Clock.getInstance().now(), new ArrayList()); + } + + OutboundSession(PublicKey target, SessionKey curKey, long established, long lastUsed, List tagSets) { + _target = target; + _currentKey = curKey; + _established = established; + _lastUsed = lastUsed; + _tagSets = tagSets; + } + + /** list of TagSet objects */ + List getTagSets() { + synchronized (_tagSets) { + return new ArrayList(_tagSets); + } + } + + public PublicKey getTarget() { return _target; } + public SessionKey getCurrentKey() { return _currentKey; } + public void setCurrentKey(SessionKey key) { + if (_currentKey != null) { + if (!_currentKey.equals(key)) { + int dropped = 0; + List sets = _tagSets; + _tagSets = new ArrayList(); + for (int i = 0; i < sets.size(); i++) { + TagSet set = (TagSet)sets.get(i); + dropped += set.getTags().size(); + } + _log.info("Rekeyed from " + _currentKey + " to " + key + ": dropping " + dropped + " session tags"); + } + } + _currentKey = key; + + } + public long getEstablishedDate() { return _established; } + public long getLastUsedDate() { return _lastUsed; } + /** + * Expire old tags, returning the number of tag sets removed + */ + public int expireTags() { + long now = Clock.getInstance().now(); + Set toRemove = new HashSet(64); + synchronized (_tagSets) { + for (int i = 0; i < _tagSets.size(); i++) { + TagSet set = (TagSet)_tagSets.get(i); + if (set.getDate() + SESSION_TAG_DURATION_MS <= now) { + toRemove.add(set); + } + } + _tagSets.removeAll(toRemove); + } + return toRemove.size(); + } + public SessionTag consumeNext() { + long now = Clock.getInstance().now(); + synchronized (_tagSets) { + while (_tagSets.size() > 0) { + TagSet set = (TagSet)_tagSets.get(0); + if (set.getDate() + SESSION_TAG_DURATION_MS > now) { + SessionTag tag = set.consumeNext(); + if (tag != null) return tag; + } else { + _log.info("TagSet from " + new Date(set.getDate()) + " expired"); + } + _tagSets.remove(0); + } + } + return null; + } + public int availableTags() { + int tags = 0; + synchronized (_tagSets) { + for (int i = 0; i < _tagSets.size(); i++) { + TagSet set = (TagSet)_tagSets.get(i); + tags += set.getTags().size(); + } + } + return tags; + } + /** + * Get the furthest away tag set expiration date - after which all of the + * tags will have expired + * + */ + public long getLastExpirationDate() { + long last = 0; + synchronized (_tagSets) { + for (Iterator iter = _tagSets.iterator(); iter.hasNext(); ) { + TagSet set = (TagSet)iter.next(); + if (set.getDate() > last) + last = set.getDate(); + } + } + return last + SESSION_TAG_DURATION_MS; + } + public void addTags(TagSet set) { + synchronized (_tagSets) { + _tagSets.add(set); + } + } + } + + static class TagSet { + private Set _sessionTags; + private SessionKey _key; + private long _date; + public TagSet(Set tags, SessionKey key) { + if (key == null) + throw new IllegalArgumentException("Missing key"); + if (tags == null) + throw new IllegalArgumentException("Missing tags"); + _sessionTags = tags; + _key = key; + _date = Clock.getInstance().now(); + } + public long getDate() { return _date; } + void setDate(long when) { _date = when; } + public Set getTags() { return _sessionTags; } + public SessionKey getAssociatedKey() { return _key; } + public boolean contains(SessionTag tag) { return _sessionTags.contains(tag); } + public void consume(SessionTag tag) { + if (contains(tag)) { + _sessionTags.remove(tag); + } + } + public SessionTag consumeNext() { + if (_sessionTags.size() <= 0) { + return null; + } else { + SessionTag first = (SessionTag)_sessionTags.iterator().next(); + _sessionTags.remove(first); + return first; + } + } + public int hashCode() { + long rv = 0; + if (_key != null) + rv = rv*7 + _key.hashCode(); + rv = rv*7 + _date; + if (_sessionTags != null) + rv = rv*7 + DataHelper.hashCode(_sessionTags); + return (int)rv; + } + public boolean equals(Object o) { + if ( (o == null) || !(o instanceof TagSet) ) return false; + TagSet ts = (TagSet)o; + return DataHelper.eq(ts.getAssociatedKey(), getAssociatedKey()) && + DataHelper.eq(ts.getTags(), getTags()) && + ts.getDate() == getDate(); + } + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/crypto/YKGenerator.java b/core/java/src/net/i2p/crypto/YKGenerator.java new file mode 100644 index 000000000..beb160c51 --- /dev/null +++ b/core/java/src/net/i2p/crypto/YKGenerator.java @@ -0,0 +1,193 @@ +package net.i2p.crypto; +/* + * 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.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.util.NativeBigInteger; +import net.i2p.util.RandomSource; + +/** + * Precalculate the Y and K for ElGamal encryption operations. + * + * This class precalcs a set of values on its own thread, using those transparently + * when a new instance is created. By default, the minimum threshold for creating + * new values for the pool is 5, and the max pool size is 10. Whenever the pool has + * less than the minimum, it fills it up again to the max. There is a delay after + * each precalculation so that the CPU isn't hosed during startup (defaulting to 10 seconds). + * These three parameters are controlled by java environmental variables and + * can be adjusted via: + * -Dcrypto.yk.precalc.min=40 -Dcrypto.yk.precalc.max=100 -Dcrypto.yk.precalc.delay=60000 + * + * (delay is milliseconds) + * + * To disable precalculation, set min to 0 + * + * @author jrandom + */ +class YKGenerator { + private final static Log _log = new Log(YKGenerator.class); + private static int MIN_NUM_BUILDERS = -1; + private static int MAX_NUM_BUILDERS = -1; + private static int CALC_DELAY = -1; + private static volatile List _values = new ArrayList(50); // list of BigInteger[] values (y and k) + private static Thread _precalcThread = null; + + public final static String PROP_YK_PRECALC_MIN = "crypto.yk.precalc.min"; + public final static String PROP_YK_PRECALC_MAX = "crypto.yk.precalc.max"; + public final static String PROP_YK_PRECALC_DELAY = "crypto.yk.precalc.delay"; + public final static String DEFAULT_YK_PRECALC_MIN = "10"; + public final static String DEFAULT_YK_PRECALC_MAX = "30"; + public final static String DEFAULT_YK_PRECALC_DELAY = "10000"; + + /** check every 30 seconds whether we have less than the minimum */ + private final static long CHECK_DELAY = 30*1000; + + static { + try { + int val = Integer.parseInt(System.getProperty(PROP_YK_PRECALC_MIN, DEFAULT_YK_PRECALC_MIN)); + MIN_NUM_BUILDERS = val; + } catch (Throwable t) { + int val = Integer.parseInt(DEFAULT_YK_PRECALC_MIN); + MIN_NUM_BUILDERS = val; + } + try { + int val = Integer.parseInt(System.getProperty(PROP_YK_PRECALC_MAX, DEFAULT_YK_PRECALC_MAX)); + MAX_NUM_BUILDERS = val; + } catch (Throwable t) { + int val = Integer.parseInt(DEFAULT_YK_PRECALC_MAX); + MAX_NUM_BUILDERS = val; + } + try { + int val = Integer.parseInt(System.getProperty(PROP_YK_PRECALC_DELAY, DEFAULT_YK_PRECALC_DELAY)); + CALC_DELAY = val; + } catch (Throwable t) { + int val = Integer.parseInt(DEFAULT_YK_PRECALC_DELAY); + CALC_DELAY = val; + } + + if (_log.shouldLog(Log.DEBUG)) _log.debug("ElGamal YK Precalc (minimum: " + MIN_NUM_BUILDERS + " max: " + MAX_NUM_BUILDERS + ", delay: " + CALC_DELAY + ")"); + + _precalcThread = new Thread(new YKPrecalcRunner(MIN_NUM_BUILDERS, MAX_NUM_BUILDERS)); + _precalcThread.setName("YK Precalc"); + _precalcThread.setDaemon(true); + _precalcThread.setPriority(Thread.MIN_PRIORITY); + _precalcThread.start(); + } + + private static final int getSize() { synchronized (_values) { return _values.size(); } } + private static final int addValues(BigInteger yk[]) { + int sz = 0; + synchronized (_values) { + _values.add(yk); + sz = _values.size(); + } + return sz; + } + + public static BigInteger[] getNextYK() { + if (true) { + synchronized (_values) { + if (_values.size() > 0) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Sufficient precalculated YK values - fetch the existing"); + return (BigInteger[])_values.remove(0); + } + } + } + if (_log.shouldLog(Log.INFO)) _log.info("Insufficient precalculated YK values - create a new one"); + return generateYK(); + } + + private final static BigInteger _two = new NativeBigInteger(1, new byte[] { 0x02 } ); + + private static final BigInteger[] generateYK() { + NativeBigInteger k = null; + BigInteger y = null; + long t0 = 0; + long t1 = 0; + while (k == null) { + t0 = Clock.getInstance().now(); + k = new NativeBigInteger(2048, RandomSource.getInstance()); + t1 = Clock.getInstance().now(); + if (BigInteger.ZERO.compareTo(k) == 0) { + k = null; + continue; + } + BigInteger kPlus2 = k.add(_two); + if (kPlus2.compareTo(CryptoConstants.elgp) > 0) + k = null; + } + long t2 = Clock.getInstance().now(); + y = CryptoConstants.elgg.modPow(k, CryptoConstants.elgp); + + BigInteger yk[] = new BigInteger[2]; + yk[0] = y; + yk[1] = k; + + long diff = t2 - t0; + if (diff > 1000) { + if (_log.shouldLog(Log.WARN)) _log.warn("Took too long to generate YK value for ElGamal (" + diff + "ms)"); + } + + return yk; + } + + + public static void main(String args[]) { + RandomSource.getInstance().nextBoolean(); // warm it up + try { Thread.sleep(20*1000); } catch (InterruptedException ie) {} + _log.debug("\n\n\n\nBegin test\n"); + long negTime = 0; + for (int i = 0; i < 5; i++) { + long startNeg = Clock.getInstance().now(); + getNextYK(); + long endNeg = Clock.getInstance().now(); + } + _log.debug("YK fetch time for 5 runs: " + negTime + " @ " + negTime/5l + "ms each"); + try { Thread.sleep(30*1000); } catch (InterruptedException ie) {} + } + + + private static class YKPrecalcRunner implements Runnable { + private int _minSize; + private int _maxSize; + private YKPrecalcRunner(int minSize, int maxSize) { + _minSize = minSize; + _maxSize = maxSize; + } + public void run() { + while (true) { + int curSize = 0; + long start = Clock.getInstance().now(); + int startSize = getSize(); + curSize = startSize; + while (curSize < _minSize) { + while (curSize < _maxSize) { + long begin = Clock.getInstance().now(); + curSize = addValues(generateYK()); + long end = Clock.getInstance().now(); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Precalculated YK value in " + (end-begin) + "ms"); + // for some relief... + try { Thread.sleep(CALC_DELAY); } catch (InterruptedException ie) {} + } + } + long end = Clock.getInstance().now(); + int numCalc = curSize - startSize; + if (numCalc > 0) { + if (_log.shouldLog(Log.DEBUG)) _log.debug("Precalced " + numCalc + " to " + curSize + " in " + (end-start-CALC_DELAY*numCalc) + "ms (not counting " + (CALC_DELAY*numCalc) + "ms relief). now sleeping"); + } + try { Thread.sleep(CHECK_DELAY); } catch (InterruptedException ie) {} + } + } + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/data/Base64.java b/core/java/src/net/i2p/data/Base64.java new file mode 100644 index 000000000..f9d4e2358 --- /dev/null +++ b/core/java/src/net/i2p/data/Base64.java @@ -0,0 +1,642 @@ +package net.i2p.data; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Encodes and decodes to and from Base64 notation. + * + *

+ * Change Log: + *

+ *
    + *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • + *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream + * where last buffer being read, if not completely full, was not returned.
  • + *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • + *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • + *
+ * + *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit http://iharder.net/xmlizable + * periodically to check for updates or to contribute improvements. + *

+ * + * Modified by jrandom for i2p, using safeEncode / safeDecode to create filesystem and URL safe + * base64 values (replacing / with ~, and + with -) + * + * @author Robert Harder + * @author rob@iharder.net + * @version 1.3.4 + */ +public class Base64 +{ + public static String encode(byte[] source) { return safeEncode(source); } + public static byte[] decode(String s) { return safeDecode(s); } + + /** Maximum line length (76) of Base64 output. */ + private final static int MAX_LINE_LENGTH = 76; + + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte)'='; + + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte)'\n'; + + + /** The 64 valid Base64 values. */ + private final static byte[] ALPHABET = + { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' + }; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + **/ + private final static byte[] DECODABET = + { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9,-9,-9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' + 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' + -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 + 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' + 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' + -9,-9,-9,-9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + private final static byte BAD_ENCODING = -9; // Indicates error in encoding + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + + /** Defeats instantiation. */ + private Base64(){} + + public static void main(String[] args) + { + if (args.length == 0) { + help(); + return; + } + runApp(args); + } + + private static void runApp(String args[]) { + try { + InputStream in = System.in; + OutputStream out = System.out; + if (args.length >= 3) { + out = new FileOutputStream(args[2]); + } + if (args.length >= 2) { + in = new FileInputStream(args[1]); + } + if ("encode".equalsIgnoreCase(args[0])) { + encode(in, out); + return; + } + if ("decode".equalsIgnoreCase(args[0])) { + decode(in, out); + return; + } + } catch (IOException ioe) { + ioe.printStackTrace(System.err); + } + } + + + private static byte[] read(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); + byte buf[] = new byte[4096]; + while (true) { + int read = in.read(buf); + if (read < 0) + break; + baos.write(buf, 0, read); + } + return baos.toByteArray(); + } + + private static void encode(InputStream in, OutputStream out) throws IOException { + String encoded = encode(read(in)); + out.write(encoded.getBytes()); + } + + private static void decode(InputStream in, OutputStream out) throws IOException { + byte decoded[] = decode(new String(read(in))); + out.write(decoded); + } + + private static void help() { + System.out.println("Syntax: Base64 encode "); + System.out.println("or : Base64 encode "); + System.out.println("or : Base64 encode"); + System.out.println("or : Base64 decode "); + System.out.println("or : Base64 decode "); + System.out.println("or : Base64 decode"); + System.out.println("or : Base64 test"); + } + + + private static void test() { + String orig = "you smell"; + String encoded = Base64.encode(orig.getBytes()); + System.out.println("Encoded: [" + encoded + "]"); + byte decoded[] = Base64.decode(encoded); + String transformed = new String(decoded); + if (orig.equals(transformed)) + System.out.println("D(E('you smell')) == 'you smell'"); + else + throw new RuntimeException("D(E('you smell')) != 'you smell'!!! transformed = [" + transformed + "]"); + byte all[] = new byte[256]; + for (int i = 0; i < all.length; i++) + all[i] = (byte)(0xFF & i); + encoded = Base64.encode(all); + System.out.println("Encoded: [" + encoded + "]"); + decoded = Base64.decode(encoded); + if (DataHelper.eq(decoded, all)) + System.out.println("D(E([all bytes])) == [all bytes]"); + else + throw new RuntimeException("D(E([all bytes])) != [all bytes]!!!"); + } + + +/* ******** E N C O D I N G M E T H O D S ******** */ + + + /** + * Encodes the first three bytes of array threeBytes + * and returns a four-byte array in Base64 notation. + * + * @param threeBytes the array to convert + * @return four byte array in Base64 notation. + * @since 1.3 + */ + private static byte[] encode3to4( byte[] threeBytes ) + { return encode3to4( threeBytes, 3 ); + } // end encodeToBytes + + + + /** + * Encodes up to the first three bytes of array threeBytes + * and returns a four-byte array in Base64 notation. + * The actual number of significant bytes in your array is + * given by numSigBytes. + * The array threeBytes needs only be as big as + * numSigBytes. + * + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.3 + */ + private static byte[] encode3to4( byte[] threeBytes, int numSigBytes ) + { byte[] dest = new byte[4]; + encode3to4( threeBytes, 0, numSigBytes, dest, 0 ); + return dest; + } + + + + /** + * Encodes up to three bytes of the array source + * and writes the resulting four Base64 bytes to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 3 for + * the source array or destOffset + 4 for + * the destination array. + * The actual number of significant bytes in your array is + * given by numSigBytes. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4( + byte[] source, int srcOffset, int numSigBytes, + byte[] destination, int destOffset ) + { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) + | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) + | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); + + switch( numSigBytes ) + { + case 3: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; + return destination; + + case 2: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + case 1: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = EQUALS_SIGN; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Encodes a byte array into Base64 notation. + * Equivalen to calling + * encodeBytes( source, 0, source.length ) + * + * @param source The data to convert + * @since 1.4 + */ + private static String encodeBytes( byte[] source ) + { + return encodeBytes( source, false ); // don't add newlines + } // end encodeBytes + + /** + * Same as encodeBytes, except uses a filesystem / URL friendly set of characters, + * replacing / with ~, and + with - + */ + private static String safeEncode(byte[] source) { + String encoded = encodeBytes(source); + encoded = encoded.replace('/', '~'); + encoded = encoded.replace('+', '-'); + return encoded; + } + + + /** + * Same as decode, except from a filesystem / URL friendly set of characters, + * replacing / with ~, and + with - + */ + private static byte[] safeDecode(String source) { + String toDecode = source.replace('~', '/'); + toDecode = toDecode.replace('-', '+'); + return standardDecode(toDecode); + } + + /** + * Encodes a byte array into Base64 notation. + * Equivalen to calling + * encodeBytes( source, 0, source.length ) + * + * @param source The data to convert + * @param breakLines Break lines at 80 characters or less. + * @since 1.4 + */ + private static String encodeBytes( byte[] source, boolean breakLines ) + { + return encodeBytes( source, 0, source.length, breakLines ); + } // end encodeBytes + + + /** + * Encodes a byte array into Base64 notation. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @since 1.4 + */ + private static String encodeBytes( byte[] source, int off, int len ) + { + return encodeBytes( source, off, len, true ); + } // end encodeBytes + + + /** + * Encodes a byte array into Base64 notation. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param breakLines Break lines at 80 characters or less. + * @since 1.4 + */ + private static String encodeBytes( byte[] source, int off, int len, boolean breakLines ) + { + int len43 = len * 4 / 3; + byte[] outBuff = new byte[ ( len43 ) // Main 4:3 + + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding + + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for( ; d < len2; d+=3, e+=4 ) + { + encode3to4( source, d+off, 3, outBuff, e ); + + lineLength += 4; + if( breakLines && lineLength == MAX_LINE_LENGTH ) + { + outBuff[e+4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if( d < len ) + { + encode3to4( source, d+off, len - d, outBuff, e ); + e += 4; + } // end if: some padding needed + + return new String( outBuff, 0, e ); + } // end encodeBytes + + + /** + * Encodes a string in Base64 notation with line breaks + * after every 75 Base64 characters. + * + * @param s the string to encode + * @return the encoded string + * @since 1.3 + */ + private static String encodeString( String s ) + { + return encodeString( s, true ); + } // end encodeString + + /** + * Encodes a string in Base64 notation with line breaks + * after every 75 Base64 characters. + * + * @param s the string to encode + * @param breakLines Break lines at 80 characters or less. + * @return the encoded string + * @since 1.3 + */ + private static String encodeString( String s, boolean breakLines ) + { + return encodeBytes( s.getBytes(), breakLines ); + } // end encodeString + + + + +/* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes the first four bytes of array fourBytes + * and returns an array up to three bytes long with the + * decoded values. + * + * @param fourBytes the array with Base64 content + * @return array with decoded values + * @since 1.3 + */ + private static byte[] decode4to3( byte[] fourBytes ) + { + byte[] outBuff1 = new byte[3]; + int count = decode4to3( fourBytes, 0, outBuff1, 0 ); + byte[] outBuff2 = new byte[ count ]; + + for( int i = 0; i < count; i++ ) + outBuff2[i] = outBuff1[i]; + + return outBuff2; + } + + + + + /** + * Decodes four bytes from array source + * and writes the resulting bytes (up to three of them) + * to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 4 for + * the source array or destOffset + 3 for + * the destination array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3( byte[] source, int srcOffset, byte[] destination, int destOffset ) + { + // Example: Dk== + if( source[ srcOffset + 2] == EQUALS_SIGN ) + { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 ); + + destination[ destOffset ] = (byte)( outBuff >>> 16 ); + return 1; + } + + // Example: DkL= + else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) + { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) + | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 ); + + destination[ destOffset ] = (byte)( outBuff >>> 16 ); + destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 ); + return 2; + } + + // Example: DkLE + else + { + try{ + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) + | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6) + | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) ); + + + destination[ destOffset ] = (byte)( outBuff >> 16 ); + destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); + destination[ destOffset + 2 ] = (byte)( outBuff ); + + return 3; + }catch( Exception e){ + System.out.println(""+source[srcOffset]+ ": " + ( DECODABET[ source[ srcOffset ] ] ) ); + System.out.println(""+source[srcOffset+1]+ ": " + ( DECODABET[ source[ srcOffset + 1 ] ] ) ); + System.out.println(""+source[srcOffset+2]+ ": " + ( DECODABET[ source[ srcOffset + 2 ] ] ) ); + System.out.println(""+source[srcOffset+3]+ ": " + ( DECODABET[ source[ srcOffset + 3 ] ] ) ); + return -1; + } //e nd catch + } + } // end decodeToBytes + + /** + * Decodes data from Base64 notation. + * + * @param s the string to decode + * @return the decoded data + * @since 1.4 + */ + private static byte[] standardDecode( String s ) + { + byte[] bytes = s.getBytes(); + return decode( bytes, 0, bytes.length ); + } // end decode + + + /** + * Decodes data from Base64 notation and + * returns it as a string. + * Equivlaent to calling + * new String( decode( s ) ) + * + * @param s the strind to decode + * @return The data as a string + * @since 1.4 + */ + private static String decodeToString( String s ) + { return new String( decode( s ) ); + } // end decodeToString + + /** + * Decodes Base64 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @return decoded data + * @since 1.3 + */ + private static byte[] decode( byte[] source, int off, int len ) + { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for( i = 0; i < len; i++ ) + { + sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits + sbiDecode = DECODABET[ sbiCrop ]; + + if( sbiDecode >= WHITE_SPACE_ENC ) // White space, Equals sign or better + { + if( sbiDecode >= EQUALS_SIGN_ENC ) + { + b4[ b4Posn++ ] = sbiCrop; + if( b4Posn > 3 ) + { + outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn ); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if( sbiCrop == EQUALS_SIGN ) + break; + } // end if: quartet built + + } // end if: equals sign or better + + } // end if: white space, equals sign or better + else + { + System.err.println( "Bad Base64 input character at " + i + ": " + source[i] + "(decimal)" ); + return null; + } // end else: + } // each input character + + byte[] out = new byte[ outBuffPosn ]; + System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); + return out; + } // end decode +} // end class Base64 diff --git a/core/java/src/net/i2p/data/ByteArray.java b/core/java/src/net/i2p/data/ByteArray.java new file mode 100644 index 000000000..296cbf915 --- /dev/null +++ b/core/java/src/net/i2p/data/ByteArray.java @@ -0,0 +1,46 @@ +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.io.Serializable; + +/** + * Wrap up an array of bytes so that they can be compared and placed in hashes, + * maps, and the like. + * + * @author jrandom + */ +public class ByteArray implements Serializable { + private byte[] _data; + public ByteArray() { this(null); } + public ByteArray(byte[] data) { _data = data; } + public byte[] getData() { return _data; } + public void setData(byte[] data) { _data = data; } + + public boolean equals(Object o) { + if (o == null) return false; + if (o instanceof ByteArray) { + return compare(getData(), ((ByteArray)o).getData()); + } else { + try { + byte val[] = (byte[])o; + return compare(getData(), val); + } catch (Throwable t) { + return false; + } + } + } + + private boolean compare(byte[] lhs, byte[] rhs) { + return DataHelper.eq(lhs, rhs); + } + + public int hashCode() { return DataHelper.hashCode(getData()); } + public String toString() { return DataHelper.toString(getData(), 32); } +} diff --git a/core/java/src/net/i2p/data/Certificate.java b/core/java/src/net/i2p/data/Certificate.java new file mode 100644 index 000000000..6013f8b31 --- /dev/null +++ b/core/java/src/net/i2p/data/Certificate.java @@ -0,0 +1,115 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Defines a certificate that can be attached to various I2P structures, such + * as RouterIdentity and Destination, allowing routers and clients to help + * manage denial of service attacks and the network utilization. Certificates + * can even be defined to include identifiable information signed by some + * certificate authority, though that use probably isn't appropriate for an + * anonymous network ;) + * + * @author jrandom + */ +public class Certificate extends DataStructureImpl { + private final static Log _log = new Log(Certificate.class); + private int _type; + private byte[] _payload; + + /** Specifies a null certificate type with no payload */ + public final static int CERTIFICATE_TYPE_NULL = 0; + /** specifies a Hashcash style certificate */ + public final static int CERTIFICATE_TYPE_HASHCASH = 1; + + public Certificate() { + _type = 0; + _payload = null; + } + + public Certificate(int type, byte[] payload) { + _type = type; + _payload = payload; + } + + /** */ + public int getCertificateType() { return _type; } + public void setCertificateType(int type) { _type = type; } + + public byte[] getPayload() { return _payload; } + public void setPayload(byte[] payload) { _payload = payload; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _type = (int)DataHelper.readLong(in, 1); + int length = (int)DataHelper.readLong(in, 2); + if (length > 0) { + _payload = new byte[length]; + int read = read(in, _payload); + if (read != length) + throw new DataFormatException("Not enough bytes for the payload (read: " + read + " length: " + length + ")"); + } + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_type < 0) + throw new DataFormatException("Invalid certificate type: " + _type); + if ( (_type != 0) && (_payload == null) ) + throw new DataFormatException("Payload is required for non null type"); + + DataHelper.writeLong(out, 1, (long)_type); + if (_payload != null) { + DataHelper.writeLong(out, 2, (long)_payload.length); + out.write(_payload); + } else { + DataHelper.writeLong(out, 2, 0L); + } + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof Certificate) ) + return false; + Certificate cert = (Certificate)object; + return getCertificateType() == cert.getCertificateType() && + DataHelper.eq(getPayload(), cert.getPayload()); + } + + public int hashCode() { + return getCertificateType() + DataHelper.hashCode(getPayload()); + } + + public String toString() { + StringBuffer buf = new StringBuffer(64); + buf.append("[Certificate: type: "); + if (getCertificateType() == CERTIFICATE_TYPE_NULL) + buf.append("Null certificate"); + else if (getCertificateType() == CERTIFICATE_TYPE_HASHCASH) + buf.append("Hashcash certificate"); + else + buf.append("Unknown certificiate 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)); + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/DataFormatException.java b/core/java/src/net/i2p/data/DataFormatException.java new file mode 100644 index 000000000..23f2c1c76 --- /dev/null +++ b/core/java/src/net/i2p/data/DataFormatException.java @@ -0,0 +1,28 @@ +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 net.i2p.util.Log; +import net.i2p.I2PException; + +/** + * Thrown when the data was not available to read or write a DataStructure + * + * @author jrandom + */ +public class DataFormatException extends I2PException { + private final static Log _log = new Log(DataFormatException.class); + + public DataFormatException(String msg, Throwable t) { + super(msg, t); + } + public DataFormatException(String msg) { + super(msg); + } +} diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java new file mode 100644 index 000000000..0f8a188da --- /dev/null +++ b/core/java/src/net/i2p/data/DataHelper.java @@ -0,0 +1,552 @@ +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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.TreeMap; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import net.i2p.util.Log; +import net.i2p.util.OrderedProperties; + +/** + * Defines some simple IO routines for dealing with marshalling data structures + * + * @author jrandom + */ +public class DataHelper { + private final static Log _log = new Log(DataHelper.class); + private final static String _equal = "="; // in UTF-8 + private final static String _semicolon = ";"; // in UTF-8 + + /** Read a mapping from the stream, as defined by the I2P data structure spec, + * and store it into a Properties object. + * + * A mapping is a set of key / value pairs. It starts with a 2 byte Integer (ala readLong(rawStream, 2)) + * defining how many bytes make up the mapping. After that comes that many bytes making + * up a set of UTF-8 encoded characters. The characters are organized as key=value;. + * The key is a String (ala readString(rawStream)) unique as a key within the current + * mapping that does not include the UTF-8 characters '=' or ';'. After the key + * comes the literal UTF-8 character '='. After that comes a String (ala readString(rawStream)) + * for the value. Finally after that comes the literal UTF-8 character ';'. This key=value; + * is repeated until there are no more bytes (not characters!) left as defined by the + * first two byte integer. + * @param rawStream stream to read the mapping from + * @throws DataFormatException if the format is invalid + * @throws IOException if there is a problem reading the data + * @return mapping + */ + public static Properties readProperties(InputStream rawStream) throws DataFormatException, IOException { + Properties props = new OrderedProperties(); + long size = readLong(rawStream, 2); + byte data[] = new byte[(int)size]; + int read = read(rawStream, data); + if (read != size) + throw new DataFormatException("Not enough data to read the properties"); + ByteArrayInputStream in = new ByteArrayInputStream(data); + byte eqBuf[] = _equal.getBytes(); + byte semiBuf[] = _semicolon.getBytes(); + try { + while (in.available() > 0) { + String key = readString(in); + read = read(in, eqBuf); + if ((read != eqBuf.length) || (!eq(new String(eqBuf), _equal))) { + _log.debug("Failed eqtest [" + new String(eqBuf) + "]"); + break; + } + String val = readString(in); + read = read(in, semiBuf); + if ((read != semiBuf.length) || (!eq(new String(semiBuf), _semicolon))) { + _log.debug("Failed semitest [" + new String(semiBuf) + "]"); + break; + } + props.put(key, val); + } + } catch (IOException ioe) { + _log.warn("Error reading properties", ioe); + } + return props; + } + + /** + * Write a mapping to the stream, as defined by the I2P data structure spec, + * and store it into a Properties object. See readProperties for the format. + * + * @param rawStream stream to write to + * @param props properties to write out + * @throws DataFormatException if there is not enough valid data to write out + * @throws IOException if there is an IO error writing out the data + */ + public static void writeProperties(OutputStream rawStream, Properties props) throws DataFormatException, IOException { + OrderedProperties p = new OrderedProperties(); + if (props != null) + p.putAll(props); + ByteArrayOutputStream baos = new ByteArrayOutputStream(32); + for (Iterator iter = p.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = p.getProperty(key); + // now make sure they're in UTF-8 + key = new String(key.getBytes(), "UTF-8"); + val = new String(val.getBytes(), "UTF-8"); + writeString(baos, key); + baos.write(_equal.getBytes()); + writeString(baos, val); + baos.write(_semicolon.getBytes()); + } + baos.close(); + byte propBytes[] = baos.toByteArray(); + writeLong(rawStream, 2, propBytes.length); + rawStream.write(propBytes); + } + + /** + * Pretty print the mapping + * + */ + public static String toString(Properties options) { + StringBuffer buf = new StringBuffer(); + if (options != null) { + for (Iterator iter = options.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = options.getProperty(key); + buf.append("[").append(key).append("] = [").append(val).append("]"); + } + } else { + buf.append("(null properties map)"); + } + return buf.toString(); + } + + /** + * Pretty print the collection + * + */ + public static String toString(Collection col) { + StringBuffer buf = new StringBuffer(); + if (col != null) { + for (Iterator iter = col.iterator(); iter.hasNext(); ) { + Object o = iter.next(); + buf.append("[").append(o).append("]"); + if (iter.hasNext()) + buf.append(", "); + } + } else { + buf.append("null"); + } + return buf.toString(); + } + + + public static String toString(byte buf[]) { + if (buf == null) return ""; + else return toString(buf, buf.length); + } + + public static String toString(byte buf[], int len) { + if (buf == null) buf = "".getBytes(); + StringBuffer out = new StringBuffer(); + if (len > buf.length) { + for (int i = 0; i < len - buf.length; i++) + out.append("00"); + } + for (int i = 0; i < buf.length && i < len; i++) { + StringBuffer temp = new StringBuffer(Integer.toHexString((int)buf[i])); + while (temp.length() < 2) { + temp.insert(0, '0'); + } + temp = new StringBuffer(temp.substring(temp.length()-2)); + out.append(temp.toString()); + } + return out.toString(); + } + public static String toDecimalString(byte buf[], int len) { + if (buf == null) buf = "".getBytes(); + BigInteger val = new BigInteger(1, buf); + return val.toString(10); + } + + public final static String toHexString(byte data[]) { + if ( (data == null) || (data.length <= 0) ) return "00"; + BigInteger bi = new BigInteger(1, data); + return bi.toString(16); + } + + public final static byte[] fromHexString(String val) { + BigInteger bv = new BigInteger(val, 16); + return bv.toByteArray(); + } + + /** Read the stream for an integer as defined by the I2P data structure specification. + * Integers are a fixed number of bytes (numBytes), stored as unsigned integers in network byte order. + * @param rawStream stream to read from + * @param numBytes number of bytes to read and format into a number + * @throws DataFormatException if the stream doesn't contain a validly formatted number of that many bytes + * @throws IOException if there is an IO error reading the number + * @return number + */ + public static long readLong(InputStream rawStream, int numBytes) throws DataFormatException, IOException { + if (numBytes > 8) + throw new DataFormatException("readLong doesn't currently support reading numbers > 8 bytes [as thats bigger than java's long]"); + byte data[] = new byte[numBytes]; + int num = read(rawStream, data); + if (num != numBytes) + throw new DataFormatException("Not enough bytes [" + num + "] as required for the field [" + numBytes + "]"); + + UnsignedInteger val = new UnsignedInteger(data); + return val.getLong(); + } + + /** Write an integer as defined by the I2P data structure specification to the stream. + * Integers are a fixed number of bytes (numBytes), stored as unsigned integers in network byte order. + * @param value value to write out + * @param rawStream stream to write to + * @param numBytes number of bytes to write the number into (padding as necessary) + * @throws DataFormatException if the stream doesn't contain a validly formatted number of that many bytes + * @throws IOException if there is an IO error writing to the stream + */ + public static void writeLong(OutputStream rawStream, int numBytes, long value) throws DataFormatException, IOException { + UnsignedInteger i = new UnsignedInteger(value); + rawStream.write(i.getBytes(numBytes)); + } + + /** Read in a date from the stream as specified by the I2P data structure spec. + * A date is an 8 byte unsigned integer in network byte order specifying the number of + * milliseconds since midnight on January 1, 1970 in the GMT timezone. If the number is + * 0, the date is undefined or null. (yes, this means you can't represent midnight on 1/1/1970) + * @param in stream to read from + * @throws DataFormatException if the stream doesn't contain a validly formatted date + * @throws IOException if there is an IO error reading the date + * @return date read, or null + */ + public static Date readDate(InputStream in) throws DataFormatException, IOException { + long date = readLong(in, 8); + if (date == 0L) + return null; + else + return new Date(date); + } + + /** Write out a date to the stream as specified by the I2P data structure spec. + * @param out stream to write to + * @param date date to write (can be null) + * @throws DataFormatException if the date is not valid + * @throws IOException if there is an IO error writing the date + */ + public static void writeDate(OutputStream out, Date date) throws DataFormatException, IOException { + if (date == null) + writeLong(out, 8, 0L); + else + writeLong(out, 8, date.getTime()); + } + + /** Read in a string from the stream as specified by the I2P data structure spec. + * A string is 1 or more bytes where the first byte is the number of bytes (not characters!) + * in the string and the remaining 0-255 bytes are the non-null terminated UTF-8 encoded character array. + * @param in stream to read from + * @throws DataFormatException if the stream doesn't contain a validly formatted string + * @throws IOException if there is an IO error reading the string + * @return UTF-8 string + */ + public static String readString(InputStream in) throws DataFormatException, IOException { + int size = (int)readLong(in, 1); + byte raw[] = new byte[size]; + int read = read(in, raw); + if (read != size) + throw new DataFormatException("Not enough bytes to read the string"); + return new String(raw); + } + /** Write out a string to the stream as specified by the I2P data structure spec. Note that the max + * size for a string allowed by the spec is 255 bytes. + * + * @param out stream to write string + * @param string string to write out: null strings are perfectly valid, but strings of excess length will + * cause a DataFormatException to be thrown + * @throws DataFormatException if the string is not valid + * @throws IOException if there is an IO error writing the string + */ + public static void writeString(OutputStream out, String string) throws DataFormatException, IOException { + if (string == null) { + writeLong(out, 1, 0); + } else { + if (string.length() > 255) + throw new DataFormatException("The I2P data spec limits strings to 255 bytes or less, but this is " + string.length() + " [" + string + "]"); + byte raw[] = string.getBytes(); + writeLong(out, 1, raw.length); + out.write(raw); + } + } + + /** Read in a boolean as specified by the I2P data structure spec. + * A boolean is 1 byte that is either 0 (false), 1 (true), or 2 (null) + * @param in stream to read from + * @throws DataFormatException if the boolean is not valid + * @throws IOException if there is an IO error reading the boolean + * @return boolean value, or null + */ + public static Boolean readBoolean(InputStream in) throws DataFormatException, IOException { + int val = (int)readLong(in, 1); + switch (val) { + case 0: + return Boolean.FALSE; + case 1: + return Boolean.TRUE; + case 2: + return null; + default: + throw new DataFormatException("Uhhh.. readBoolean read a value that isn't a known ternary val (0,1,2): " + val); + } + } + /** Write out a boolean as specified by the I2P data structure spec. + * A boolean is 1 byte that is either 0 (false), 1 (true), or 2 (null) + * @param out stream to write to + * @param bool boolean value, or null + * @throws DataFormatException if the boolean is not valid + * @throws IOException if there is an IO error writing the boolean + */ + public static void writeBoolean(OutputStream out, Boolean bool) throws DataFormatException, IOException { + if (bool == null) + writeLong(out, 1, 2); + else if (Boolean.TRUE.equals(bool)) + writeLong(out, 1, 1); + else + writeLong(out, 1, 0); + } + + // + // The following comparator helpers make it simpler to write consistently comparing + // functions for objects based on their value, not JVM memory address + // + + + /** + * Helper util to compare two objects, treating (null == null) as true, and + * (null == (!null)) as false. + * + */ + public final static boolean eq(Object lhs, Object rhs) { + try { + boolean eq = ( ( (lhs == null) && (rhs == null) ) || + ( (lhs != null) && (lhs.equals(rhs))) ); + return eq; + } catch (ClassCastException cce) { + _log.warn("Error comparing [" + lhs + "] with [" + rhs + "]", cce); + return false; + } + } + /** + * Deep compare two collections, treating (null == null) as true, + * (null == (!null)) as false, and then comparing each element via eq(object, object). + * If the size of the collections are not equal, the comparison returns false. + * The collection order should be consistent, as this simply iterates across both and compares + * based on the value of each at each step along the way. + * + */ + public final static boolean eq(Collection lhs, Collection rhs) { + if ( (lhs == null) && (rhs == null) ) return true; + if ( (lhs == null) || (rhs == null) ) return false; + if (lhs.size() != rhs.size()) return false; + Iterator liter = lhs.iterator(); + Iterator riter = rhs.iterator(); + while ( (liter.hasNext()) && (riter.hasNext()) ) + if (!(eq(liter.next(), riter.next()))) + return false; + return true; + } + + /** + * Compare the byte arrays byte by byte, treating (null == null) as + * true, (null == (!null)) as false, and unequal length arrays as false. + * + */ + public final static boolean eq(byte lhs[], byte rhs[]) { + boolean eq = ( ( (lhs == null) && (rhs == null) ) || + ( (lhs != null) && (rhs != null) && (Arrays.equals(lhs, rhs)) ) ); + return eq; + } + /** + * Compare two integers, really just for consistency. + */ + public final static boolean eq(int lhs, int rhs) { return lhs == rhs; } + /** + * Compare two longs, really just for consistency. + */ + public final static boolean eq(long lhs, long rhs) { return lhs == rhs; } + /** + * Compare two bytes, really just for consistency. + */ + public final static boolean eq(byte lhs, byte rhs) { return lhs == rhs; } + + public final static int compareTo(byte lhs[], byte rhs[]) { + if ( (rhs == null) && (lhs == null) ) return 0; + if (lhs == null) return -1; + if (rhs == null) return 1; + if (rhs.length < lhs.length) return 1; + if (rhs.length > lhs.length) return -1; + for (int i = 0; i < rhs.length; i++) { + if (rhs[i] > lhs[i]) return -1; + else if (rhs[i] < lhs[i]) return 1; + } + return 0; + } + + public final static byte[] xor(byte lhs[], byte rhs[]) { + if ( (lhs == null) || (rhs == null) || (lhs.length != rhs.length) ) return null; + byte diff[] = new byte[lhs.length]; + for (int i = 0; i < lhs.length; i++) + diff[i] = (byte)(lhs[i] ^ rhs[i]); + return diff; + } + + + // + // The following hashcode helpers make it simpler to write consistently hashing + // functions for objects based on their value, not JVM memory address + // + + /** + * Calculate the hashcode of the object, using 0 for null + * + */ + public static int hashCode(Object obj) { + if (obj == null) return 0; + else return obj.hashCode(); + } + /** + * Calculate the hashcode of the date, using 0 for null + * + */ + public static int hashCode(Date obj) { + if (obj == null) return 0; + else return (int)obj.getTime(); + } + /** + * Calculate the hashcode of the byte array, using 0 for null + * + */ + public static int hashCode(byte b[]) { + int rv = 0; + if (b != null) { + for (int i = 0; i < b.length && i < 8; i++) + rv += b[i]; + } + return rv; + } + + /** + * Calculate the hashcode of the collection, using 0 for null + * + */ + public static int hashCode(Collection col) { + if (col == null) return 0; + int c = 0; + for (Iterator iter = col.iterator(); iter.hasNext(); ) + c = 7*c + hashCode(iter.next()); + return c; + } + + public static int read(InputStream in, byte target[]) throws IOException { + int cur = 0; + while (cur < target.length) { + int numRead = in.read(target, cur, target.length - cur); + if (numRead == -1) { + if (cur == 0) + return -1; // throw new EOFException("EOF Encountered during reading"); + else + return cur; + } + cur += numRead; + } + return cur; + } + + + public static List sortStructures(Collection dataStructures) { + if (dataStructures == null) return new ArrayList(); + ArrayList rv = new ArrayList(dataStructures.size()); + TreeMap tm = new TreeMap(); + for (Iterator iter = dataStructures.iterator(); iter.hasNext(); ) { + DataStructure struct = (DataStructure)iter.next(); + tm.put(struct.calculateHash().toString(), struct); + } + for (Iterator iter = tm.keySet().iterator(); iter.hasNext();) { + Object k = iter.next(); + rv.add(tm.get(k)); + } + return rv; + } + + public static String formatDuration(long ms) { + if (ms < 30*1000) { + return ms + "ms"; + } else if (ms < 5*60*1000) { + return (ms/1000) + "s"; + } else if (ms < 90*60*1000) { + return (ms / (60*1000)) + "m"; + } else if (ms < 3*24*60*60*1000) { + return (ms / (60*60*1000)) + "h"; + } else { + return (ms / (24*60*60*1000)) + "d"; + } + } + + /** compress the data and return a new GZIP compressed array */ + public static byte[] compress(byte orig[]) { + if ( (orig == null) || (orig.length <= 0) ) return orig; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(orig.length); + GZIPOutputStream out = new GZIPOutputStream(baos, orig.length); + out.write(orig); + out.finish(); + out.flush(); + byte rv[] = baos.toByteArray(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Compression of " + orig.length + " into " + rv.length + " (or " + 100.0d*(((double)orig.length) / ((double)rv.length)) + "% savings)"); + return rv; + } catch (IOException ioe) { + _log.error("Error compressing?!", ioe); + return null; + } + } + + /** decompress the GZIP compressed data (returning null on error) */ + public static byte[] decompress(byte orig[]) { + if ( (orig == null) || (orig.length <= 0) ) return orig; + try { + GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(orig), orig.length); + ByteArrayOutputStream baos = new ByteArrayOutputStream(orig.length * 2); + byte buf[] = new byte[4*1024]; + while (true) { + int read = in.read(buf); + if (read == -1) + break; + baos.write(buf, 0, read); + } + byte rv[] = baos.toByteArray(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Decompression of " + orig.length + " into " + rv.length + " (or " + 100.0d*(((double)rv.length) / ((double)orig.length)) + "% savings)"); + return rv; + } catch (IOException ioe) { + _log.error("Error decompressing?", ioe); + return null; + } + } +} diff --git a/core/java/src/net/i2p/data/DataStructure.java b/core/java/src/net/i2p/data/DataStructure.java new file mode 100644 index 000000000..8044ded8c --- /dev/null +++ b/core/java/src/net/i2p/data/DataStructure.java @@ -0,0 +1,64 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.io.Serializable; + +/** + * Defines the class as a standard object with particular bit representation, + * exposing methods to read and write that representation. + * + * @author jrandom + */ +public interface DataStructure extends Serializable { + /** + * Load up the current object with data from the given stream. Data loaded + * this way must match the I2P data structure specification. + * + * @param in stream to read from + * @throws DataFormatException if the data is improperly formatted + * @throws IOException if there was a problem reading the stream + */ + public void readBytes(InputStream in) throws DataFormatException, IOException; + + /** + * Write out the data structure to the stream, using the format defined in the + * I2P data structure specification. + * + * @param out stream to write to + * @throws DataFormatException if the data was incomplete or not yet ready to be written + * @throws IOException if there was a problem writing to the stream + */ + public void writeBytes(OutputStream out) throws DataFormatException, IOException; + + /** + * render the structure into modified base 64 notation + * @return null on error + */ + public String toBase64(); + + /** + * Load the structure from the base 64 encoded data provided + * + */ + public void fromBase64(String data) throws DataFormatException; + + public byte[] toByteArray(); + public void fromByteArray(byte data[]) throws DataFormatException; + + /** + * Calculate the SHA256 value of this object (useful for a few scenarios) + * + * @return SHA256 hash, or null if there were problems (data format or io errors) + */ + public Hash calculateHash(); +} diff --git a/core/java/src/net/i2p/data/DataStructureImpl.java b/core/java/src/net/i2p/data/DataStructureImpl.java new file mode 100644 index 000000000..cbe9bcfb0 --- /dev/null +++ b/core/java/src/net/i2p/data/DataStructureImpl.java @@ -0,0 +1,80 @@ +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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.crypto.SHA256Generator; +import net.i2p.util.Log; + +/** + * Base implementation of all data structures + * + * @author jrandom + */ +public abstract class DataStructureImpl implements DataStructure { + private final static Log _log = new Log(DataStructureImpl.class); + + public String toBase64() { + byte data[] = toByteArray(); + if (data == null) return null; + else return Base64.encode(data); + } + + public void fromBase64(String data) throws DataFormatException { + if (data == null) + throw new DataFormatException("Null data passed in"); + byte bytes[] = Base64.decode(data); + fromByteArray(bytes); + } + + public Hash calculateHash() { + byte data[] = toByteArray(); + if (data != null) + return SHA256Generator.getInstance().calculateHash(data); + return null; + } + + public byte[] toByteArray() { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + writeBytes(baos); + return baos.toByteArray(); + } catch (IOException ioe) { + _log.error("Error writing out the byte array", ioe); + return null; + } catch (DataFormatException dfe) { + _log.error("Error writing out the byte array", dfe); + return null; + } + } + + public void fromByteArray(byte data[]) throws DataFormatException { + if (data == null) + throw new DataFormatException("Null data passed in"); + try { + ByteArrayInputStream bais = new ByteArrayInputStream(data); + readBytes(bais); + } catch (IOException ioe) { + throw new DataFormatException("Error reading the byte array", ioe); + } + } + + /** + * Repeated reads until the buffer is full or IOException is thrown + * + * @return number of bytes read (should always equal target.length) + */ + protected int read(InputStream in, byte target[]) throws IOException { + return DataHelper.read(in, target); + } +} diff --git a/core/java/src/net/i2p/data/Destination.java b/core/java/src/net/i2p/data/Destination.java new file mode 100644 index 000000000..dfc08986b --- /dev/null +++ b/core/java/src/net/i2p/data/Destination.java @@ -0,0 +1,102 @@ +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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import net.i2p.util.Log; + +/** + * Defines an end point in the I2P network. The Destination may move aroundn + * 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; + + public Destination() { + setCertificate(null); + setSigningPublicKey(null); + setPublicKey(null); + __calculatedHash = null; + } + + public Certificate getCertificate() { return _certificate; } + public void setCertificate(Certificate cert) { + _certificate = cert; + __calculatedHash = null; + } + public PublicKey getPublicKey() { return _publicKey; } + public void setPublicKey(PublicKey key) { + _publicKey = key; + __calculatedHash = null; + } + public SigningPublicKey getSigningPublicKey() { return _signingKey; } + public void setSigningPublicKey(SigningPublicKey key) { + _signingKey = key; + __calculatedHash = null; + } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _publicKey = new PublicKey(); + _publicKey.readBytes(in); + _signingKey = new SigningPublicKey(); + _signingKey.readBytes(in); + _certificate = new Certificate(); + _certificate.readBytes(in); + __calculatedHash = null; + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if ( (_certificate == null) || (_publicKey == null) || (_signingKey == null) ) + throw new DataFormatException("Not enough data to format the destination"); + _publicKey.writeBytes(out); + _signingKey.writeBytes(out); + _certificate.writeBytes(out); + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof Destination)) + return false; + Destination dst = (Destination)object; + return DataHelper.eq(getCertificate(), dst.getCertificate()) && + DataHelper.eq(getSigningPublicKey(), dst.getSigningPublicKey()) && + DataHelper.eq(getPublicKey(), dst.getPublicKey()); + } + + public int hashCode() { + return DataHelper.hashCode(getCertificate()) + + DataHelper.hashCode(getSigningPublicKey()) + + DataHelper.hashCode(getPublicKey()); + } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append("[Destination: "); + buf.append("\n\tHash: ").append(calculateHash().toBase64()); + buf.append("\n\tPublic Key: ").append(getPublicKey()); + buf.append("\n\tSigning Public Key: ").append(getSigningPublicKey()); + buf.append("\n\tCertificate: ").append(getCertificate()); + buf.append("]"); + return buf.toString(); + } + + public Hash calculateHash() { + if (__calculatedHash == null) + __calculatedHash = super.calculateHash(); + return __calculatedHash; + } +} diff --git a/core/java/src/net/i2p/data/Hash.java b/core/java/src/net/i2p/data/Hash.java new file mode 100644 index 000000000..4f9e71ec7 --- /dev/null +++ b/core/java/src/net/i2p/data/Hash.java @@ -0,0 +1,79 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Defines the hash as defined by the I2P data structure spec. + * AA hash is the SHA-256 of some data, taking up 32 bytes. + * + * @author jrandom + */ +public class Hash extends DataStructureImpl { + private final static Log _log = new Log(Hash.class); + private byte[] _data; + private volatile String _stringified; + + public final static int HASH_LENGTH = 32; + public final static Hash FAKE_HASH = new Hash(new byte[HASH_LENGTH]); + + public Hash() { setData(null); } + public Hash(byte data[]) { setData(data); } + + public byte[] getData() { return _data; } + public void setData(byte[] data) { + _data = data; + _stringified = null; + } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _data = new byte[HASH_LENGTH]; + _stringified = null; + int read = read(in, _data); + if (read != HASH_LENGTH) + throw new DataFormatException("Not enough bytes to read the hash"); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_data == null) + throw new DataFormatException("No data in the hash to write out"); + if (_data.length != HASH_LENGTH) + throw new DataFormatException("Invalid size of data in the private key"); + out.write(_data); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof Hash)) + return false; + return DataHelper.eq(_data, ((Hash)obj)._data); + } + + public int hashCode() { + return DataHelper.hashCode(_data); + } + public String toString() { + if (_stringified == null) { + StringBuffer buf = new StringBuffer(64); + buf.append("[Hash: "); + if (_data == null) { + buf.append("null hash"); + } else { + buf.append(toBase64()); + } + buf.append("]"); + _stringified = buf.toString(); + } + return _stringified; + } +} diff --git a/core/java/src/net/i2p/data/Lease.java b/core/java/src/net/i2p/data/Lease.java new file mode 100644 index 000000000..3b38265b3 --- /dev/null +++ b/core/java/src/net/i2p/data/Lease.java @@ -0,0 +1,125 @@ +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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; + +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Defines the proof that a particular router / tunnel is allowed to receive + * messages for a particular Destination during some period of time. + * + * @author jrandom + */ +public class Lease extends DataStructureImpl { + private final static Log _log = new Log(Lease.class); + private RouterIdentity _routerIdentity; + private TunnelId _tunnelId; + private Date _end; + private int _numSuccess; + private int _numFailure; + + public Lease() { + setRouterIdentity(null); + setTunnelId(null); + setEndDate(null); + setNumSuccess(0); + setNumFailure(0); + } + + /** Retrieve the router at which the destination can be contacted + * @return identity of the router acting as a gateway + */ + public RouterIdentity getRouterIdentity() { return _routerIdentity; } + /** Configure the router at which the destination can be contacted + * @param ident router acting as the gateway + */ + public void setRouterIdentity(RouterIdentity ident) { _routerIdentity = ident; } + /** Tunnel on the gateway to communicate with + * @return tunnel ID + */ + public TunnelId getTunnelId() { return _tunnelId; } + /** Configure the tunnel on the gateway to communicate with + * @param id tunnel ID + */ + public void setTunnelId(TunnelId id) { _tunnelId = id; } + public Date getEndDate() { return _end; } + public void setEndDate(Date date) { _end = date; } + + /** + * Transient attribute of the lease, used to note how many times messages sent + * to the destination through the current lease were successful. + * + */ + public int getNumSuccess() { return _numSuccess; } + public void setNumSuccess(int num) { _numSuccess = num; } + + /** + * Transient attribute of the lease, used to note how many times messages sent + * to the destination through the current lease failed. + * + */ + public int getNumFailure() { return _numFailure; } + public void setNumFailure(int num) { _numFailure = num; } + + /** has this lease already expired? */ + public boolean isExpired() { return isExpired(0); } + /** has this lease already expired (giving allowing up the fudgeFactor milliseconds for clock skew)? */ + public boolean isExpired(long fudgeFactor) { + if (_end == null) return true; + return _end.getTime() < Clock.getInstance().now() - fudgeFactor; + } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _routerIdentity = new RouterIdentity(); + _routerIdentity.readBytes(in); + _tunnelId = new TunnelId(); + _tunnelId.readBytes(in); + _end = DataHelper.readDate(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if ( (_routerIdentity == null) || (_tunnelId == null) ) + throw new DataFormatException("Not enough data to write out a Lease"); + + _routerIdentity.writeBytes(out); + _tunnelId.writeBytes(out); + DataHelper.writeDate(out, _end); + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof Lease) ) + return false; + Lease lse = (Lease)object; + return DataHelper.eq(getEndDate(), lse.getEndDate()) && + DataHelper.eq(getRouterIdentity(), lse.getRouterIdentity()); + + } + + public int hashCode() { + return DataHelper.hashCode(getEndDate()) + + DataHelper.hashCode(getRouterIdentity()) + + DataHelper.hashCode(getTunnelId()); + } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append("[Lease: "); + buf.append("\n\tEnd Date: ").append(getEndDate()); + buf.append("\n\tRouter Identity: ").append(getRouterIdentity()); + buf.append("\n\tTunnelId: ").append(getTunnelId()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/LeaseSet.java b/core/java/src/net/i2p/data/LeaseSet.java new file mode 100644 index 000000000..e50f9911b --- /dev/null +++ b/core/java/src/net/i2p/data/LeaseSet.java @@ -0,0 +1,280 @@ +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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import net.i2p.crypto.DSAEngine; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +/** + * Defines the set of leases a destination currently has. + * + * @author jrandom + */ +public class LeaseSet extends DataStructureImpl { + private final static Log _log = new Log(LeaseSet.class); + private Destination _destination; + private PublicKey _encryptionKey; + private SigningPublicKey _signingKey; + private List _leases; + private Signature _signature; + private volatile Hash _currentRoutingKey; + private volatile byte[] _routingKeyGenMod; + + /** um, no lease can last more than a year. */ + private final static long MAX_FUTURE_EXPIRATION = 365*24*60*60*1000L; + + public LeaseSet() { + setDestination(null); + setEncryptionKey(null); + setSigningKey(null); + setSignature(null); + setRoutingKey(null); + _leases = new ArrayList(); + _routingKeyGenMod = null; + } + + public Destination getDestination() { return _destination; } + public void setDestination(Destination dest) { _destination = dest; } + public PublicKey getEncryptionKey() { return _encryptionKey; } + public void setEncryptionKey(PublicKey encryptionKey) { _encryptionKey = encryptionKey; } + public SigningPublicKey getSigningKey() { return _signingKey; } + public void setSigningKey(SigningPublicKey key) { _signingKey = key; } + public void addLease(Lease lease) { _leases.add(lease); } + public void removeLease(Lease lease) { _leases.remove(lease); } + public int getLeaseCount() { return _leases.size(); } + public Lease getLease(int index) { return (Lease)_leases.get(index); } + public Signature getSignature() { return _signature; } + public void setSignature(Signature sig) { _signature = sig; } + + /** + * Get the routing key for the structure using the current modifier in the RoutingKeyGenerator. + * This only calculates a new one when necessary though (if the generator's key modifier changes) + * + */ + public Hash getRoutingKey() { + RoutingKeyGenerator gen = RoutingKeyGenerator.getInstance(); + if ( (gen.getModData() == null) || (_routingKeyGenMod == null) || (!DataHelper.eq(gen.getModData(), _routingKeyGenMod)) ) { + setRoutingKey(gen.getRoutingKey(getDestination().calculateHash())); + _routingKeyGenMod = gen.getModData(); + } + return _currentRoutingKey; + } + public void setRoutingKey(Hash key) { _currentRoutingKey = key; } + + public boolean validateRoutingKey() { + Hash destKey = getDestination().calculateHash(); + Hash rk = RoutingKeyGenerator.getInstance().getRoutingKey(destKey); + if (rk.equals(getRoutingKey())) + return true; + else + return false; + } + + /** + * Retrieve the end date of the earliest lease include in this leaseSet. + * This is the date that should be used in comparisons for leaseSet age - to + * determine which LeaseSet was published more recently (later earliestLeaseSetDate + * means it was published later) + * + * @return earliest end date of any lease in the set, or -1 if there are no leases + */ + public long getEarliestLeaseDate() { + long when = -1; + for (int i = 0; i < getLeaseCount(); i++) { + Lease lse = (Lease)getLease(i); + if ( (lse != null) && (lse.getEndDate() != null) ) { + if ( (when <= 0) || (lse.getEndDate().getTime() < when) ) + when = lse.getEndDate().getTime(); + } + } + return when; + } + + /** + * Sign the structure using the supplied signing key + * + */ + public void sign(SigningPrivateKey key) throws DataFormatException { + byte[] bytes = getBytes(); + if (bytes == null) throw new DataFormatException("Not enough data to sign"); + // now sign with the key + Signature sig = DSAEngine.getInstance().sign(bytes, key); + setSignature(sig); + } + + /** + * Verify that the signature matches the lease set's destination's signing public key. + * + * @return true only if the signature matches + */ + public boolean verifySignature() { + if (getSignature() == null) return false; + if (getDestination() == null) return false; + byte data[] = getBytes(); + if (data == null) return false; + boolean signedByDest = DSAEngine.getInstance().verifySignature(getSignature(), data, getDestination().getSigningPublicKey()); + boolean signedByRevoker = false; + if (!signedByDest) { + signedByRevoker = DSAEngine.getInstance().verifySignature(getSignature(), data, _signingKey); + } + return signedByDest || signedByRevoker; + } + /** + * Verify that the signature matches the lease set's destination's signing public key. + * + * @return true only if the signature matches + */ + public boolean verifySignature(SigningPublicKey signingKey) { + if (getSignature() == null) return false; + if (getDestination() == null) return false; + byte data[] = getBytes(); + if (data == null) return false; + boolean signedByDest = DSAEngine.getInstance().verifySignature(getSignature(), data, getDestination().getSigningPublicKey()); + boolean signedByRevoker = false; + if (!signedByDest) { + signedByRevoker = DSAEngine.getInstance().verifySignature(getSignature(), data, signingKey); + } + return signedByDest || signedByRevoker; + } + + /** + * Determine whether there are currently valid leases, at least within a given + * fudge factor + * + * @param fudge milliseconds fudge factor to allow between the current time + * @return true if there are current leases, false otherwise + */ + public boolean isCurrent(long fudge) { + long now = Clock.getInstance().now(); + long insane = now + MAX_FUTURE_EXPIRATION; + int cnt = getLeaseCount(); + for (int i = 0; i < cnt; i++) { + Lease l = getLease(i); + if (l.getEndDate().getTime() > insane) { + _log.warn("LeaseSet" + calculateHash() + " expires an insane amount in the future - skip it: " + l); + return false; + } + // if it hasn't finished, we're current + if (l.getEndDate().getTime() > now) { + _log.debug("LeaseSet " + calculateHash() + " isn't exired: " + l); + return true; + } else if (l.getEndDate().getTime() > now - fudge) { + _log.debug("LeaseSet " + calculateHash() + " isn't quite expired, but its within the fudge factor so we'll let it slide: " + l); + return true; + } + } + return false; + } + + private byte[] getBytes() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + if ( (_destination == null) || (_encryptionKey == null) || (_signingKey == null) || + (_leases == null) ) + return null; + + _destination.writeBytes(out); + _encryptionKey.writeBytes(out); + _signingKey.writeBytes(out); + DataHelper.writeLong(out, 1, _leases.size()); + //DataHelper.writeLong(out, 4, _version); + for (Iterator iter = _leases.iterator(); iter.hasNext();) { + Lease lease = (Lease)iter.next(); + lease.writeBytes(out); + } + } catch (IOException ioe) { + return null; + } catch (DataFormatException dfe) { + return null; + } + return out.toByteArray(); + } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _destination = new Destination(); + _destination.readBytes(in); + _encryptionKey = new PublicKey(); + _encryptionKey.readBytes(in); + _signingKey = new SigningPublicKey(); + _signingKey.readBytes(in); + int numLeases = (int)DataHelper.readLong(in, 1); + //_version = DataHelper.readLong(in, 4); + _leases.clear(); + for (int i = 0; i < numLeases; i++) { + Lease lease = new Lease(); + lease.readBytes(in); + _leases.add(lease); + } + _signature = new Signature(); + _signature.readBytes(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if ( (_destination == null) || (_encryptionKey == null) || (_signingKey == null) || + (_leases == null) || (_signature == null) ) + throw new DataFormatException("Not enough data to write out a LeaseSet"); + + _destination.writeBytes(out); + _encryptionKey.writeBytes(out); + _signingKey.writeBytes(out); + DataHelper.writeLong(out, 1, _leases.size()); + //DataHelper.writeLong(out, 4, _version); + for (Iterator iter = _leases.iterator(); iter.hasNext();) { + Lease lease = (Lease)iter.next(); + lease.writeBytes(out); + } + _signature.writeBytes(out); + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof LeaseSet) ) + return false; + LeaseSet ls = (LeaseSet)object; + return DataHelper.eq(getEncryptionKey(), ls.getEncryptionKey()) && + //DataHelper.eq(getVersion(), ls.getVersion()) && + DataHelper.eq(_leases, ls._leases) && + DataHelper.eq(getSignature(), ls.getSignature()) && + DataHelper.eq(getSigningKey(), ls.getSigningKey()) && + DataHelper.eq(getDestination(), ls.getDestination()); + + } + + public int hashCode() { + return DataHelper.hashCode(getEncryptionKey()) + + //(int)_version + + DataHelper.hashCode(_leases) + + DataHelper.hashCode(getSignature()) + + DataHelper.hashCode(getSigningKey()) + + DataHelper.hashCode(getDestination()); + } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append("[LeaseSet: "); + buf.append("\n\tDestination: ").append(getDestination()); + buf.append("\n\tEncryptionKey: ").append(getEncryptionKey()); + buf.append("\n\tSigningKey: ").append(getSigningKey()); + //buf.append("\n\tVersion: ").append(getVersion()); + buf.append("\n\tSignature: ").append(getSignature()); + buf.append("\n\tLeases: #").append(getLeaseCount()); + for (int i = 0; i < getLeaseCount(); i++) + buf.append("\n\t\tLease (").append(i).append("): ").append(getLease(i)); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/Payload.java b/core/java/src/net/i2p/data/Payload.java new file mode 100644 index 000000000..639c43460 --- /dev/null +++ b/core/java/src/net/i2p/data/Payload.java @@ -0,0 +1,100 @@ +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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import net.i2p.util.Log; + +/** + * Defines the actual payload of a message being delivered, including the + * standard encryption wrapping, as defined by the I2P data structure spec.

+ * + * @author jrandom + */ +public class Payload extends DataStructureImpl { + private final static Log _log = new Log(Payload.class); + private byte[] _encryptedData; + private byte[] _unencryptedData; + + public Payload() { + setUnencryptedData(null); + setEncryptedData(null); + } + + /** + * Retrieve the unencrypted body of the message. + * + * @return body of the message, or null if the message has either not been + * decrypted yet or if the hash is not correct + */ + public byte[] getUnencryptedData() { + return _unencryptedData; + } + /** + * Populate the message body with data. This does not automatically encrypt + * yet. + * + */ + public void setUnencryptedData(byte[] data) { _unencryptedData = data; } + public byte[] getEncryptedData() { return _encryptedData; } + public void setEncryptedData(byte[] data) { _encryptedData = data; } + + public int getSize() { + if (_unencryptedData != null) + return _unencryptedData.length; + else if (_encryptedData != null) + return _encryptedData.length; + else + return 0; + } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + int size = (int)DataHelper.readLong(in, 4); + _encryptedData = new byte[size]; + int read = read(in, _encryptedData); + if (read != size) + throw new DataFormatException("Incorrect number of bytes read in the payload structure"); + _log.debug("read payload: " + read + " bytes"); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_encryptedData == null) + throw new DataFormatException("Not yet encrypted. Please set the encrypted data"); + DataHelper.writeLong(out, 4, _encryptedData.length); + out.write(_encryptedData); + _log.debug("wrote payload: " + _encryptedData.length); + } + + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof Payload) ) + return false; + Payload p = (Payload)object; + return DataHelper.eq(getUnencryptedData(), p.getUnencryptedData()) && + DataHelper.eq(getEncryptedData(), p.getEncryptedData()); + } + + public int hashCode() { + return DataHelper.hashCode(getUnencryptedData()); + } + + public String toString() { + StringBuffer buf = new StringBuffer(128); + buf.append("[Payload: "); + if (getUnencryptedData() != null) + buf.append("\n\tData: ").append(DataHelper.toString(getUnencryptedData(), 16)); + else + buf.append("\n\tData: *encrypted* = [").append(DataHelper.toString(getEncryptedData(), 16)).append("]"); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/PrivateKey.java b/core/java/src/net/i2p/data/PrivateKey.java new file mode 100644 index 000000000..f908fe49f --- /dev/null +++ b/core/java/src/net/i2p/data/PrivateKey.java @@ -0,0 +1,76 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Defines the PrivateKey as defined by the I2P data structure spec. + * A private key is 256byte Integer. The private key represents only the + * exponent, not the primes, which are constant and defined in the crypto spec. + * + * @author jrandom + */ +public class PrivateKey extends DataStructureImpl { + private final static Log _log = new Log(PrivateKey.class); + private byte[] _data; + + public final static int KEYSIZE_BYTES = 256; + + public PrivateKey() { setData(null); } + + public byte[] getData() { return _data; } + public void setData(byte[] data) { _data = data; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _data = new byte[KEYSIZE_BYTES]; + int read = read(in, _data); + if (read != KEYSIZE_BYTES) + throw new DataFormatException("Not enough bytes to read the private key"); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_data == null) + throw new DataFormatException("No data in the private key to write out"); + if (_data.length != KEYSIZE_BYTES) + throw new DataFormatException("Invalid size of data in the private key [" + _data.length + "]"); + out.write(_data); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof PrivateKey)) + return false; + return DataHelper.eq(_data, ((PrivateKey)obj)._data); + } + + public int hashCode() { + return DataHelper.hashCode(_data); + } + + public String toString() { + StringBuffer buf = new StringBuffer(64); + buf.append("[PrivateKey: "); + if (_data == null) { + buf.append("null key"); + } else { + buf.append("size: ").append(_data.length); + int len = 32; + if (len > _data.length) + len = _data.length; + buf.append(" first ").append(len).append(" bytes: "); + buf.append(DataHelper.toString(_data, len)); + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/PublicKey.java b/core/java/src/net/i2p/data/PublicKey.java new file mode 100644 index 000000000..bde5f7922 --- /dev/null +++ b/core/java/src/net/i2p/data/PublicKey.java @@ -0,0 +1,75 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Defines the PublicKey as defined by the I2P data structure spec. + * A public key is 256byte Integer. The public key represents only the + * exponent, not the primes, which are constant and defined in the crypto spec. + * + * @author jrandom + */ +public class PublicKey extends DataStructureImpl { + private final static Log _log = new Log(PublicKey.class); + private byte[] _data; + + public final static int KEYSIZE_BYTES = 256; + + public PublicKey() { setData(null); } + + public byte[] getData() { return _data; } + public void setData(byte[] data) { _data = data; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _data = new byte[KEYSIZE_BYTES]; + int read = read(in, _data); + if (read != KEYSIZE_BYTES) + throw new DataFormatException("Not enough bytes to read the public key"); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_data == null) + throw new DataFormatException("No data in the public key to write out"); + if (_data.length != KEYSIZE_BYTES) + throw new DataFormatException("Invalid size of data in the public key"); + out.write(_data); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof PublicKey)) + return false; + return DataHelper.eq(_data, ((PublicKey)obj)._data); + } + + public int hashCode() { + return DataHelper.hashCode(_data); + } + public String toString() { + StringBuffer buf = new StringBuffer(64); + buf.append("[PublicKey: "); + if (_data == null) { + buf.append("null key"); + } else { + buf.append("size: ").append(_data.length); + int len = 32; + if (len > _data.length) + len = _data.length; + buf.append(" first ").append(len).append(" bytes: "); + buf.append(DataHelper.toString(_data, len)); + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/RouterAddress.java b/core/java/src/net/i2p/data/RouterAddress.java new file mode 100644 index 000000000..f5ab3dfff --- /dev/null +++ b/core/java/src/net/i2p/data/RouterAddress.java @@ -0,0 +1,133 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import java.util.Properties; +import java.util.Iterator; +import java.util.Date; + +import net.i2p.util.Log; + +/** + * Defines a method of communicating with a router + * + * @author jrandom + */ +public class RouterAddress extends DataStructureImpl { + private final static Log _log = new Log(RouterAddress.class); + private int _cost; + private Date _expiration; + private String _transportStyle; + private Properties _options; + + public RouterAddress() { + setCost(-1); + setExpiration(null); + setTransportStyle(null); + setOptions(null); + } + + /** + * Retrieve the weighted cost of this address, relative to other methods of + * contacting this router. 0 means free and 255 means really expensive. + * No value above 255 is allowed. + * + */ + public int getCost() { return _cost; } + /** + * Configure the weighted cost of using the address. + * No value above 255 is allowed. + * + */ + public void setCost(int cost) { _cost = cost; } + /** + * Retrieve the date after which the address should not be used. If this + * is null, then the address never expires. + * + */ + public Date getExpiration() { return _expiration; } + /** + * Configure the expiration date of the address (null for no expiration) + * + */ + public void setExpiration(Date expiration) { _expiration = expiration; } + /** + * Retrieve the type of transport that must be used to communicate on this address. + * + */ + public String getTransportStyle() { return _transportStyle; } + /** + * Configure the type of transport that must be used to communicate on this address + * + */ + public void setTransportStyle(String transportStyle) { _transportStyle = transportStyle; } + /** + * Retrieve the transport specific options necessary for communication + * + */ + public Properties getOptions() { return _options; } + /** + * Specify the transport specific options necessary for communication + * + */ + public void setOptions(Properties options) { _options = options; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _cost = (int)DataHelper.readLong(in, 1); + _expiration = DataHelper.readDate(in); + _transportStyle = DataHelper.readString(in); + _options = DataHelper.readProperties(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if ( (_cost < 0) || (_transportStyle == null) || (_options == null) ) + throw new DataFormatException("Not enough data to write a router address"); + DataHelper.writeLong(out, 1, _cost); + DataHelper.writeDate(out, _expiration); + DataHelper.writeString(out, _transportStyle); + DataHelper.writeProperties(out, _options); + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof RouterAddress) ) + return false; + RouterAddress addr = (RouterAddress)object; + return DataHelper.eq(getCost(), addr.getCost()) && + DataHelper.eq(getExpiration(), addr.getExpiration()) && + DataHelper.eq(getOptions(), addr.getOptions()) && + DataHelper.eq(getTransportStyle(), addr.getTransportStyle()); + } + + public int hashCode() { + return getCost() + + DataHelper.hashCode(getTransportStyle()) + + DataHelper.hashCode(getExpiration()) + + DataHelper.hashCode(getOptions()); + } + + public String toString() { + StringBuffer buf = new StringBuffer(64); + buf.append("[RouterAddress: "); + buf.append("\n\tTransportStyle: ").append(getTransportStyle()); + buf.append("\n\tCost: ").append(getCost()); + buf.append("\n\tExpiration: ").append(getExpiration()); + buf.append("\n\tOptions: #: ").append(getOptions().size()); + for (Iterator iter = getOptions().keySet().iterator(); iter.hasNext();) { + String key = (String)iter.next(); + String val = getOptions().getProperty(key); + buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]"); + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/RouterIdentity.java b/core/java/src/net/i2p/data/RouterIdentity.java new file mode 100644 index 000000000..ed113aa8a --- /dev/null +++ b/core/java/src/net/i2p/data/RouterIdentity.java @@ -0,0 +1,117 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import net.i2p.crypto.SHA256Generator; +import net.i2p.util.Log; + +/** + * Defines the unique identifier of a router, including any certificate or + * public key. + * + * @author jrandom + */ +public class RouterIdentity extends DataStructureImpl { + private final static Log _log = new Log(RouterIdentity.class); + private Certificate _certificate; + private SigningPublicKey _signingKey; + private PublicKey _publicKey; + private Hash __calculatedHash; + + public RouterIdentity() { + setCertificate(null); + setSigningPublicKey(null); + setPublicKey(null); + __calculatedHash = null; + } + + public Certificate getCertificate() { return _certificate; } + public void setCertificate(Certificate cert) { + _certificate = cert; + __calculatedHash = null; + } + public PublicKey getPublicKey() { return _publicKey; } + public void setPublicKey(PublicKey key) { + _publicKey = key; + __calculatedHash = null; + } + public SigningPublicKey getSigningPublicKey() { return _signingKey; } + public void setSigningPublicKey(SigningPublicKey key) { + _signingKey = key; + __calculatedHash = null; + } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _publicKey = new PublicKey(); + _publicKey.readBytes(in); + _signingKey = new SigningPublicKey(); + _signingKey.readBytes(in); + _certificate = new Certificate(); + _certificate.readBytes(in); + __calculatedHash = null; + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if ( (_certificate == null) || (_publicKey == null) || (_signingKey == null) ) + throw new DataFormatException("Not enough data to format the destination"); + _publicKey.writeBytes(out); + _signingKey.writeBytes(out); + _certificate.writeBytes(out); + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof RouterIdentity) ) + return false; + RouterIdentity ident = (RouterIdentity)object; + return DataHelper.eq(getCertificate(), ident.getCertificate()) && + DataHelper.eq(getSigningPublicKey(), ident.getSigningPublicKey()) && + DataHelper.eq(getPublicKey(), ident.getPublicKey()); + } + + public int hashCode() { + return DataHelper.hashCode(getCertificate()) + + DataHelper.hashCode(getSigningPublicKey()) + + DataHelper.hashCode(getPublicKey()); + } + public String toString() { + StringBuffer buf = new StringBuffer(64); + buf.append("[RouterIdentity: "); + buf.append("\n\tHash: ").append(getHash().toBase64()); + buf.append("\n\tCertificate: ").append(getCertificate()); + buf.append("\n\tPublicKey: ").append(getPublicKey()); + buf.append("\n\tSigningPublicKey: ").append(getSigningPublicKey()); + buf.append("]"); + return buf.toString(); + } + + public Hash calculateHash() { return getHash(); } + + public Hash getHash() { + if (__calculatedHash != null) { + //_log.info("Returning cached ident hash"); + return __calculatedHash; + } + byte identBytes[] = null; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeBytes(baos); + identBytes = baos.toByteArray(); + } catch (Throwable t) { + _log.error("Error writing out hash"); + return null; + } + __calculatedHash = SHA256Generator.getInstance().calculateHash(identBytes); + return __calculatedHash; + } +} diff --git a/core/java/src/net/i2p/data/RouterInfo.java b/core/java/src/net/i2p/data/RouterInfo.java new file mode 100644 index 000000000..49e990a2e --- /dev/null +++ b/core/java/src/net/i2p/data/RouterInfo.java @@ -0,0 +1,432 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Properties; +import java.util.List; +import java.util.Date; + +import net.i2p.util.Log; +import net.i2p.util.OrderedProperties; +import net.i2p.util.Clock; +import net.i2p.crypto.DSAEngine; +import net.i2p.crypto.SHA256Generator; + +/** + * Defines the data that a router either publishes to the global routing table or + * provides to trusted peers. + * + * @author jrandom + */ +public class RouterInfo extends DataStructureImpl { + private final static Log _log = new Log(RouterInfo.class); + private RouterIdentity _identity; + private volatile long _published; + private Set _addresses; + private Set _peers; + private Properties _options; + private volatile Signature _signature; + private volatile Hash _currentRoutingKey; + private volatile byte _routingKeyGenMod[]; + private volatile boolean _validated; + private volatile boolean _isValid; + private volatile String _stringified; + private volatile byte _byteified[]; + + public RouterInfo() { + setIdentity(null); + setPublished(0); + _addresses = new HashSet(); + _peers = new HashSet(); + _options = new OrderedProperties(); + setSignature(null); + _validated = false; + _isValid = false; + _currentRoutingKey = null; + _stringified = null; + _byteified = null; + } + + public RouterInfo(RouterInfo old) { + this(); + setIdentity(old.getIdentity()); + setPublished(old.getPublished()); + setAddresses(old.getAddresses()); + setPeers(old.getPeers()); + setOptions(old.getOptions()); + setSignature(old.getSignature()); + } + + private void resetCache() { + _stringified = null; + _byteified = null; + } + + /** + * Retrieve the identity of the router represented + * + */ + public RouterIdentity getIdentity() { return _identity; } + /** + * Configure the identity of the router represented + * + */ + public void setIdentity(RouterIdentity ident) { + _identity = ident; + resetCache(); + } + /** + * Retrieve the approximate date on which the info was published + * (essentially a version number for the routerInfo structure, except that + * it also contains freshness information - whether or not the router is + * currently publishing its information). This should be used to help expire + * old routerInfo structures + * + */ + public long getPublished() { return _published; } + /** + * Date on which it was published, in milliseconds since Midnight GMT on Jan 01, 1970 + * + */ + public void setPublished(long published) { _published = published; } + /** + * Retrieve the set of RouterAddress structures at which this + * router can be contacted. + * + */ + public Set getAddresses() { + synchronized (_addresses) { + return new HashSet(_addresses); + } + } + /** + * Specify a set of RouterAddress structures at which this router + * can be contacted. + * + */ + public void setAddresses(Set addresses) { + synchronized (_addresses) { + _addresses.clear(); + if (addresses != null) + _addresses.addAll(addresses); + } + resetCache(); + } + /** + * Retrieve a set of SHA-256 hashes of RouterIdentities from rotuers + * this router can be reached through. + * + */ + public Set getPeers() { return _peers; } + /** + * Specify a set of SHA-256 hashes of RouterIdentities from rotuers + * this router can be reached through. + * + */ + public void setPeers(Set peers) { + synchronized (_peers) { + _peers.clear(); + if (peers != null) + _peers.addAll(peers); + } + resetCache(); + } + /** + * Retrieve a set of options or statistics that the router can expose + * + */ + public Properties getOptions() { + if (_options == null) return new Properties(); + synchronized (_options) { + return (Properties)_options.clone(); + } + } + /** + * Configure a set of options or statistics that the router can expose + * + */ + public void setOptions(Properties options) { + synchronized (_options) { + _options.clear(); + if (options != null) { + for (Iterator iter = options.keySet().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + if (name == null) continue; + String val = options.getProperty(name); + if (val == null) continue; + _options.setProperty(name, val); + } + } + } + resetCache(); + } + /** + * Retrieve the proof that the identity stands behind the info here + * + */ + public Signature getSignature() { return _signature; } + /** + * Configure the proof that the entity stands behind the info here + * + */ + public void setSignature(Signature signature) { + _signature = signature; + resetCache(); + } + + /** + * Sign the structure using the supplied signing key + * + */ + public synchronized void sign(SigningPrivateKey key) throws DataFormatException { + byte[] bytes = getBytes(); + if (bytes == null) throw new DataFormatException("Not enough data to sign"); + // now sign with the key + Signature sig = DSAEngine.getInstance().sign(bytes, key); + setSignature(sig); + //_log.debug("Signed " + SHA256Generator.getInstance().calculateHash(bytes).toBase64() + " with " + key); + //_log.debug("verify ok? " + DSAEngine.getInstance().verifySignature(sig, bytes, getIdentity().getSigningPublicKey())); + //_log.debug("Signed data: \n" + Base64.encode(bytes)); + //_log.debug("Signature: " + getSignature()); + + resetCache(); + } + + private byte[] getBytes() throws DataFormatException { + if (_byteified != null) return _byteified; + if (_identity == null) throw new IllegalStateException("Router identity isn't set? wtf!"); + if (_addresses == null) throw new IllegalStateException("Router addressess isn't set? wtf!"); + if (_peers == null) throw new IllegalStateException("Router peers isn't set? wtf!"); + if (_options == null) throw new IllegalStateException("Router options isn't set? wtf!"); + + long before = Clock.getInstance().now(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + _identity.writeBytes(out); + DataHelper.writeDate(out, new Date(_published)); + DataHelper.writeLong(out, 1, _addresses.size()); + List addresses = DataHelper.sortStructures(_addresses); + for (Iterator iter = addresses.iterator(); iter.hasNext(); ) { + RouterAddress addr = (RouterAddress)iter.next(); + addr.writeBytes(out); + } + DataHelper.writeLong(out, 1, _peers.size()); + List peers = DataHelper.sortStructures(_peers); + for (Iterator iter = peers.iterator(); iter.hasNext(); ) { + Hash peerHash = (Hash)iter.next(); + peerHash.writeBytes(out); + } + DataHelper.writeProperties(out, _options); + } catch (IOException ioe) { + throw new DataFormatException("IO Error getting bytes", ioe); + } + byte data[] = out.toByteArray(); + long after = Clock.getInstance().now(); + _log.debug("getBytes() took " + (after-before) + "ms"); + _byteified = data; + return data; + } + + /** + * Determine whether this router info is authorized with a valid signature + * + */ + public synchronized boolean isValid() { + if (!_validated) + doValidate(); + return _isValid; + } + + /** + * Get the routing key for the structure using the current modifier in the RoutingKeyGenerator. + * This only calculates a new one when necessary though (if the generator's key modifier changes) + * + */ + public synchronized Hash getRoutingKey() { + RoutingKeyGenerator gen = RoutingKeyGenerator.getInstance(); + if ( (gen.getModData() == null) || (_routingKeyGenMod == null) || (!DataHelper.eq(gen.getModData(), _routingKeyGenMod)) ) { + setRoutingKey(gen.getRoutingKey(getIdentity().getHash())); + _routingKeyGenMod = gen.getModData(); + } + return _currentRoutingKey; + } + public void setRoutingKey(Hash key) { _currentRoutingKey = key; } + + public boolean validateRoutingKey() { + Hash identKey = getIdentity().getHash(); + Hash rk = RoutingKeyGenerator.getInstance().getRoutingKey(identKey); + if (rk.equals(getRoutingKey())) + return true; + else + return false; + } + + + /** + * Determine whether the router was published recently (within the given age milliseconds). + * The age should be large enough to take into consideration any clock fudge factor, so + * values such as 1 or 2 hours are probably reasonable. + * + * @param maxAgeMs milliseconds between the current time and publish date to check + * @return true if it was published recently, false otherwise + */ + public boolean isCurrent(long maxAgeMs) { + long earliestExpire = Clock.getInstance().now() - maxAgeMs; + if (getPublished() < earliestExpire) { + return false; + } else { + return true; + } + } + + /** + * Actually validate the signature + */ + private synchronized void doValidate() { + _validated = true; + if (getSignature() == null) { + _log.error("Signature is null"); + _isValid = false; + return; + } + byte data[] = null; + try { + data = getBytes(); + } catch (DataFormatException dfe) { + _log.error("Error validating", dfe); + _isValid = false; + return; + } + if (data == null) { + _log.error("Data could not be loaded"); + _isValid = false; + return; + } + _isValid = DSAEngine.getInstance().verifySignature(_signature, data, _identity.getSigningPublicKey()); + if (!_isValid) { + _log.error("Invalid [" + SHA256Generator.getInstance().calculateHash(data).toBase64() + "] w/ signing key: " + _identity.getSigningPublicKey(), new Exception("Signature failed")); + _log.debug("Failed data: \n" + Base64.encode(data)); + _log.debug("Signature: " + getSignature()); + } + } + + public synchronized void readBytes(InputStream in) throws DataFormatException, IOException { + _identity = new RouterIdentity(); + _identity.readBytes(in); + _published = DataHelper.readDate(in).getTime(); + int numAddresses = (int)DataHelper.readLong(in, 1); + for (int i = 0; i < numAddresses; i++) { + RouterAddress address = new RouterAddress(); + address.readBytes(in); + _addresses.add(address); + } + int numPeers = (int)DataHelper.readLong(in, 1); + for (int i = 0; i < numPeers; i++) { + Hash peerIdentityHash = new Hash(); + peerIdentityHash.readBytes(in); + _peers.add(peerIdentityHash); + } + _options = DataHelper.readProperties(in); + _signature = new Signature(); + _signature.readBytes(in); + + resetCache(); + + //_log.debug("Read routerInfo: " + toString()); + } + + public synchronized void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_identity == null) + throw new DataFormatException("Missing identity"); + if (_published <= 0) + throw new DataFormatException("Invalid published date: " + _published); + if (_signature == null) + throw new DataFormatException("Signature is null"); + //if (!isValid()) + // throw new DataFormatException("Data is not valid"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + _identity.writeBytes(baos); + DataHelper.writeDate(baos, new Date(_published)); + DataHelper.writeLong(baos, 1, _addresses.size()); + for (Iterator iter = _addresses.iterator(); iter.hasNext(); ) { + RouterAddress addr = (RouterAddress)iter.next(); + addr.writeBytes(baos); + } + DataHelper.writeLong(baos, 1, _peers.size()); + for (Iterator iter = _peers.iterator(); iter.hasNext(); ) { + Hash peerHash = (Hash)iter.next(); + peerHash.writeBytes(baos); + } + DataHelper.writeProperties(baos, _options); + _signature.writeBytes(baos); + + byte data[] = baos.toByteArray(); + //_log.debug("Writing routerInfo [len=" + data.length + "]: " + toString()); + out.write(data); + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof RouterInfo) ) + return false; + RouterInfo info = (RouterInfo)object; + return DataHelper.eq(getAddresses(), info.getAddresses()) && + DataHelper.eq(getIdentity(), info.getIdentity()) && + DataHelper.eq(getOptions(), info.getOptions()) && + DataHelper.eq(getPeers(), info.getPeers()) && + DataHelper.eq(getSignature(), info.getSignature()) && + DataHelper.eq(getPublished(), info.getPublished()); + } + + public int hashCode() { + return DataHelper.hashCode(getAddresses()) + + DataHelper.hashCode(getIdentity()) + + DataHelper.hashCode(getOptions()) + + DataHelper.hashCode(getPeers()) + + DataHelper.hashCode(getSignature()) + + (int)getPublished(); + } + + public String toString() { + if (_stringified != null) + return _stringified; + StringBuffer buf = new StringBuffer(128); + buf.append("[RouterInfo: "); + buf.append("\n\tIdentity: ").append(getIdentity()); + buf.append("\n\tSignature: ").append(getSignature()); + buf.append("\n\tPublished on: ").append(new Date(getPublished())); + buf.append("\n\tAddresses: #: ").append(getAddresses().size()); + for (Iterator iter = getAddresses().iterator(); iter.hasNext();) { + RouterAddress addr = (RouterAddress)iter.next(); + buf.append("\n\t\tAddress: ").append(addr); + } + buf.append("\n\tPeers: #: ").append(getPeers().size()); + for (Iterator iter = getPeers().iterator(); iter.hasNext();) { + Hash hash = (Hash)iter.next(); + buf.append("\n\t\tPeer hash: ").append(hash); + } + Properties options = getOptions(); + buf.append("\n\tOptions: #: ").append(options.size()); + for (Iterator iter = options.keySet().iterator(); iter.hasNext();) { + String key = (String)iter.next(); + String val = options.getProperty(key); + buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]"); + } + buf.append("]"); + _stringified = buf.toString(); + return _stringified; + } +} diff --git a/core/java/src/net/i2p/data/RoutingKeyGenerator.java b/core/java/src/net/i2p/data/RoutingKeyGenerator.java new file mode 100644 index 000000000..c4ed8e457 --- /dev/null +++ b/core/java/src/net/i2p/data/RoutingKeyGenerator.java @@ -0,0 +1,112 @@ +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 net.i2p.crypto.SHA256Generator; + +import net.i2p.util.Log; +import net.i2p.util.RandomSource; +import net.i2p.util.Clock; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.Date; +import java.text.SimpleDateFormat; + +/** + * Component to manage the munging of hashes into routing keys - given a hash, + * perform some consistent transformation against it and return the result. + * This transformation is fed by the current "mod data". + * + * Right now the mod data is the current date (GMT) as a string: "yyyyMMdd", + * and the transformation takes the original hash, appends the bytes of that mod data, + * then returns the SHA256 of that concatenation. + * + * Do we want this to simply do the XOR of the SHA256 of the current mod data and + * the key? does that provide the randomization we need? It'd save an SHA256 op. + * Bah, too much effort to think about for so little gain. Other algorithms may come + * into play layer on about making periodic updates to the routing key for data elements + * to mess with Sybil. This may be good enough though. + * + * Also - the method generateDateBasedModData() should be called after midnight GMT + * once per day to generate the correct routing keys! + * + */ +public class RoutingKeyGenerator { + private final static RoutingKeyGenerator _instance = new RoutingKeyGenerator(); + public static RoutingKeyGenerator getInstance() { return _instance; } + private final static Log _log = new Log(RoutingKeyGenerator.class); + private byte _currentModData[]; + + private final static Calendar _cal = GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT")); + private final static SimpleDateFormat _fmt = new SimpleDateFormat("yyyyMMdd"); + + public byte[] getModData() { return _currentModData; } + public void setModData(byte modData[]) { + _currentModData = modData; + } + + /** + * Update the current modifier data with some bytes derived from the current + * date (yyyyMMdd in GMT) + * + */ + public void generateDateBasedModData() { + Date today = null; + synchronized (_cal) { + _cal.setTime(new Date(Clock.getInstance().now())); + _cal.set(Calendar.HOUR_OF_DAY, 0); + _cal.set(Calendar.MINUTE, 0); + _cal.set(Calendar.SECOND, 0); + _cal.set(Calendar.MILLISECOND, 0); + today = _cal.getTime(); + } + byte mod[] = null; + synchronized (_fmt) { + mod = _fmt.format(today).getBytes(); + } + _log.info("Routing modifier generated: " + new String(mod)); + setModData(mod); + } + + /** + * Generate a modified (yet consistent) hash from the origKey by generating the + * SHA256 of the targetKey with the current modData appended to it, *then* + * + * This makes Sybil's job a lot harder, as she needs to essentially take over the + * whole keyspace. + * + * @throws IllegalArgumentException if origKey is null + */ + public Hash getRoutingKey(Hash origKey) { + if (origKey == null) + throw new IllegalArgumentException("Original key is null"); + if (_currentModData == null) generateDateBasedModData(); + byte modVal[] = new byte[Hash.HASH_LENGTH+_currentModData.length]; + System.arraycopy(origKey.getData(), 0, modVal, 0, Hash.HASH_LENGTH); + System.arraycopy(_currentModData, 0, modVal, Hash.HASH_LENGTH, _currentModData.length); + return SHA256Generator.getInstance().calculateHash(modVal); + } + + public static void main(String args[]) { + Hash k1 = new Hash(); + byte k1d[] = new byte[Hash.HASH_LENGTH]; + RandomSource.getInstance().nextBytes(k1d); + k1.setData(k1d); + + for (int i = 0; i < 10; i++) { + System.out.println("K1: " + k1); + Hash k1m = RoutingKeyGenerator.getInstance().getRoutingKey(k1); + System.out.println("MOD: " + new String(RoutingKeyGenerator.getInstance().getModData())); + System.out.println("K1M: " + k1m); + } + try { Thread.sleep(2000); } catch (Throwable t) {} + } +} diff --git a/core/java/src/net/i2p/data/SessionKey.java b/core/java/src/net/i2p/data/SessionKey.java new file mode 100644 index 000000000..3928b5661 --- /dev/null +++ b/core/java/src/net/i2p/data/SessionKey.java @@ -0,0 +1,75 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Defines the SessionKey as defined by the I2P data structure spec. + * A session key is 32byte Integer. + * + * @author jrandom + */ +public class SessionKey extends DataStructureImpl { + private final static Log _log = new Log(SessionKey.class); + private byte[] _data; + + public final static int KEYSIZE_BYTES = 32; + + public SessionKey() { setData(null); } + + public byte[] getData() { return _data; } + public void setData(byte[] data) { _data = data; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _data = new byte[KEYSIZE_BYTES]; + int read = read(in, _data); + if (read != KEYSIZE_BYTES) + throw new DataFormatException("Not enough bytes to read the session key"); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_data == null) + throw new DataFormatException("No data in the session key to write out"); + if (_data.length != KEYSIZE_BYTES) + throw new DataFormatException("Invalid size of data in the private key"); + out.write(_data); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof SessionKey)) + return false; + return DataHelper.eq(_data, ((SessionKey)obj)._data); + } + + public int hashCode() { + return DataHelper.hashCode(_data); + } + + public String toString() { + StringBuffer buf = new StringBuffer(64); + buf.append("[SessionKey: "); + if (_data == null) { + buf.append("null key"); + } else { + buf.append("size: ").append(_data.length); + int len = 32; + if (len > _data.length) + len = _data.length; + buf.append(" first ").append(len).append(" bytes: "); + buf.append(DataHelper.toString(_data, len)); + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/SessionTag.java b/core/java/src/net/i2p/data/SessionTag.java new file mode 100644 index 000000000..3e61c8afa --- /dev/null +++ b/core/java/src/net/i2p/data/SessionTag.java @@ -0,0 +1,34 @@ +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 net.i2p.util.RandomSource; + +public class SessionTag extends ByteArray { + public final static int BYTE_LENGTH = 32; + public SessionTag() { super(); } + public SessionTag(boolean create) { + super(); + if (create) { + byte buf[] = new byte[BYTE_LENGTH]; + RandomSource.getInstance().nextBytes(buf); + setData(buf); + } + } + public SessionTag(byte val[]) { + super(); + setData(val); + } + + public void setData(byte val[]) throws IllegalArgumentException { + if (val == null) super.setData(null); + if (val.length != BYTE_LENGTH) throw new IllegalArgumentException("SessionTags must be " + BYTE_LENGTH + " bytes"); + super.setData(val); + } +} diff --git a/core/java/src/net/i2p/data/Signature.java b/core/java/src/net/i2p/data/Signature.java new file mode 100644 index 000000000..1a8aeee7b --- /dev/null +++ b/core/java/src/net/i2p/data/Signature.java @@ -0,0 +1,81 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Defines the signature as defined by the I2P data structure spec. + * A signature is a 40byte Integer verifying the authenticity of some data + * using the algorithm defined in the crypto spec. + * + * @author jrandom + */ +public class Signature extends DataStructureImpl { + private final static Log _log = new Log(Signature.class); + private byte[] _data; + + public final static int SIGNATURE_BYTES = 40; + public final static byte[] FAKE_SIGNATURE = new byte[SIGNATURE_BYTES]; + static { + for (int i = 0; i < SIGNATURE_BYTES; i++) + FAKE_SIGNATURE[i] = 0x00; + } + + public Signature() { setData(null); } + + public byte[] getData() { return _data; } + public void setData(byte[] data) { _data = data; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _data = new byte[SIGNATURE_BYTES]; + int read = read(in, _data); + if (read != SIGNATURE_BYTES) + throw new DataFormatException("Not enough bytes to read the signature"); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_data == null) + throw new DataFormatException("No data in the signature to write out"); + if (_data.length != SIGNATURE_BYTES) + throw new DataFormatException("Invalid size of data in the private key"); + out.write(_data); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof Signature)) + return false; + return DataHelper.eq(_data, ((Signature)obj)._data); + } + + public int hashCode() { + return DataHelper.hashCode(_data); + } + + public String toString() { + StringBuffer buf = new StringBuffer(64); + buf.append("[Signature: "); + if (_data == null) { + buf.append("null signature"); + } else { + buf.append("size: ").append(_data.length); + int len = 32; + if (len > _data.length) + len = _data.length; + buf.append(" first ").append(len).append(" bytes: "); + buf.append(DataHelper.toString(_data, len)); + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/SigningPrivateKey.java b/core/java/src/net/i2p/data/SigningPrivateKey.java new file mode 100644 index 000000000..b0ab6326f --- /dev/null +++ b/core/java/src/net/i2p/data/SigningPrivateKey.java @@ -0,0 +1,77 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Defines the SigningPrivateKey as defined by the I2P data structure spec. + * A private key is 256byte Integer. The private key represents only the + * exponent, not the primes, which are constant and defined in the crypto spec. + * This key varies from the PrivateKey in its usage (signing, not decrypting) + * + * @author jrandom + */ +public class SigningPrivateKey extends DataStructureImpl { + private final static Log _log = new Log(SigningPrivateKey.class); + private byte[] _data; + + public final static int KEYSIZE_BYTES = 20; + + public SigningPrivateKey() { setData(null); } + + public byte[] getData() { return _data; } + public void setData(byte[] data) { _data = data; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _data = new byte[KEYSIZE_BYTES]; + int read = read(in, _data); + if (read != KEYSIZE_BYTES) + throw new DataFormatException("Not enough bytes to read the private key"); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_data == null) + throw new DataFormatException("No data in the private key to write out"); + if (_data.length != KEYSIZE_BYTES) + throw new DataFormatException("Invalid size of data in the private key"); + out.write(_data); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof SigningPrivateKey)) + return false; + return DataHelper.eq(_data, ((SigningPrivateKey)obj)._data); + } + + public int hashCode() { + return DataHelper.hashCode(_data); + } + + public String toString() { + StringBuffer buf = new StringBuffer(64); + buf.append("[SigningPrivateKey: "); + if (_data == null) { + buf.append("null key"); + } else { + buf.append("size: ").append(_data.length); + int len = 32; + if (len > _data.length) + len = _data.length; + buf.append(" first ").append(len).append(" bytes: "); + buf.append(DataHelper.toString(_data, len)); + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/SigningPublicKey.java b/core/java/src/net/i2p/data/SigningPublicKey.java new file mode 100644 index 000000000..59f182097 --- /dev/null +++ b/core/java/src/net/i2p/data/SigningPublicKey.java @@ -0,0 +1,76 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Defines the SigningPublicKey as defined by the I2P data structure spec. + * A public key is 256byte Integer. The public key represents only the + * exponent, not the primes, which are constant and defined in the crypto spec. + * This key varies from the PrivateKey in its usage (verifying signatures, not encrypting) + * + * @author jrandom + */ +public class SigningPublicKey extends DataStructureImpl { + private final static Log _log = new Log(SigningPublicKey.class); + private byte[] _data; + + public final static int KEYSIZE_BYTES = 128; + + public SigningPublicKey() { setData(null); } + + public byte[] getData() { return _data; } + public void setData(byte[] data) { _data = data; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _data = new byte[KEYSIZE_BYTES]; + int read = read(in, _data); + if (read != KEYSIZE_BYTES) + throw new DataFormatException("Not enough bytes to read the public key"); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_data == null) + throw new DataFormatException("No data in the public key to write out"); + if (_data.length != KEYSIZE_BYTES) + throw new DataFormatException("Invalid size of data in the public key"); + out.write(_data); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof SigningPublicKey)) + return false; + return DataHelper.eq(_data, ((SigningPublicKey)obj)._data); + } + + public int hashCode() { + return DataHelper.hashCode(_data); + } + public String toString() { + StringBuffer buf = new StringBuffer(64); + buf.append("[SigningPublicKey: "); + if (_data == null) { + buf.append("null key"); + } else { + buf.append("size: ").append(_data.length); + int len = 32; + if (len > _data.length) + len = _data.length; + buf.append(" first ").append(len).append(" bytes: "); + buf.append(DataHelper.toString(_data, len)); + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/TunnelId.java b/core/java/src/net/i2p/data/TunnelId.java new file mode 100644 index 000000000..4c1cc0804 --- /dev/null +++ b/core/java/src/net/i2p/data/TunnelId.java @@ -0,0 +1,74 @@ +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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +/** + * Defines the tunnel ID that messages are passed through on a set of routers. + * This is not globally unique, but must be unique on each router making up + * the tunnel (otherwise they would get confused and send messages down the + * wrong one). + * + * @author jrandom + */ +public class TunnelId extends DataStructureImpl { + private final static Log _log = new Log(TunnelId.class); + private long _tunnelId; + private int _type; + + public final static int TYPE_UNSPECIFIED = 0; + public final static int TYPE_INBOUND = 1; + public final static int TYPE_OUTBOUND = 2; + public final static int TYPE_PARTICIPANT = 3; + + public TunnelId() { + setTunnelId(-1); + setType(TYPE_UNSPECIFIED); + } + + public long getTunnelId() { return _tunnelId; } + public void setTunnelId(long id) { _tunnelId = id; } + + /** + * contains the metadata for the tunnel - is it inbound, outbound, or a participant? + * This data point is kept in memory only and is useful for the router. + * + * @return type of tunnel (per constants TYPE_UNSPECIFIED, TYPE_INBOUND, TYPE_OUTBOUND, TYPE_PARTICIPANT) + */ + public int getType() { return _type; } + public void setType(int type) { _type = type; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _tunnelId = DataHelper.readLong(in, 4); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_tunnelId < 0) throw new DataFormatException("Invalid tunnel ID: " + _tunnelId); + DataHelper.writeLong(out, 4, _tunnelId); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof TunnelId)) + return false; + return getTunnelId() == ((TunnelId)obj).getTunnelId(); + } + + public int hashCode() { + return (int)getTunnelId(); + } + + public String toString() { + return "[TunnelID: " + getTunnelId() + "]"; + } +} diff --git a/core/java/src/net/i2p/data/UnsignedInteger.java b/core/java/src/net/i2p/data/UnsignedInteger.java new file mode 100644 index 000000000..ab75e2055 --- /dev/null +++ b/core/java/src/net/i2p/data/UnsignedInteger.java @@ -0,0 +1,174 @@ +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 net.i2p.util.Log; + +import java.math.BigInteger; + +/** + * Manage an arbitrarily large unsigned integer, using the first bit and first byte + * as the most significant one. Also allows the exporting to byte arrays with whatever + * padding is requested. + * + * WARNING: Range is currently limited to 0 through 2^63-1, due to Java's two's complement + * format. Fix when we need it. + * + * @author jrandom + */ +public class UnsignedInteger { + private final static Log _log = new Log(UnsignedInteger.class); + private byte[] _data; + private long _value; + + /** + * Construct the integer from the bytes given, making the value accessible + * immediately. + * + * @param data unsigned number in network byte order (first bit, first byte + * is the most significant) + */ + public UnsignedInteger(byte[] data) { + // strip off excess bytes + int start = 0; + for (int i = 0; i < data.length; i++) { + if (data[i] == 0) { + start++; + } else { + break; + } + } + _data = new byte[data.length - start]; + for (int i = 0; i < _data.length; i++) + _data[i] = data[i+start]; + // done stripping excess bytes, now calc + _value = calculateValue(_data); + } + + /** + * Construct the integer with the java number given, making the bytes + * available immediately. + * + * @param value number to represent + */ + public UnsignedInteger(long value) { + _value = value; + _data = calculateBytes(value); + } + + /** + * Calculate the value of the array of bytes, treating it as an unsigned integer + * with the most significant bit and byte first + * + */ + private static long calculateValue(byte[] data) { + if (data == null) { + _log.error("Null data to be calculating for", new Exception("Argh")); + return 0; + } else if (data.length == 0) { + return 0; + } + BigInteger bi = new BigInteger(1, data); + return bi.longValue(); + } + + /** + * hexify the byte array + * + */ + private final static String toString(byte[] val) { + return "0x" + DataHelper.toString(val, val.length); + } + + /** + * Calculate the bytes as an unsigned integer with the most significant bit and byte in the first position + */ + private static byte[] calculateBytes(long value) { + BigInteger bi = new BigInteger(""+value); + byte buf[] = bi.toByteArray(); + if ( (buf == null) || (buf.length <= 0) ) + throw new IllegalArgumentException("Value [" + value + "] cannot be transformed"); + int trim = 0; + while ( (trim < buf.length) && (buf[trim] == 0x00) ) + trim++; + byte rv[] = new byte[buf.length - trim]; + System.arraycopy(buf, trim, rv, 0, rv.length); + return rv; + } + + /** + * Get the unsigned bytes, most significant bit and bytes first, without any padding + * + */ + public byte[] getBytes() { return _data; } + /** + * Get the unsigned bytes, most significant bit and bytes first, zero padded to the + * specified number of bytes + * + * @throws IllegalArgumentException if numBytes < necessary number of bytes + */ + public byte[] getBytes(int numBytes) throws IllegalArgumentException { + if ( (_data == null) || (numBytes < _data.length) ) + throw new IllegalArgumentException("Value (" +_value+") is greater than the requested number of bytes ("+numBytes+")"); + + byte[] data = new byte[numBytes]; + System.arraycopy(_data, 0, data, numBytes - _data.length, _data.length); + return data; + } + + public BigInteger getBigInteger() { return new BigInteger(1, _data); } + public long getLong() { return _value; } + public int getInt() { return (int)_value; } + public short getShort() { return (short)_value; } + + public boolean equals(Object obj) { + if ( (obj != null) && (obj instanceof UnsignedInteger) ) { + return DataHelper.eq(_data, ((UnsignedInteger)obj)._data) && + DataHelper.eq(_value, ((UnsignedInteger)obj)._value); + } else { + return false; + } + } + + public int hashCode() { + return DataHelper.hashCode(_data) + (int)_value; + } + + public String toString() { + return "UnsignedInteger: " + getLong() + "/" + toString(getBytes()); + } + + public static void main(String args[]) { + _log.debug("Testing 1024"); + testNum(1024L); + _log.debug("Testing 1025"); + testNum(1025L); + _log.debug("Testing 2Gb-1"); + testNum(1024*1024*1024*2L-1L); + _log.debug("Testing 4Gb-1"); + testNum(1024*1024*1024*4L-1L); + _log.debug("Testing 4Gb"); + testNum(1024*1024*1024*4L); + _log.debug("Testing 4Gb+1"); + testNum(1024*1024*1024*4L+1L); + _log.debug("Testing MaxLong"); + testNum(Long.MAX_VALUE); + try { Thread.sleep(1000); } catch (Throwable t) {} + } + + private static void testNum(long num) { + UnsignedInteger i = new UnsignedInteger(num); + _log.debug(num + " turned into an unsigned integer: " + i + " (" + i.getLong() + "/" + toString(i.getBytes()) + ")"); + _log.debug(num + " turned into an BigInteger: " + i.getBigInteger()); + byte[] val = i.getBytes(); + UnsignedInteger val2 = new UnsignedInteger(val); + _log.debug(num + " turned into a byte array and back again: " + val2 + " (" + val2.getLong() + "/" + toString(val2.getBytes()) + ")"); + _log.debug(num + " As an 8 byte array: " + toString(val2.getBytes(8))); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/AbuseReason.java b/core/java/src/net/i2p/data/i2cp/AbuseReason.java new file mode 100644 index 000000000..82966fd60 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/AbuseReason.java @@ -0,0 +1,56 @@ +package net.i2p.data.i2cp; +/* + * 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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; + +/** + * Defines the structure for why abuse was reported either by the client to + * the router or by the router to the client + * + * @author jrandom + */ +public class AbuseReason extends DataStructureImpl { + private final static Log _log = new Log(AbuseReason.class); + private String _reason; + + public AbuseReason() { setReason(null); } + + public String getReason() { return _reason; } + public void setReason(String reason) { _reason = reason; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _reason = DataHelper.readString(in); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_reason == null) throw new DataFormatException("Invalid abuse reason"); + DataHelper.writeString(out, _reason); + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof AbuseReason) ) + return false; + return DataHelper.eq(getReason(), ((AbuseReason)object).getReason()); + } + + public int hashCode() { return DataHelper.hashCode(getReason()); } + + public String toString() { + return "[AbuseReason: " + getReason() + "]"; + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/data/i2cp/AbuseSeverity.java b/core/java/src/net/i2p/data/i2cp/AbuseSeverity.java new file mode 100644 index 000000000..8d9220fac --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/AbuseSeverity.java @@ -0,0 +1,57 @@ +package net.i2p.data.i2cp; +/* + * 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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; + +/** + * Provides a severity level (larger numbers are more severe) in association with + * a client reporting abusive behavior to the router or the router reporting it + * to the client + * + * @author jrandom + */ +public class AbuseSeverity extends DataStructureImpl { + private final static Log _log = new Log(AbuseSeverity.class); + private int _severityId; + + public AbuseSeverity() { setSeverity(-1); } + + public int getSeverity() { return _severityId; } + public void setSeverity(int id) { _severityId = id; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _severityId = (int)DataHelper.readLong(in, 1); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_severityId < 0) throw new DataFormatException("Invalid abuse severity: " + _severityId); + DataHelper.writeLong(out, 1, _severityId); + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof AbuseSeverity) ) + return false; + return DataHelper.eq(getSeverity(), ((AbuseSeverity)object).getSeverity()); + } + + public int hashCode() { return getSeverity(); } + + public String toString() { + return "[AbuseSeverity: " + getSeverity() + "]"; + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/data/i2cp/CreateLeaseSetMessage.java b/core/java/src/net/i2p/data/i2cp/CreateLeaseSetMessage.java new file mode 100644 index 000000000..569974a2c --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/CreateLeaseSetMessage.java @@ -0,0 +1,106 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.LeaseSet; +import net.i2p.data.PrivateKey; +import net.i2p.data.SigningPrivateKey; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when authorizing + * the LeaseSet + * + * @author jrandom + */ +public class CreateLeaseSetMessage extends I2CPMessageImpl { + private final static Log _log = new Log(CreateLeaseSetMessage.class); + public final static int MESSAGE_TYPE = 4; + private SessionId _sessionId; + private LeaseSet _leaseSet; + private SigningPrivateKey _signingPrivateKey; + private PrivateKey _privateKey; + + public CreateLeaseSetMessage() { + setSessionId(null); + setLeaseSet(null); + setSigningPrivateKey(null); + setPrivateKey(null); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + public SigningPrivateKey getSigningPrivateKey() { return _signingPrivateKey; } + public void setSigningPrivateKey(SigningPrivateKey key) { _signingPrivateKey = key; } + public PrivateKey getPrivateKey() { return _privateKey; } + public void setPrivateKey(PrivateKey privateKey) { _privateKey = privateKey; } + public LeaseSet getLeaseSet() { return _leaseSet; } + public void setLeaseSet(LeaseSet leaseSet) { _leaseSet = leaseSet; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + _signingPrivateKey = new SigningPrivateKey(); + _signingPrivateKey.readBytes(in); + _privateKey = new PrivateKey(); + _privateKey.readBytes(in); + _leaseSet = new LeaseSet(); + _leaseSet.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error reading the CreateLeaseSetMessage", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if ( (_sessionId == null) || (_signingPrivateKey == null) || (_privateKey == null) || (_leaseSet == null) ) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(512); + try { + _sessionId.writeBytes(os); + _signingPrivateKey.writeBytes(os); + _privateKey.writeBytes(os); + _leaseSet.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof CreateLeaseSetMessage) ) { + CreateLeaseSetMessage msg = (CreateLeaseSetMessage)object; + return DataHelper.eq(getSessionId(),msg.getSessionId()) && + DataHelper.eq(getSigningPrivateKey(),msg.getSigningPrivateKey()) && + DataHelper.eq(getPrivateKey(), msg.getPrivateKey()) && + DataHelper.eq(getLeaseSet(),msg.getLeaseSet()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[CreateLeaseSetMessage: "); + buf.append("\n\tLeaseSet: ").append(getLeaseSet()); + buf.append("\n\tSigningPrivateKey: ").append(getSigningPrivateKey()); + buf.append("\n\tPrivateKey: ").append(getPrivateKey()); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("]"); + return buf.toString(); + } +} \ No newline at end of file diff --git a/core/java/src/net/i2p/data/i2cp/CreateSessionMessage.java b/core/java/src/net/i2p/data/i2cp/CreateSessionMessage.java new file mode 100644 index 000000000..126eef6f9 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/CreateSessionMessage.java @@ -0,0 +1,76 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when establishing a new + * session. + * + * @author jrandom + */ +public class CreateSessionMessage extends I2CPMessageImpl { + private final static Log _log = new Log(CreateSessionMessage.class); + public final static int MESSAGE_TYPE = 1; + private SessionConfig _sessionConfig; + + public CreateSessionMessage(SessionConfig config) { setSessionConfig(config); } + public CreateSessionMessage() { setSessionConfig(new SessionConfig()); } + + public SessionConfig getSessionConfig() { return _sessionConfig; } + public void setSessionConfig(SessionConfig config) { _sessionConfig = config; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + SessionConfig config = new SessionConfig(); + try { + config.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the session configuration", dfe); + } + setSessionConfig(config); + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_sessionConfig == null) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + _sessionConfig.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the session config", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof CreateSessionMessage) ) { + CreateSessionMessage msg = (CreateSessionMessage)object; + return DataHelper.eq(getSessionConfig(), msg.getSessionConfig()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[CreateSessionMessage: "); + buf.append("\n\tConfig: ").append(getSessionConfig()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/DestroySessionMessage.java b/core/java/src/net/i2p/data/i2cp/DestroySessionMessage.java new file mode 100644 index 000000000..1c27225d4 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/DestroySessionMessage.java @@ -0,0 +1,77 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when destroying + * existing session. + * + * @author jrandom + */ +public class DestroySessionMessage extends I2CPMessageImpl { + private final static Log _log = new Log(DestroySessionMessage.class); + public final static int MESSAGE_TYPE = 3; + private SessionId _sessionId; + + public DestroySessionMessage() { + setSessionId(null); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + SessionId id = new SessionId(); + try { + id.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + setSessionId(id); + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_sessionId == null) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + _sessionId.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof DestroySessionMessage) ) { + DestroySessionMessage msg = (DestroySessionMessage)object; + return DataHelper.eq(getSessionId(),msg.getSessionId()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DestroySessionMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/DisconnectMessage.java b/core/java/src/net/i2p/data/i2cp/DisconnectMessage.java new file mode 100644 index 000000000..2d4ecb564 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/DisconnectMessage.java @@ -0,0 +1,73 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when destroying + * existing session. + * + * @author jrandom + */ +public class DisconnectMessage extends I2CPMessageImpl { + private final static Log _log = new Log(DisconnectMessage.class); + public final static int MESSAGE_TYPE = 30; + private String _reason; + + public DisconnectMessage() { + setReason(null); + } + + public String getReason() { return _reason; } + public void setReason(String reason) { _reason = reason; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _reason = DataHelper.readString(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + DataHelper.writeString(os, _reason); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof DisconnectMessage) ) { + DisconnectMessage msg = (DisconnectMessage)object; + return DataHelper.eq(getReason(),msg.getReason()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DisconnectMessage: "); + buf.append("\n\tReason: ").append(getReason()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/GetDateMessage.java b/core/java/src/net/i2p/data/i2cp/GetDateMessage.java new file mode 100644 index 000000000..ecc73959a --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/GetDateMessage.java @@ -0,0 +1,52 @@ +package net.i2p.data.i2cp; +/* + * 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.io.IOException; +import java.io.InputStream; + +import net.i2p.util.Log; + +/** + * Request the other side to send us what they think the current time is + * + */ +public class GetDateMessage extends I2CPMessageImpl { + private final static Log _log = new Log(GetDateMessage.class); + public final static int MESSAGE_TYPE = 32; + + public GetDateMessage() { + super(); + } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + // noop + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + byte rv[] = new byte[0]; + return rv; + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof GetDateMessage) ) { + return true; + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[GetDateMessage]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessage.java b/core/java/src/net/i2p/data/i2cp/I2CPMessage.java new file mode 100644 index 000000000..2ef4cd9e3 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessage.java @@ -0,0 +1,64 @@ +package net.i2p.data.i2cp; +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import net.i2p.data.DataStructure; + +/** + * Defines the base functionality of API messages + * + * @author jrandom + */ +public interface I2CPMessage extends DataStructure { + /** + * Read the contents from the input stream into the current class's format. + * The stream should be the message body as defined by the client access layer + * specification after the message header (4 bytes specifying the size of the + * message, 1 byte specifying the type of the message). + * + * @param in stream to read from + * @param size number of bytes in the message payload + * @param type type of message (should equal getType()) + * @throws I2CPMessageException if the stream doesn't contain a valid message + * that this class can read. + * @throws IOException if there is a problem reading from the stream + */ + public void readMessage(InputStream in, int size, int type) throws I2CPMessageException, IOException; + /** + * Read the contents from the input stream into the current class's format. + * The stream should be the message header and body as defined by the I2CP + * specification + * + * @param in stream to read from + * @throws I2CPMessageException if the stream doesn't contain a valid message + * that this class can read. + * @throws IOException if there is a problem reading from the stream + */ + public void readMessage(InputStream in) throws I2CPMessageException, IOException; + + /** + * Write the current message to the output stream as a full message following + * the specification from the I2CP definition. + * + * @throws I2CPMessageException if the current object doesn't have sufficient data + * to write a properly formatted message. + * @throws IOException if there is a problem writing to the stream + */ + public void writeMessage(OutputStream out) throws I2CPMessageException, IOException; + + /** + * Return the unique identifier for this type of APIMessage, as specified in the + * network specification document under #ClientAccessLayerMessages + */ + public int getType(); +} diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageException.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageException.java new file mode 100644 index 000000000..3132f170d --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageException.java @@ -0,0 +1,28 @@ +package net.i2p.data.i2cp; +/* + * 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 net.i2p.I2PException; +import net.i2p.util.Log; + +/** + * Represent an error serializing or deserializing an APIMessage + * + * @author jrandom + */ +public class I2CPMessageException extends I2PException { + private final static Log _log = new Log(I2CPMessageException.class); + + public I2CPMessageException(String message, Throwable parent) { + super(message, parent); + } + public I2CPMessageException(String message) { + super(message); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java new file mode 100644 index 000000000..77813fb38 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java @@ -0,0 +1,94 @@ +package net.i2p.data.i2cp; +/* + * 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.io.InputStream; +import java.io.IOException; +import java.io.FileInputStream; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataFormatException; + +import net.i2p.util.Log; + +/** + * Handle messages from the server for the client + * + */ +public class I2CPMessageHandler { + private final static Log _log = new Log(I2CPMessageHandler.class); + + /** + * Read an I2CPMessage from the stream and return the fully populated object. + * + * @throws IOException if there is an IO problem reading from the stream + * @throws I2CPMessageException if there is a problem handling the particular + * message - if it is an unknown type or has improper formatting, etc. + */ + public static I2CPMessage readMessage(InputStream in) throws IOException, I2CPMessageException { + try { + int length = (int)DataHelper.readLong(in, 4); + if (length < 0) throw new I2CPMessageException("Invalid message length specified"); + int type = (int)DataHelper.readLong(in, 1); + I2CPMessage msg = createMessage(in, length, type); + msg.readMessage(in, length, type); + return msg; + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error reading the message", dfe); + } + } + + /** + * Yes, this is fairly ugly, but its the only place it ever happens. + * + */ + private static I2CPMessage createMessage(InputStream in, int length, int type) throws IOException, I2CPMessageException { + switch (type) { + case CreateLeaseSetMessage.MESSAGE_TYPE: + return new CreateLeaseSetMessage(); + case CreateSessionMessage.MESSAGE_TYPE: + return new CreateSessionMessage(); + case DestroySessionMessage.MESSAGE_TYPE: + return new DestroySessionMessage(); + case DisconnectMessage.MESSAGE_TYPE: + return new DisconnectMessage(); + case MessageStatusMessage.MESSAGE_TYPE: + return new MessageStatusMessage(); + case MessagePayloadMessage.MESSAGE_TYPE: + return new MessagePayloadMessage(); + case ReceiveMessageBeginMessage.MESSAGE_TYPE: + return new ReceiveMessageBeginMessage(); + case ReceiveMessageEndMessage.MESSAGE_TYPE: + return new ReceiveMessageEndMessage(); + case ReportAbuseMessage.MESSAGE_TYPE: + return new ReportAbuseMessage(); + case RequestLeaseSetMessage.MESSAGE_TYPE: + return new RequestLeaseSetMessage(); + case SendMessageMessage.MESSAGE_TYPE: + return new SendMessageMessage(); + case SessionStatusMessage.MESSAGE_TYPE: + return new SessionStatusMessage(); + case GetDateMessage.MESSAGE_TYPE: + return new GetDateMessage(); + case SetDateMessage.MESSAGE_TYPE: + return new SetDateMessage(); + default: + throw new I2CPMessageException("The type "+ type + " is an unknown I2CP message"); + } + } + + public static void main(String args[]) { + try { + I2CPMessage msg = readMessage(new FileInputStream(args[0])); + System.out.println(msg); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageImpl.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageImpl.java new file mode 100644 index 000000000..10f4b61ce --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageImpl.java @@ -0,0 +1,113 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.util.Log; + +/** + * Defines the base message implementation. + * + * @author jrandom + */ +public abstract class I2CPMessageImpl extends DataStructureImpl implements I2CPMessage { + private final static Log _log = new Log(I2CPMessageImpl.class); + public I2CPMessageImpl() {} + + /** + * Validate the type and size of the message. and then read the message + * into the data structures. + * + */ + public void readMessage(InputStream in) throws I2CPMessageException, IOException { + int length = 0; + try { + length = (int)DataHelper.readLong(in, 4); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error reading the length bytes", dfe); + } + if (length < 0) throw new I2CPMessageException("Invalid message length specified"); + int type = -1; + try { + type = (int)DataHelper.readLong(in, 1); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error reading the type byte", dfe); + } + readMessage(in, length, type); + } + + /** + * Read the body into the data structures + * + */ + public void readMessage(InputStream in, int length, int type) throws I2CPMessageException, IOException { + if (type != getType()) throw new I2CPMessageException("Invalid message type (found: " + type + " supported: " + getType() + " class: " + getClass().getName()+ ")"); + if (length < 0) throw new IOException("Negative payload size"); + + byte buf[] = new byte[length]; + int read = DataHelper.read(in, buf); + if (read != length) + throw new IOException("Not able to read enough bytes [" + read + "] read, expected [ " + length + "]"); + + ByteArrayInputStream bis = new ByteArrayInputStream(buf); + + doReadMessage(bis, length); + } + + /** + * Read in the payload part of the message (after the initial 4 byte size and 1 + * byte type) + * + */ + protected abstract void doReadMessage(InputStream buf, int size) throws I2CPMessageException, IOException; + /** + * Write out the payload part of the message (not including the 4 byte size and + * 1 byte type) + * + */ + protected abstract byte[] doWriteMessage() throws I2CPMessageException, IOException; + + /** + * Write out the full message to the stream, including the 4 byte size and 1 + * byte type header. + * + */ + public void writeMessage(OutputStream out) throws I2CPMessageException, IOException { + byte[] data = doWriteMessage(); + try { + DataHelper.writeLong(out, 4, data.length); + DataHelper.writeLong(out, 1, getType()); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to write the message length or type", dfe); + } + out.write(data); + } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + try { + readMessage(in); + } catch (I2CPMessageException ime) { + throw new DataFormatException("Error reading the message", ime); + } + } + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + try { + writeMessage(out); + } catch (I2CPMessageException ime) { + throw new DataFormatException("Error writing the message", ime); + } + } +} diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java new file mode 100644 index 000000000..d526b055a --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java @@ -0,0 +1,145 @@ +package net.i2p.data.i2cp; +/* + * 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.io.IOException; +import java.io.InputStream; + +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * The I2CPMessageReader reads an InputStream (using + * {@link I2CPMessageHandler I2CPMessageHandler}) and passes out events to a registered + * listener, where events are either messages being received, exceptions being + * thrown, or the connection being closed. Applications should use this rather + * than read from the stream themselves. + * + * @author jrandom + */ +public class I2CPMessageReader { + private final static Log _log = new Log(I2CPMessageReader.class); + private InputStream _stream; + private I2CPMessageEventListener _listener; + private I2CPMessageReaderRunner _reader; + private Thread _readerThread; + + public I2CPMessageReader(InputStream stream, I2CPMessageEventListener lsnr) { + _stream = stream; + setListener(lsnr); + _reader = new I2CPMessageReaderRunner(); + _readerThread = new I2PThread(_reader); + _readerThread.setDaemon(true); + _readerThread.setName("I2CP Reader"); + } + + public void setListener(I2CPMessageEventListener lsnr) { _listener = lsnr; } + public I2CPMessageEventListener getListener() { return _listener; } + + /** + * Instruct the reader to begin reading messages off the stream + * + */ + public void startReading() { _readerThread.start(); } + /** + * Have the already started reader pause its reading indefinitely + * + */ + public void pauseReading() { _reader.pauseRunner(); } + /** + * Resume reading after a pause + * + */ + public void resumeReading() { _reader.resumeRunner(); } + /** + * Cancel reading. + * + */ + public void stopReading() { _reader.cancelRunner(); } + + /** + * Defines the different events the reader produces while reading the stream + * + */ + public static interface I2CPMessageEventListener { + /** + * Notify the listener that a message has been received from the given + * reader + * + */ + public void messageReceived(I2CPMessageReader reader, I2CPMessage message); + /** + * Notify the listener that an exception was thrown while reading from the given + * reader + * + */ + public void readError(I2CPMessageReader reader, Exception error); + /** + * Notify the listener that the stream the given reader was running off + * closed + * + */ + public void disconnected(I2CPMessageReader reader); + } + + private class I2CPMessageReaderRunner implements Runnable { + private boolean _doRun; + private boolean _stayAlive; + public I2CPMessageReaderRunner() { + _doRun = true; + _stayAlive = true; + } + public void pauseRunner() { _doRun = false; } + public void resumeRunner() { _doRun = true; } + public void cancelRunner() { + _doRun = false; + _stayAlive = false; + if (_stream != null) { + try { + _stream.close(); + } catch (IOException ioe) { + _log.error("Error closing the stream", ioe); + } + } + _stream = null; + } + public void run() { + while (_stayAlive) { + while (_doRun) { + // do read + try { + I2CPMessage msg = I2CPMessageHandler.readMessage(_stream); + if (msg != null) { + _log.debug("Before handling the newly received message"); + _listener.messageReceived(I2CPMessageReader.this, msg); + _log.debug("After handling the newly received message"); + } + } catch (I2CPMessageException ime) { + _log.error("Error handling message", ime); + _listener.readError(I2CPMessageReader.this, ime); + cancelRunner(); + } catch (IOException ioe) { + _log.error("IO Error handling message", ioe); + _listener.disconnected(I2CPMessageReader.this); + cancelRunner(); + } catch (Throwable t) { + _log.log(Log.CRIT, "Unhandled error reading I2CP stream", t); + _listener.disconnected(I2CPMessageReader.this); + cancelRunner(); + } + } + if (!_doRun) { + // pause .5 secs when we're paused + try { Thread.sleep(500); } catch (InterruptedException ie) {} + } + } + // boom bye bye bad bwoy + } + } +} diff --git a/core/java/src/net/i2p/data/i2cp/MessageId.java b/core/java/src/net/i2p/data/i2cp/MessageId.java new file mode 100644 index 000000000..f4023aa12 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/MessageId.java @@ -0,0 +1,56 @@ +package net.i2p.data.i2cp; +/* + * 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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; + +/** + * Defines the message ID of a message delivered between a router and a client + * in a particular session. These IDs are not globally unique. + * + * @author jrandom + */ +public class MessageId extends DataStructureImpl { + private final static Log _log = new Log(MessageId.class); + private int _messageId; + + public MessageId() { setMessageId(-1); } + + public int getMessageId() { return _messageId; } + public void setMessageId(int id) { _messageId = id; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _messageId = (int)DataHelper.readLong(in, 4); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_messageId < 0) throw new DataFormatException("Invalid message ID: " + _messageId); + DataHelper.writeLong(out, 4, _messageId); + } + + public boolean equals(Object object) { + if ( (object == null) || !(object instanceof MessageId) ) + return false; + return DataHelper.eq(getMessageId(), ((MessageId)object).getMessageId()); + } + + public int hashCode() { return getMessageId(); } + + public String toString() { + return "[MessageId: " + getMessageId() + "]"; + } +} diff --git a/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java new file mode 100644 index 000000000..17f352643 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java @@ -0,0 +1,100 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Payload; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router to ask it to deliver + * a new message + * + * @author jrandom + */ +public class MessagePayloadMessage extends I2CPMessageImpl { + private final static Log _log = new Log(MessagePayloadMessage.class); + public final static int MESSAGE_TYPE = 31; + private SessionId _sessionId; + private MessageId _messageId; + private Payload _payload; + + public MessagePayloadMessage() { + setSessionId(null); + setMessageId(null); + setPayload(null); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + public MessageId getMessageId() { return _messageId; } + public void setMessageId(MessageId id) { _messageId = id; } + public Payload getPayload() { return _payload; } + public void setPayload(Payload payload) { _payload = payload; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + _messageId = new MessageId(); + _messageId.readBytes(in); + _payload = new Payload(); + _payload.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_sessionId == null) + throw new I2CPMessageException("Unable to write out the message, as the session ID has not been defined"); + if (_messageId == null) + throw new I2CPMessageException("Unable to write out the message, as the message ID has not been defined"); + if (_payload == null) + throw new I2CPMessageException("Unable to write out the message, as the payload has not been defined"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(512); + try { + _sessionId.writeBytes(os); + _messageId.writeBytes(os); + _payload.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof MessagePayloadMessage) ) { + MessagePayloadMessage msg = (MessagePayloadMessage)object; + return DataHelper.eq(getSessionId(),msg.getSessionId()) && + DataHelper.eq(getMessageId(),msg.getMessageId()) && + DataHelper.eq(getPayload(),msg.getPayload()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[MessagePayloadMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("\n\tMessageId: ").append(getMessageId()); + buf.append("\n\tPayload: ").append(getPayload()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/MessageStatusMessage.java b/core/java/src/net/i2p/data/i2cp/MessageStatusMessage.java new file mode 100644 index 000000000..2b172a6a9 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/MessageStatusMessage.java @@ -0,0 +1,128 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when destroying + * existing session. + * + * @author jrandom + */ +public class MessageStatusMessage extends I2CPMessageImpl { + private final static Log _log = new Log(SessionStatusMessage.class); + public final static int MESSAGE_TYPE = 22; + private SessionId _sessionId; + private MessageId _messageId; + private long _nonce; + private long _size; + private int _status; + + public final static int STATUS_AVAILABLE = 0; + public final static int STATUS_SEND_ACCEPTED = 1; + public final static int STATUS_SEND_BEST_EFFORT_SUCCESS = 2; + public final static int STATUS_SEND_BEST_EFFORT_FAILURE = 3; + public final static int STATUS_SEND_GUARANTEED_SUCCESS = 4; + public final static int STATUS_SEND_GUARANTEED_FAILURE = 5; + + public MessageStatusMessage() { + setSessionId(null); + setStatus(-1); + setMessageId(null); + setSize(-1); + setNonce(-1); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + public int getStatus() { return _status; } + public void setStatus(int status) { _status = status; } + public MessageId getMessageId() { return _messageId; } + public void setMessageId(MessageId id) { _messageId = id; } + public long getSize() { return _size; } + public void setSize(long size) { _size = size; } + public long getNonce() { return _nonce; } + public void setNonce(long nonce) { _nonce = nonce; } + + public static final String getStatusString(int status) { + switch (status) { + case STATUS_AVAILABLE: return "AVAILABLE "; + case STATUS_SEND_ACCEPTED: return "SEND ACCEPTED "; + case STATUS_SEND_BEST_EFFORT_SUCCESS: return "BEST EFFORT SUCCESS"; + case STATUS_SEND_BEST_EFFORT_FAILURE: return "BEST EFFORT FAILURE"; + case STATUS_SEND_GUARANTEED_SUCCESS: return "GUARANTEED SUCCESS "; + case STATUS_SEND_GUARANTEED_FAILURE: return "GUARANTEED FAILURE "; + default: return "***INVALID STATUS: " + status; + } + } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + _messageId = new MessageId(); + _messageId.readBytes(in); + _status = (int)DataHelper.readLong(in, 1); + _size = DataHelper.readLong(in, 4); + _nonce = DataHelper.readLong(in, 4); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if ( (_sessionId == null) || (_messageId == null) || (_status < 0) || (_nonce <= 0) ) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + _sessionId.writeBytes(os); + _messageId.writeBytes(os); + DataHelper.writeLong(os, 1, _status); + DataHelper.writeLong(os, 4, _size); + DataHelper.writeLong(os, 4, _nonce); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof MessageStatusMessage) ) { + MessageStatusMessage msg = (MessageStatusMessage)object; + return DataHelper.eq(getSessionId(),msg.getSessionId()) && + DataHelper.eq(getMessageId(),msg.getMessageId()) && + (getNonce() == msg.getNonce()) && + DataHelper.eq(getSize(),msg.getSize()) && + DataHelper.eq(getStatus(),msg.getStatus()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[MessageStatusMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("\n\tNonce: ").append(getNonce()); + buf.append("\n\tMessageId: ").append(getMessageId()); + buf.append("\n\tStatus: ").append(getStatusString(getStatus())); + buf.append("\n\tSize: ").append(getSize()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/ReceiveMessageBeginMessage.java b/core/java/src/net/i2p/data/i2cp/ReceiveMessageBeginMessage.java new file mode 100644 index 000000000..ce55d17d8 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/ReceiveMessageBeginMessage.java @@ -0,0 +1,85 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when asking the + * router to start sending a message to it. + * + * @author jrandom + */ +public class ReceiveMessageBeginMessage extends I2CPMessageImpl { + private final static Log _log = new Log(ReceiveMessageBeginMessage.class); + public final static int MESSAGE_TYPE = 6; + private SessionId _sessionId; + private MessageId _messageId; + + public ReceiveMessageBeginMessage() { + setSessionId(null); + setMessageId(null); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + public MessageId getMessageId() { return _messageId; } + public void setMessageId(MessageId id) { _messageId = id; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + _messageId = new MessageId(); + _messageId.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if ( (_sessionId == null) || (_messageId == null) ) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + _sessionId.writeBytes(os); + _messageId.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof ReceiveMessageBeginMessage) ) { + ReceiveMessageBeginMessage msg = (ReceiveMessageBeginMessage)object; + return DataHelper.eq(getSessionId(),msg.getSessionId()) && + DataHelper.eq(getMessageId(),msg.getMessageId()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[ReceiveMessageBeginMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("\n\tMessageId: ").append(getMessageId()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/ReceiveMessageEndMessage.java b/core/java/src/net/i2p/data/i2cp/ReceiveMessageEndMessage.java new file mode 100644 index 000000000..6721e3b09 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/ReceiveMessageEndMessage.java @@ -0,0 +1,85 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when asking the + * router to start sending a message to it. + * + * @author jrandom + */ +public class ReceiveMessageEndMessage extends I2CPMessageImpl { + private final static Log _log = new Log(ReceiveMessageEndMessage.class); + public final static int MESSAGE_TYPE = 7; + private SessionId _sessionId; + private MessageId _messageId; + + public ReceiveMessageEndMessage() { + setSessionId(null); + setMessageId(null); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + public MessageId getMessageId() { return _messageId; } + public void setMessageId(MessageId id) { _messageId = id; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + _messageId = new MessageId(); + _messageId.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if ( (_sessionId == null) || (_messageId == null) ) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + _sessionId.writeBytes(os); + _messageId.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof ReceiveMessageEndMessage) ) { + ReceiveMessageEndMessage msg = (ReceiveMessageEndMessage)object; + return DataHelper.eq(getSessionId(),msg.getSessionId()) && + DataHelper.eq(getMessageId(),msg.getMessageId()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[ReceiveMessageEndMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("\n\tMessageId: ").append(getMessageId()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/ReportAbuseMessage.java b/core/java/src/net/i2p/data/i2cp/ReportAbuseMessage.java new file mode 100644 index 000000000..fe67d19c3 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/ReportAbuseMessage.java @@ -0,0 +1,107 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when asking the + * router what its address visibility is + * + * @author jrandom + */ +public class ReportAbuseMessage extends I2CPMessageImpl { + private final static Log _log = new Log(ReportAbuseMessage.class); + public final static int MESSAGE_TYPE = 29; + private SessionId _sessionId; + private AbuseSeverity _severity; + private AbuseReason _reason; + private MessageId _messageId; + + public ReportAbuseMessage() { + setSessionId(null); + setSeverity(null); + setReason(null); + setMessageId(null); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + public AbuseSeverity getSeverity() { return _severity; } + public void setSeverity(AbuseSeverity severity) { _severity = severity; } + public AbuseReason getReason() { return _reason; } + public void setReason(AbuseReason reason) { _reason = reason; } + public MessageId getMessageId() { return _messageId; } + public void setMessageId(MessageId id) { _messageId = id; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + _severity = new AbuseSeverity(); + _severity.readBytes(in); + _reason = new AbuseReason(); + _reason.readBytes(in); + _messageId = new MessageId(); + _messageId.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if ( (_sessionId == null) || (_severity == null) || (_reason == null) ) + throw new I2CPMessageException("Not enough information to construct the message"); + ByteArrayOutputStream os = new ByteArrayOutputStream(32); + try { + _sessionId.writeBytes(os); + _severity.writeBytes(os); + _reason.writeBytes(os); + if (_messageId == null) { + _messageId = new MessageId(); + _messageId.setMessageId(0); + } + _messageId.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof ReportAbuseMessage) ) { + ReportAbuseMessage msg = (ReportAbuseMessage)object; + return DataHelper.eq(getSessionId(),msg.getSessionId()) && + DataHelper.eq(getSeverity(),msg.getSeverity()) && + DataHelper.eq(getReason(),msg.getReason()) && + DataHelper.eq(getMessageId(),msg.getMessageId()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[ReportAbuseMessage: "); + buf.append("\n\tSessionID: ").append(getSessionId()); + buf.append("\n\tSeverity: ").append(getSeverity()); + buf.append("\n\tReason: ").append(getReason()); + buf.append("\n\tMessageId: ").append(getMessageId()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java b/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java new file mode 100644 index 000000000..238e14b24 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java @@ -0,0 +1,153 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.RouterIdentity; +import net.i2p.data.TunnelId; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when destroying + * existing session. + * + * @author jrandom + */ +public class RequestLeaseSetMessage extends I2CPMessageImpl { + private final static Log _log = new Log(RequestLeaseSetMessage.class); + public final static int MESSAGE_TYPE = 21; + private SessionId _sessionId; + private List _endpoints; + private Date _end; + + public RequestLeaseSetMessage() { + setSessionId(null); + _endpoints = new ArrayList(); + setEndDate(null); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + public int getEndpoints() { return _endpoints.size(); } + public RouterIdentity getRouter(int endpoint) { + if ( (endpoint < 0) || (_endpoints.size() < endpoint) ) return null; + return ((TunnelEndpoint)_endpoints.get(endpoint)).getRouter(); + } + public TunnelId getTunnelId(int endpoint) { + if ( (endpoint < 0) || (_endpoints.size() < endpoint) ) return null; + return ((TunnelEndpoint)_endpoints.get(endpoint)).getTunnelId(); + } + public void remoteEndpoint(int endpoint) { + if ( (endpoint >= 0) && (endpoint < _endpoints.size()) ) + _endpoints.remove(endpoint); + } + public void addEndpoint(RouterIdentity router, TunnelId tunnel) { + _endpoints.add(new TunnelEndpoint(router,tunnel)); + } + public Date getEndDate() { return _end; } + public void setEndDate(Date end) { _end = end; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + int numTunnels = (int)DataHelper.readLong(in, 1); + _endpoints.clear(); + for (int i = 0; i < numTunnels; i++) { + RouterIdentity router = new RouterIdentity(); + router.readBytes(in); + TunnelId tunnel = new TunnelId(); + tunnel.readBytes(in); + _endpoints.add(new TunnelEndpoint(router, tunnel)); + } + _end = DataHelper.readDate(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if ( (_sessionId == null) || (_endpoints == null) ) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + _sessionId.writeBytes(os); + DataHelper.writeLong(os, 1, _endpoints.size()); + for (int i = 0; i < _endpoints.size(); i++) { + RouterIdentity router = getRouter(i); + router.writeBytes(os); + TunnelId tunnel = getTunnelId(i); + tunnel.writeBytes(os); + } + DataHelper.writeDate(os, _end); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof RequestLeaseSetMessage) ) { + RequestLeaseSetMessage msg = (RequestLeaseSetMessage)object; + if (getEndpoints() != msg.getEndpoints()) return false; + for (int i = 0; i < getEndpoints(); i++) { + if (!DataHelper.eq(getRouter(i), msg.getRouter(i)) || + DataHelper.eq(getTunnelId(i), msg.getTunnelId(i))) + return false; + } + return DataHelper.eq(getSessionId(),msg.getSessionId()) && + DataHelper.eq(getEndDate(),msg.getEndDate()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[RequestLeaseMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("\n\tTunnels:"); + for (int i = 0; i < getEndpoints(); i++) { + buf.append("\n\t\tRouterIdentity: ").append(getRouter(i)); + buf.append("\n\t\tTunnelId: ").append(getTunnelId(i)); + } + buf.append("\n\tEndDate: ").append(getEndDate()); + buf.append("]"); + return buf.toString(); + } + + private class TunnelEndpoint { + private RouterIdentity _router; + private TunnelId _tunnelId; + + public TunnelEndpoint() { + _router = null; + _tunnelId = null; + } + public TunnelEndpoint(RouterIdentity router, TunnelId id) { + _router = router; + _tunnelId = id; + } + + public RouterIdentity getRouter() { return _router; } + public void setRouter(RouterIdentity router) { _router = router; } + public TunnelId getTunnelId() { return _tunnelId; } + public void setTunnelId(TunnelId tunnelId) { _tunnelId = tunnelId; } + } +} diff --git a/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java new file mode 100644 index 000000000..4f94b5e2b --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java @@ -0,0 +1,104 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Payload; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router to ask it to deliver + * a new message + * + * @author jrandom + */ +public class SendMessageMessage extends I2CPMessageImpl { + private final static Log _log = new Log(SendMessageMessage.class); + public final static int MESSAGE_TYPE = 5; + private SessionId _sessionId; + private Destination _destination; + private Payload _payload; + private long _nonce; + + public SendMessageMessage() { + setSessionId(null); + setDestination(null); + setPayload(null); + setNonce(0); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + public Destination getDestination() { return _destination; } + public void setDestination(Destination destination) { _destination = destination; } + public Payload getPayload() { return _payload; } + public void setPayload(Payload payload) { _payload = payload; } + public long getNonce() { return _nonce; } + public void setNonce(long nonce) { _nonce = nonce; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + _destination = new Destination(); + _destination.readBytes(in); + _payload = new Payload(); + _payload.readBytes(in); + _nonce = DataHelper.readLong(in, 4); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if ( (_sessionId == null) || (_destination == null) || (_payload == null) || (_nonce <= 0) ) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(512); + try { + _sessionId.writeBytes(os); + _destination.writeBytes(os); + _payload.writeBytes(os); + DataHelper.writeLong(os, 4, _nonce); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof SendMessageMessage) ) { + SendMessageMessage msg = (SendMessageMessage)object; + return DataHelper.eq(getSessionId(),msg.getSessionId()) && + DataHelper.eq(getDestination(),msg.getDestination()) && + (getNonce() == msg.getNonce()) && + DataHelper.eq(getPayload(),msg.getPayload()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[SendMessageMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("\n\tNonce: ").append(getNonce()); + buf.append("\n\tDestination: ").append(getDestination()); + buf.append("\n\tPayload: ").append(getPayload()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/SessionConfig.java b/core/java/src/net/i2p/data/i2cp/SessionConfig.java new file mode 100644 index 000000000..96f4ab6c5 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/SessionConfig.java @@ -0,0 +1,217 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Iterator; +import java.util.Properties; + +import net.i2p.crypto.DSAEngine; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.Destination; +import net.i2p.data.Signature; +import net.i2p.data.SigningPrivateKey; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Date; + +/** + * Defines the information a client must provide to create a session + * + * @author jrandom + */ +public class SessionConfig extends DataStructureImpl { + private final static Log _log = new Log(SessionConfig.class); + private Destination _destination; + private Signature _signature; + private Date _creationDate; + private Properties _options; + + /** + * if the client authorized this session more than the specified period ago, + * refuse it, since it may be a replay attack + * + */ + private final static long OFFSET_VALIDITY = 30*1000; + + public SessionConfig() { + _destination = null; + _signature = null; + _creationDate = new Date(Clock.getInstance().now()); + _options = null; + } + + /** + * Retrieve the destination for which this session is supposed to connect + * + */ + public Destination getDestination() { return _destination; } + /** + * Specify the destination for which this session is supposed to connect + * + */ + public void setDestination(Destination dest) { _destination = dest; } + + /** + * Determine when this session was authorized by the destination (so we can + * prevent replay attacks) + * + */ + public Date getCreationDate() { return _creationDate; } + public void setCreationDate(Date date) { _creationDate = date; } + + /** + * Retrieve any configuration options for the session + * + */ + public Properties getOptions() { return _options; } + /** + * Configure the session with the given options + * + */ + public void setOptions(Properties options) { _options = options; } + + public Signature getSignature() { return _signature; } + public void setSignature(Signature sig) { _signature = sig; } + + /** + * Sign the structure using the supplied private key + * + */ + public void signSessionConfig(SigningPrivateKey signingKey) throws DataFormatException { + byte data[] = getBytes(); + if (data == null) throw new DataFormatException("Unable to retrieve bytes for signing"); + _signature = DSAEngine.getInstance().sign(data, signingKey); + } + + /** + * Verify that the signature matches the destination's signing public key. + * + * @return true only if the signature matches + */ + public boolean verifySignature() { + if (getSignature() == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Signature is null!"); + return false; + } + if (getDestination() == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Destination is null!"); + return false; + } + if (getCreationDate() == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Date is null!"); + return false; + } + if (tooOld()) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Too old!"); + return false; + } + byte data[] = getBytes(); + if (data == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Bytes could not be found - wtf?"); + return false; + } + + boolean ok = DSAEngine.getInstance().verifySignature(getSignature(), data, getDestination().getSigningPublicKey()); + if (!ok) { + if (_log.shouldLog(Log.WARN)) + _log.warn("DSA signature failed!"); + } + return ok; + } + + public boolean tooOld() { + long now = Clock.getInstance().now(); + long earliestValid = now - OFFSET_VALIDITY; + long latestValid = now + OFFSET_VALIDITY; + if (_creationDate == null) return true; + if (_creationDate.getTime() < earliestValid) return true; + if (_creationDate.getTime() > latestValid) return true; + return false; + } + + private byte[] getBytes() { + if (_destination == null) return null; + if (_options == null) return null; + if (_creationDate == null) return null; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + _log.debug("PubKey size for destination: " + _destination.getPublicKey().getData().length); + _log.debug("SigningKey size for destination: " + _destination.getSigningPublicKey().getData().length); + _destination.writeBytes(out); + DataHelper.writeProperties(out, _options); + DataHelper.writeDate(out, _creationDate); + } catch (IOException ioe) { + _log.error("IOError signing", ioe); + return null; + } catch (DataFormatException dfe) { + _log.error("Error writing out the bytes for signing/verification", dfe); + return null; + } + return out.toByteArray(); + } + + public void readBytes(InputStream rawConfig) throws DataFormatException, IOException { + _destination = new Destination(); + _destination.readBytes(rawConfig); + _options = DataHelper.readProperties(rawConfig); + _creationDate = DataHelper.readDate(rawConfig); + _signature = new Signature(); + _signature.readBytes(rawConfig); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if ( (_destination == null) || (_options == null) || (_signature == null) || (_creationDate == null) ) + throw new DataFormatException("Not enough data to create the session config"); + _destination.writeBytes(out); + DataHelper.writeProperties(out, _options); + DataHelper.writeDate(out, _creationDate); + _signature.writeBytes(out); + } + + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof SessionConfig) ) { + SessionConfig cfg = (SessionConfig)object; + return DataHelper.eq(getSignature(), cfg.getSignature()) && + DataHelper.eq(getDestination(), cfg.getDestination()) && + DataHelper.eq(getCreationDate(), cfg.getCreationDate()) && + DataHelper.eq(getOptions(), cfg.getOptions()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer("[SessionConfig: "); + buf.append("\n\tDestination: ").append(getDestination()); + buf.append("\n\tSignature: ").append(getSignature()); + buf.append("\n\tCreation Date: ").append(getCreationDate()); + buf.append("\n\tOptions: #: ").append(getOptions().size()); + for (Iterator iter = getOptions().keySet().iterator(); iter.hasNext();) { + String key = (String)iter.next(); + String val = getOptions().getProperty(key); + buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]"); + } + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/SessionId.java b/core/java/src/net/i2p/data/i2cp/SessionId.java new file mode 100644 index 000000000..01b9cad02 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/SessionId.java @@ -0,0 +1,58 @@ +package net.i2p.data.i2cp; +/* + * 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.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; +import net.i2p.data.DataFormatException; + +/** + * Defines the token passed between the router and client to associate messages + * with a particular session. These IDs are not globally unique. + * + * @author jrandom + */ +public class SessionId extends DataStructureImpl { + private final static Log _log = new Log(SessionId.class); + private int _sessionId; + + public SessionId() { setSessionId(-1); } + + public int getSessionId() { return _sessionId; } + public void setSessionId(int id) { _sessionId = id; } + + public void readBytes(InputStream in) throws DataFormatException, IOException { + _sessionId = (int)DataHelper.readLong(in, 2); + } + + public void writeBytes(OutputStream out) throws DataFormatException, IOException { + if (_sessionId < 0) throw new DataFormatException("Invalid session ID: " + _sessionId); + DataHelper.writeLong(out, 2, _sessionId); + } + + public boolean equals(Object obj) { + if ( (obj == null) || !(obj instanceof SessionId)) + return false; + return getSessionId() == ((SessionId)obj).getSessionId(); + } + + public int hashCode() { + return getSessionId(); + } + + public String toString() { + return "[SessionId: " + getSessionId() + "]"; + } +} diff --git a/core/java/src/net/i2p/data/i2cp/SessionStatusMessage.java b/core/java/src/net/i2p/data/i2cp/SessionStatusMessage.java new file mode 100644 index 000000000..6c6094c1f --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/SessionStatusMessage.java @@ -0,0 +1,89 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when destroying + * existing session. + * + * @author jrandom + */ +public class SessionStatusMessage extends I2CPMessageImpl { + private final static Log _log = new Log(SessionStatusMessage.class); + public final static int MESSAGE_TYPE = 20; + private SessionId _sessionId; + private int _status; + + public final static int STATUS_DESTROYED = 0; + public final static int STATUS_CREATED = 1; + public final static int STATUS_UPDATED = 2; + public final static int STATUS_INVALID = 3; + + public SessionStatusMessage() { + setSessionId(null); + setStatus(STATUS_INVALID); + } + + public SessionId getSessionId() { return _sessionId; } + public void setSessionId(SessionId id) { _sessionId = id; } + public int getStatus() { return _status; } + public void setStatus(int status) { _status = status; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + _status = (int)DataHelper.readLong(in, 1); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_sessionId == null) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + _sessionId.writeBytes(os); + DataHelper.writeLong(os, 1, _status); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof SessionStatusMessage) ) { + SessionStatusMessage msg = (SessionStatusMessage)object; + return DataHelper.eq(getSessionId(),msg.getSessionId()) && + DataHelper.eq(getStatus(),msg.getStatus()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[SessionStatusMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("\n\tStatus: ").append(getStatus()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/SetDateMessage.java b/core/java/src/net/i2p/data/i2cp/SetDateMessage.java new file mode 100644 index 000000000..b0e47be26 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/SetDateMessage.java @@ -0,0 +1,77 @@ +package net.i2p.data.i2cp; +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; +import net.i2p.util.Clock; + +import java.util.Date; + +/** + * Tell the other side what time it is + * + */ +public class SetDateMessage extends I2CPMessageImpl { + private final static Log _log = new Log(SetDateMessage.class); + public final static int MESSAGE_TYPE = 33; + private Date _date; + + public SetDateMessage() { + super(); + setDate(new Date(Clock.getInstance().now())); + } + + public Date getDate() { return _date; } + public void setDate(Date date) { _date = date; } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _date = DataHelper.readDate(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_date == null) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + DataHelper.writeDate(os, _date); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { return MESSAGE_TYPE; } + + public boolean equals(Object object) { + if ( (object != null) && (object instanceof SetDateMessage) ) { + SetDateMessage msg = (SetDateMessage)object; + return DataHelper.eq(getDate(), msg.getDate()); + } else { + return false; + } + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[SetDateMessage"); + buf.append("\n\tDate: ").append(getDate()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/stat/Frequency.java b/core/java/src/net/i2p/stat/Frequency.java new file mode 100644 index 000000000..8155b7bf4 --- /dev/null +++ b/core/java/src/net/i2p/stat/Frequency.java @@ -0,0 +1,125 @@ +package net.i2p.stat; + +/** + * Manage the calculation of a moving event frequency over a certain period. + * + */ +public class Frequency { + private double _avgInterval; + private double _minAverageInterval; + private long _period; + private long _lastEvent; + private long _start = now(); + private long _count = 0; + private Object _lock = this; // new Object(); // in case we want to do fancy sync later + + public Frequency(long period) { + setPeriod(period); + setLastEvent(0); + setAverageInterval(0); + setMinAverageInterval(0); + } + + /** how long is this frequency averaged over? */ + public long getPeriod() { synchronized (_lock) { return _period; } } + /** when did the last event occur? */ + public long getLastEvent() { synchronized (_lock) { return _lastEvent; } } + /** + * on average over the last $period, after how many milliseconds are events coming in, + * as calculated during the last event occurrence? + * + */ + public double getAverageInterval() { synchronized (_lock) { return _avgInterval; } } + /** what is the lowest average interval (aka most frequent) we have seen? */ + public double getMinAverageInterval() { synchronized (_lock) { return _minAverageInterval; } } + + /** calculate how many events would occur in a period given the current average */ + public double getAverageEventsPerPeriod() { + synchronized (_lock) { + if (_avgInterval > 0) + return _period / _avgInterval; + else + return 0; + } + } + /** calculate how many events would occur in a period given the maximum average */ + public double getMaxAverageEventsPerPeriod() { + synchronized (_lock) { + if (_minAverageInterval > 0) + return _period / _minAverageInterval; + else + return 0; + } + } + + /** over the lifetime of this stat, without any decay or weighting, what was the average interval between events? */ + public double getStrictAverageInterval() { + synchronized (_lock) { + long duration = now() - _start; + if ( (duration <= 0) || (_count <= 0) ) + return 0; + else + return duration / _count; + } + } + + /** using the strict average interval, how many events occur within an average period? */ + public double getStrictAverageEventsPerPeriod() { + double avgInterval = getStrictAverageInterval(); + synchronized (_lock) { + if (avgInterval > 0) + return _period / avgInterval; + else + return 0; + } + } + + /** how many events have occurred within the lifetime of this stat? */ + public long getEventCount() { synchronized (_lock) { return _count; } } + + /** + * Take note that a new event occurred, recalculating all the averages and frequencies + * + */ + public void eventOccurred() { recalculate(true); } + /** + * Recalculate the averages + * + */ + public void recalculate() { recalculate(false); } + + /** + * Recalculate, but only update the lastEvent if eventOccurred + */ + private void recalculate(boolean eventOccurred) { + synchronized (_lock) { + long now = now(); + long interval = now - _lastEvent; + if (interval >= _period) + interval = _period-1; + else if (interval <= 0) + interval = 1; + + double oldWeight = 1-(interval/(float)_period); + double newWeight = (interval/(float)_period); + + double oldInterval = _avgInterval * oldWeight; + double newInterval = interval * newWeight; + _avgInterval = oldInterval + newInterval; + + if ( (_avgInterval < _minAverageInterval) || (_minAverageInterval <= 0) ) + _minAverageInterval = _avgInterval; + + if (eventOccurred) { + _lastEvent = now; + _count++; + } + } + } + + private void setPeriod(long milliseconds) { synchronized (_lock) { _period = milliseconds; } } + private void setLastEvent(long when) { synchronized (_lock) { _lastEvent = when; } } + private void setAverageInterval(double msInterval) { synchronized (_lock) { _avgInterval = msInterval; } } + private void setMinAverageInterval(double minAverageInterval) { synchronized (_lock) { _minAverageInterval = minAverageInterval; } } + private final static long now() { return System.currentTimeMillis(); } +} diff --git a/core/java/src/net/i2p/stat/FrequencyStat.java b/core/java/src/net/i2p/stat/FrequencyStat.java new file mode 100644 index 000000000..221a474d8 --- /dev/null +++ b/core/java/src/net/i2p/stat/FrequencyStat.java @@ -0,0 +1,52 @@ +package net.i2p.stat; + +/** coordinate an event frequency over various periods */ +public class FrequencyStat { + /** unique name of the statistic */ + private String _statName; + /** grouping under which the stat is kept */ + private String _groupName; + /** describe the stat */ + private String _description; + /** actual frequency objects for this statistic */ + private Frequency _frequencies[]; + + public FrequencyStat(String name, String description, String group, long periods[]) { + _statName = name; + _description = description; + _groupName = group; + _frequencies = new Frequency[periods.length]; + for (int i = 0; i < periods.length; i++) + _frequencies[i] = new Frequency(periods[i]); + } + + /** update all of the frequencies for the various periods */ + public void eventOccurred() { + for (int i = 0; i < _frequencies.length; i++) + _frequencies[i].eventOccurred(); + } + + /** coallesce all the stats */ + public void coallesceStats() { + //for (int i = 0; i < _frequencies.length; i++) + // _frequencies[i].coallesceStats(); + } + + public String getName() { return _statName; } + public String getGroupName() { return _groupName; } + public String getDescription() { return _description; } + public long[] getPeriods() { + long rv[] = new long[_frequencies.length]; + for (int i = 0; i < _frequencies.length; i++) + rv[i] = _frequencies[i].getPeriod(); + return rv; + } + public Frequency getFrequency(long period) { + for (int i = 0; i < _frequencies.length; i++) { + if (_frequencies[i].getPeriod() == period) + return _frequencies[i]; + } + return null; + } + public int hashCode() { return _statName.hashCode(); } +} diff --git a/core/java/src/net/i2p/stat/PersistenceHelper.java b/core/java/src/net/i2p/stat/PersistenceHelper.java new file mode 100644 index 000000000..73e392583 --- /dev/null +++ b/core/java/src/net/i2p/stat/PersistenceHelper.java @@ -0,0 +1,50 @@ +package net.i2p.stat; + +import java.util.Properties; +import net.i2p.util.Log; + +/** object orientation gives you hairy palms. */ +class PersistenceHelper { + private final static Log _log = new Log(PersistenceHelper.class); + private final static String NL = System.getProperty("line.separator"); + + public final static void add(StringBuffer buf, String prefix, String name, String description, double value) { + buf.append("# ").append(prefix.toUpperCase()).append(name.toUpperCase()).append(NL); + buf.append("# ").append(description).append(NL); + buf.append(prefix).append(name).append('=').append(value).append(NL).append(NL); + } + + public final static void add(StringBuffer buf, String prefix, String name, String description, long value) { + buf.append("# ").append(prefix.toUpperCase()).append(name.toUpperCase()).append(NL); + buf.append("# ").append(description).append(NL); + buf.append(prefix).append(name).append('=').append(value).append(NL).append(NL); + } + + public final static long getLong(Properties props, String prefix, String name) { + String val = props.getProperty(prefix + name); + if (val != null) { + try { + return Long.parseLong(val); + } catch (NumberFormatException nfe) { + _log.error("Error formatting " + val + " into a long", nfe); + } + } else { + _log.error("Key " + prefix + name + " does not exist"); + } + return 0; + } + + public final static double getDouble(Properties props, String prefix, String name) { + String val = props.getProperty(prefix + name); + if (val != null) { + try { + return Double.parseDouble(val); + } catch (NumberFormatException nfe) { + _log.error("Error formatting " + val + " into a double", nfe); + } + } else { + _log.error("Key " + prefix + name + " does not exist"); + } + return 0; + } +} diff --git a/core/java/src/net/i2p/stat/Rate.java b/core/java/src/net/i2p/stat/Rate.java new file mode 100644 index 000000000..6e88166bf --- /dev/null +++ b/core/java/src/net/i2p/stat/Rate.java @@ -0,0 +1,430 @@ +package net.i2p.stat; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Properties; + +import net.i2p.util.Clock; +import net.i2p.util.Log; + +/** + * Simple rate calculator for periodically sampled data points - determining an + * average value over a period, the number of events in that period, the maximum number + * of events (using the interval between events), and lifetime data. + * + */ +public class Rate { + private final static Log _log = new Log(Rate.class); + private volatile double _currentTotalValue; + private volatile long _currentEventCount; + private volatile long _currentTotalEventTime; + private volatile double _lastTotalValue; + private volatile long _lastEventCount; + private volatile long _lastTotalEventTime; + private volatile double _extremeTotalValue; + private volatile long _extremeEventCount; + private volatile long _extremeTotalEventTime; + private volatile double _lifetimeTotalValue; + private volatile long _lifetimeEventCount; + private volatile long _lifetimeTotalEventTime; + + private volatile long _lastCoallesceDate; + private long _creationDate; + private long _period; + + /** locked during coallesce and addData */ + private Object _lock = new Object(); + + /** in the current (partial) period, what is the total value acrued through all events? */ + public double getCurrentTotalValue() { return _currentTotalValue; } + /** in the current (partial) period, how many events have occurred? */ + public long getCurrentEventCount() { return _currentEventCount; } + /** in the current (partial) period, how much of the time has been spent doing the events? */ + public long getCurrentTotalEventTime() { return _currentTotalEventTime; } + /** in the last full period, what was the total value acrued through all events? */ + public double getLastTotalValue() { return _lastTotalValue; } + /** in the last full period, how many events occurred? */ + public long getLastEventCount() { return _lastEventCount; } + /** in the last full period, how much of the time was spent doing the events? */ + public long getLastTotalEventTime() { return _lastTotalEventTime; } + /** what was the max total value acrued in any period? */ + public double getExtremeTotalValue() { return _extremeTotalValue; } + /** when the max(totalValue) was achieved, how many events occurred in that period? */ + public long getExtremeEventCount() { return _extremeEventCount; } + /** when the max(totalValue) was achieved, how much of the time was spent doing the events? */ + public long getExtremeTotalEventTime() { return _extremeTotalEventTime; } + /** since rate creation, what was the total value acrued through all events? */ + public double getLifetimeTotalValue() { return _lifetimeTotalValue; } + /** since rate creation, how many events have occurred? */ + public long getLifetimeEventCount() { return _lifetimeEventCount; } + /** since rate creation, how much of the time was spent doing the events? */ + public long getLifetimeTotalEventTime() { return _lifetimeTotalEventTime; } + /** when was the rate last coallesced? */ + public long getLastCoallesceDate() { return _lastCoallesceDate; } + /** when was this rate created? */ + public long getCreationDate() { return _creationDate; } + /** how large should this rate's cycle be? */ + public long getPeriod() { return _period; } + + /** + * + * @param period number of milliseconds in the period this rate deals with + * @throws IllegalArgumentException if the period is not greater than 0 + */ + public Rate(long period) throws IllegalArgumentException { + if (period <= 0) throw new IllegalArgumentException("The period must be strictly positive"); + _currentTotalValue = 0.0d; + _currentEventCount = 0; + _currentTotalEventTime = 0; + _lastTotalValue = 0.0d; + _lastEventCount = 0; + _lastTotalEventTime = 0; + _extremeTotalValue = 0.0d; + _extremeEventCount = 0; + _extremeTotalEventTime = 0; + _lifetimeTotalValue = 0.0d; + _lifetimeEventCount = 0; + _lifetimeTotalEventTime = 0; + + _creationDate = now(); + _lastCoallesceDate = _creationDate; + _period = period; + } + + /** + * Create a new rate and load its state from the properties, taking data + * from the data points underneath the given prefix (e.g. prefix = + * "profile.dbIntroduction.60m", this will load the associated data points such + * as "profile.dbIntroduction.60m.lifetimeEventCount"). The data can be exported + * through store(outputStream, "profile.dbIntroduction.60m"). + * + * @param prefix prefix to the property entries (should NOT end with a period) + * @param treatAsCurrent if true, we'll treat the loaded data as if no time has + * elapsed since it was written out, but if it is false, we'll + * treat the data with as much freshness (or staleness) as appropriate. + * @throws IllegalArgumentException if the data was formatted incorrectly + */ + public Rate(Properties props, String prefix, boolean treatAsCurrent) throws IllegalArgumentException { + this(1); + load(props, prefix, treatAsCurrent); + } + + + /** accrue the data in the current period as an instantaneous event */ + public void addData(long value) { addData(value, 0); } + /** + * Accrue the data in the current period as if the event took the specified amount of time + * + * @param value value to accrue in the current period + * @param eventDuration how long it took to accrue this data (set to 0 if it was instantaneous) + */ + public void addData(long value, long eventDuration) { + synchronized (_lock) { + _currentTotalValue += value; + _currentEventCount++; + _currentTotalEventTime += eventDuration; + + _lifetimeTotalValue += value; + _lifetimeEventCount++; + _lifetimeTotalEventTime += eventDuration; + } + } + + public void coallesce() { + synchronized (_lock) { + long now = now(); + long measuredPeriod = now - _lastCoallesceDate; + if (measuredPeriod < _period) { + // no need to coallesce + return; + } else { + // ok ok, lets coallesce + + // how much were we off by? (so that we can sample down the measured values) + double periodFactor = measuredPeriod / _period; + _lastTotalValue = (_currentTotalValue == 0 ? 0.0d : _currentTotalValue / periodFactor); + _lastEventCount = (_currentEventCount == 0 ? 0l : (long)(_currentEventCount / periodFactor)); + _lastTotalEventTime = (_currentTotalEventTime == 0 ? 0l : (long)(_currentTotalEventTime / periodFactor)); + _lastCoallesceDate = now; + + if (_lastTotalValue > _extremeTotalValue) { + _extremeTotalValue = _lastTotalValue; + _extremeEventCount = _lastEventCount; + _extremeTotalEventTime = _lastTotalEventTime; + } + + _currentTotalValue = 0.0d; + _currentEventCount = 0; + _currentTotalEventTime = 0; + } + } + } + + /** what was the average value across the events in the last period? */ + public double getAverageValue() { + if ( (_lastTotalValue != 0) && (_lastEventCount > 0) ) + return _lastTotalValue / _lastEventCount; + else + return 0.0d; + } + + /** what was the average value across the events in the most active period? */ + public double getExtremeAverageValue() { + if ( (_extremeTotalValue != 0) && (_extremeEventCount > 0) ) + return _extremeTotalValue / _extremeEventCount; + else + return 0.0d; + } + + /** what was the average value across the events since the stat was created? */ + public double getLifetimeAverageValue() { + if ( (_lifetimeTotalValue != 0) && (_lifetimeEventCount > 0) ) + return _lifetimeTotalValue / _lifetimeEventCount; + else + return 0.0d; + } + + + /** + * During the last period, how much of the time was spent actually processing events in proportion + * to how many events could have occurred if there were no intervals? + * + * If this statistic doesn't use event times, this returns 0. + */ + public double getLastEventSaturation() { + if ( (_lastEventCount > 0) && (_lastTotalEventTime > 0) ) { + double eventTime = (double)_lastTotalEventTime / (double)_lastEventCount; + double maxEvents = (double)_period / eventTime; + double saturation = _lastEventCount / maxEvents; + return saturation; + } else { + return 0.0d; + } + } + + /** + * During the extreme period, how much of the time was spent actually processing events in proportion + * to how many events could have occurred if there were no intervals? + * + * If this statistic doesn't use event times, this returns 0. + */ + public double getExtremeEventSaturation() { + if ( (_extremeEventCount > 0) && (_extremeTotalEventTime > 0) ) { + double eventTime = (double)_extremeTotalEventTime / (double)_extremeEventCount; + double maxEvents = (double)_period / eventTime; + return _extremeEventCount / maxEvents; + } else { + return 0.0d; + } + } + + /** + * During the lifetime of this stat, how much of the time was spent actually processing events in proportion + * to how many events could have occurred if there were no intervals? + * + * If this statistic doesn't use event times, this returns 0. + */ + public double getLifetimeEventSaturation() { + if ( (_lastEventCount > 0) && (_lifetimeTotalEventTime > 0) ) { + double eventTime = (double)_lifetimeTotalEventTime / (double)_lifetimeEventCount; + double maxEvents = (double)_period / eventTime; + double numPeriods = getLifetimePeriods(); + double avgEventsPerPeriod = _lifetimeEventCount / numPeriods; + return avgEventsPerPeriod / maxEvents; + } else { + return 0.0d; + } + } + + /** how many periods have we already completed? */ + public long getLifetimePeriods() { + long lifetime = now() - _creationDate; + double periods = lifetime / (double)_period; + return (long)Math.floor(periods); + } + + /** + * using the last period's rate, what is the total value that could have been sent if events were constant? + * If this statistic's event times are 0, this returns 0. + * + */ + public double getLastSaturationLimit() { + if ( (_lastTotalValue != 0) && (_lastEventCount > 0) && (_lastTotalEventTime > 0) ) { + double saturation = getLastEventSaturation(); + if (saturation != 0.0d) + return _lastTotalValue / saturation; + else + return 0.0d; + } else { + return 0.0d; + } + } + + /** + * using the extreme period's rate, what is the total value that could have been sent if events were constant? + * If this statistic's event times are 0, this returns 0. + * + */ + public double getExtremeSaturationLimit() { + if ( (_extremeTotalValue != 0) && (_extremeEventCount > 0) && (_extremeTotalEventTime > 0) ) { + double saturation = getExtremeEventSaturation(); + if (saturation != 0.0d) + return _extremeTotalValue / saturation; + else + return 0.0d; + } else { + return 0.0d; + } + } + + /** + * How large was the last period's value as compared to the largest period ever? + * + */ + public double getPercentageOfExtremeValue() { + if ( (_lastTotalValue != 0) && (_extremeTotalValue != 0) ) + return _lastTotalValue / _extremeTotalValue; + else + return 0.0d; + } + + /** + * How large was the last period's value as compared to the lifetime average value? + * + */ + public double getPercentageOfLifetimeValue() { + if ( (_lastTotalValue != 0) && (_lifetimeTotalValue != 0) ) { + double lifetimePeriodValue = _period * (_lifetimeTotalValue / (now() - _creationDate)); + return _lastTotalValue / lifetimePeriodValue; + } else { + return 0.0d; + } + } + + public void store(OutputStream out, String prefix) throws IOException { + StringBuffer buf = new StringBuffer(2048); + PersistenceHelper.add(buf, prefix, ".period", "Number of milliseconds in the period", _period); + PersistenceHelper.add(buf, prefix, ".creationDate", "When was this rate created? (milliseconds since the epoch, GMT)", _creationDate); + PersistenceHelper.add(buf, prefix, ".lastCoallesceDate", "When did we last coallesce this rate? (milliseconds since the epoch, GMT)", _lastCoallesceDate); + PersistenceHelper.add(buf, prefix, ".currentDate", "When did this data get written? (milliseconds since the epoch, GMT)", now()); + PersistenceHelper.add(buf, prefix, ".currentTotalValue", "Total value of data points in the current (uncoallesced) period", _currentTotalValue); + PersistenceHelper.add(buf, prefix, ".currentEventCount", "How many events have occurred in the current (uncoallesced) period?", _currentEventCount); + PersistenceHelper.add(buf, prefix, ".currentTotalEventTime", "How many milliseconds have the events in the current (uncoallesced) period consumed?", _currentTotalEventTime); + PersistenceHelper.add(buf, prefix, ".lastTotalValue", "Total value of data points in the most recent (coallesced) period", _lastTotalValue); + PersistenceHelper.add(buf, prefix, ".lastEventCount", "How many events have occurred in the most recent (coallesced) period?", _lastEventCount); + PersistenceHelper.add(buf, prefix, ".lastTotalEventTime", "How many milliseconds have the events in the most recent (coallesced) period consumed?", _lastTotalEventTime); + PersistenceHelper.add(buf, prefix, ".extremeTotalValue", "Total value of data points in the most extreme period", _extremeTotalValue); + PersistenceHelper.add(buf, prefix, ".extremeEventCount", "How many events have occurred in the most extreme period?", _extremeEventCount); + PersistenceHelper.add(buf, prefix, ".extremeTotalEventTime", "How many milliseconds have the events in the most extreme period consumed?", _extremeTotalEventTime); + PersistenceHelper.add(buf, prefix, ".lifetimeTotalValue", "Total value of data points since this stat was created", _lifetimeTotalValue); + PersistenceHelper.add(buf, prefix, ".lifetimeEventCount", "How many events have occurred since this stat was created?", _lifetimeEventCount); + PersistenceHelper.add(buf, prefix, ".lifetimeTotalEventTime", "How many milliseconds have the events since this stat was created consumed?", _lifetimeTotalEventTime); + out.write(buf.toString().getBytes()); + } + + /** + * Load this rate from the properties, taking data from the data points + * underneath the given prefix. + * + * @param prefix prefix to the property entries (should NOT end with a period) + * @param treatAsCurrent if true, we'll treat the loaded data as if no time has + * elapsed since it was written out, but if it is false, we'll + * treat the data with as much freshness (or staleness) as appropriate. + * @throws IllegalArgumentException if the data was formatted incorrectly + */ + public void load(Properties props, String prefix, boolean treatAsCurrent) throws IllegalArgumentException { + _period = PersistenceHelper.getLong(props, prefix, ".period"); + _creationDate = PersistenceHelper.getLong(props, prefix, ".creationDate"); + _lastCoallesceDate = PersistenceHelper.getLong(props, prefix, ".lastCoallesceDate"); + _currentTotalValue = PersistenceHelper.getDouble(props, prefix, ".currentTotalValue"); + _currentEventCount = PersistenceHelper.getLong(props, prefix, ".currentEventCount"); + _currentTotalEventTime = PersistenceHelper.getLong(props, prefix, ".currentTotalEventTime"); + _lastTotalValue = PersistenceHelper.getDouble(props, prefix, ".lastTotalValue"); + _lastEventCount = PersistenceHelper.getLong(props, prefix, ".lastEventCount"); + _lastTotalEventTime = PersistenceHelper.getLong(props, prefix, ".lastTotalEventTime"); + _extremeTotalValue = PersistenceHelper.getDouble(props, prefix, ".extremeTotalValue"); + _extremeEventCount = PersistenceHelper.getLong(props, prefix, ".extremeEventCount"); + _extremeTotalEventTime = PersistenceHelper.getLong(props, prefix, ".extremeTotalEventTime"); + _lifetimeTotalValue = PersistenceHelper.getDouble(props, prefix, ".lifetimeTotalValue"); + _lifetimeEventCount = PersistenceHelper.getLong(props, prefix, ".lifetimeEventCount"); + _lifetimeTotalEventTime = PersistenceHelper.getLong(props, prefix, ".lifetimeTotalEventTime"); + + if (treatAsCurrent) + _lastCoallesceDate = now(); + + if (_period <= 0) + throw new IllegalArgumentException("Period for " + prefix + " is invalid"); + coallesce(); + } + + public boolean equals(Object obj) { + if ( (obj == null) || (obj.getClass() != Rate.class) ) return false; + Rate r = (Rate)obj; + return _period == r.getPeriod() && + _creationDate == r.getCreationDate() && + //_lastCoallesceDate == r.getLastCoallesceDate() && + _currentTotalValue == r.getCurrentTotalValue() && + _currentEventCount == r.getCurrentEventCount() && + _currentTotalEventTime == r.getCurrentTotalEventTime() && + _lastTotalValue == r.getLastTotalValue() && + _lastEventCount == r.getLastEventCount() && + _lastTotalEventTime == r.getLastTotalEventTime() && + _extremeTotalValue == r.getExtremeTotalValue() && + _extremeEventCount == r.getExtremeEventCount() && + _extremeTotalEventTime == r.getExtremeTotalEventTime() && + _lifetimeTotalValue == r.getLifetimeTotalValue() && + _lifetimeEventCount == r.getLifetimeEventCount() && + _lifetimeTotalEventTime == r.getLifetimeTotalEventTime(); + } + + public String toString() { + StringBuffer buf = new StringBuffer(2048); + buf.append("\n\t total value: ").append(getLastTotalValue()); + buf.append("\n\t highest total value: ").append(getExtremeTotalValue()); + buf.append("\n\t lifetime total value: ").append(getLifetimeTotalValue()); + buf.append("\n\t # periods: ").append(getLifetimePeriods()); + buf.append("\n\t average value: ").append(getAverageValue()); + buf.append("\n\t highest average value: ").append(getExtremeAverageValue()); + buf.append("\n\t lifetime average value: ").append(getLifetimeAverageValue()); + buf.append("\n\t % of lifetime rate: ").append(100.0d * getPercentageOfLifetimeValue()); + buf.append("\n\t % of highest rate: ").append(100.0d * getPercentageOfExtremeValue()); + buf.append("\n\t # events: ").append(getLastEventCount()); + buf.append("\n\t lifetime events: ").append(getLifetimeEventCount()); + if (getLifetimeTotalEventTime() > 0) { + // we have some actual event durations + buf.append("\n\t % of time spent processing events: ").append(100.0d * getLastEventSaturation()); + buf.append("\n\t total value if we were always processing events: ").append(getLastSaturationLimit()); + buf.append("\n\t max % of time spent processing events: ").append(100.0d * getExtremeEventSaturation()); + buf.append("\n\t max total value if we were always processing events: ").append(getExtremeSaturationLimit()); + } + return buf.toString(); + } + private final static long now() { return Clock.getInstance().now(); } + + public static void main(String args[]) { + Rate rate = new Rate(1000); + for (int i = 0; i < 50; i++) { + try { Thread.sleep(20); } catch (InterruptedException ie) {} + rate.addData(i*100, 20); + } + rate.coallesce(); + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(2048); + try { + rate.store(baos, "rate.test"); + byte data[] = baos.toByteArray(); + _log.error("Stored rate: size = " + data.length + "\n" + new String(data)); + + Properties props = new Properties(); + props.load(new java.io.ByteArrayInputStream(data)); + + //_log.error("Properties loaded: \n" + props); + + Rate r = new Rate(props, "rate.test", true); + + _log.error("Comparison after store/load: " + r.equals(rate)); + } catch (Throwable t) { + _log.error("b0rk", t); + } + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + } +} diff --git a/core/java/src/net/i2p/stat/RateStat.java b/core/java/src/net/i2p/stat/RateStat.java new file mode 100644 index 000000000..55cdbef0c --- /dev/null +++ b/core/java/src/net/i2p/stat/RateStat.java @@ -0,0 +1,172 @@ +package net.i2p.stat; + +import java.io.OutputStream; +import java.io.IOException; + +import java.util.Arrays; +import java.util.Properties; + +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** coordinate a moving rate over various periods */ +public class RateStat { + private final static Log _log = new Log(RateStat.class); + /** unique name of the statistic */ + private String _statName; + /** grouping under which the stat is kept */ + private String _groupName; + /** describe the stat */ + private String _description; + /** actual rate objects for this statistic */ + private Rate _rates[]; + + public RateStat(String name, String description, String group, long periods[]) { + _statName = name; + _description = description; + _groupName = group; + _rates = new Rate[periods.length]; + for (int i = 0; i < periods.length; i++) + _rates[i] = new Rate(periods[i]); + } + + /** + * update all of the rates for the various periods with the given value. + */ + public void addData(long value, long eventDuration) { + for (int i = 0; i < _rates.length; i++) + _rates[i].addData(value, eventDuration); + } + + /** coallesce all the stats */ + public void coallesceStats() { + for (int i = 0; i < _rates.length; i++) + _rates[i].coallesce(); + } + + + public String getName() { return _statName; } + public String getGroupName() { return _groupName; } + public String getDescription() { return _description; } + public long[] getPeriods() { + long rv[] = new long[_rates.length]; + for (int i = 0; i < _rates.length; i++) + rv[i] = _rates[i].getPeriod(); + return rv; + } + public Rate getRate(long period) { + for (int i = 0; i < _rates.length; i++) { + if (_rates[i].getPeriod() == period) + return _rates[i]; + } + return null; + } + public int hashCode() { return _statName.hashCode(); } + + + private final static String NL = System.getProperty("line.separator"); + public String toString() { + StringBuffer buf = new StringBuffer(512); + buf.append(getGroupName()).append('.').append(getName()).append(": ").append(getDescription()).append('\n'); + long periods[] = getPeriods(); + Arrays.sort(periods); + for (int i = 0; i < periods.length; i++) { + buf.append('\t').append(periods[i]).append(':'); + Rate curRate = getRate(periods[i]); + buf.append(curRate.toString()); + buf.append(NL); + } + return buf.toString(); + } + + public boolean equals(Object obj) { + if ( (obj == null) || (obj.getClass() != RateStat.class) ) return false; + RateStat rs = (RateStat)obj; + if (DataHelper.eq(getGroupName(), rs.getGroupName()) && + DataHelper.eq(getDescription(), rs.getDescription()) && + DataHelper.eq(getName(), rs.getName())) { + for (int i = 0; i < _rates.length; i++) + if (!_rates[i].equals(rs.getRate(_rates[i].getPeriod()))) + return false; + return true; + } else { + return false; + } + } + + public void store(OutputStream out, String prefix) throws IOException { + StringBuffer buf = new StringBuffer(128); + buf.append(NL); + buf.append("################################################################################").append(NL); + buf.append("# Rate: ").append(_groupName).append(": ").append(_statName).append(NL); + buf.append("# ").append(_description).append(NL); + buf.append("# ").append(NL).append(NL); + out.write(buf.toString().getBytes()); + buf = null; + for (int i = 0; i < _rates.length; i++) { + StringBuffer rbuf = new StringBuffer(256); + rbuf.append("#######").append(NL); + rbuf.append("# Period : ").append(DataHelper.formatDuration(_rates[i].getPeriod())).append(" for rate ").append(_groupName).append(" - ").append(_statName).append(NL); + rbuf.append(NL); + out.write(rbuf.toString().getBytes()); + String curPrefix = prefix + "." + DataHelper.formatDuration(_rates[i].getPeriod()); + _rates[i].store(out, curPrefix); + } + } + + /** + * Load this rate stat from the properties, populating all of the rates contained + * underneath it. The comes from the given prefix (e.g. if we are given the prefix + * "profile.dbIntroduction", a series of rates may be found underneath + * "profile.dbIntroduction.60s", "profile.dbIntroduction.60m", and "profile.dbIntroduction.24h"). + * This RateStat must already be created, with the specified rate entries constructued - this + * merely loads them with data. + * + * @param prefix prefix to the property entries (should NOT end with a period) + * @param treatAsCurrent if true, we'll treat the loaded data as if no time has + * elapsed since it was written out, but if it is false, we'll + * treat the data with as much freshness (or staleness) as appropriate. + * @throws IllegalArgumentException if the data was formatted incorrectly + */ + public void load(Properties props, String prefix, boolean treatAsCurrent) throws IllegalArgumentException { + for (int i = 0; i < _rates.length; i++) { + long period = _rates[i].getPeriod(); + String curPrefix = prefix + "." + DataHelper.formatDuration(period); + try { + _rates[i].load(props, curPrefix, treatAsCurrent); + } catch (IllegalArgumentException iae) { + _rates[i] = new Rate(period); + if (_log.shouldLog(Log.WARN)) + _log.warn("Rate for " + prefix + " is corrupt, reinitializing that period"); + } + } + } + + public static void main(String args[]) { + RateStat rs = new RateStat("moo", "moo moo moo", "cow trueisms", new long[] { 60*1000, 60*60*1000, 24*60*60*1000 }); + for (int i = 0; i < 50; i++) { + try { Thread.sleep(20); } catch (InterruptedException ie) {} + rs.addData(i*100, 20); + } + rs.coallesceStats(); + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(2048); + try { + rs.store(baos, "rateStat.test"); + byte data[] = baos.toByteArray(); + _log.error("Stored rateStat: size = " + data.length + "\n" + new String(data)); + + Properties props = new Properties(); + props.load(new java.io.ByteArrayInputStream(data)); + + //_log.error("Properties loaded: \n" + props); + + RateStat loadedRs = new RateStat("moo", "moo moo moo", "cow trueisms", new long[] { 60*1000, 60*60*1000, 24*60*60*1000 }); + loadedRs.load(props, "rateStat.test", true); + + _log.error("Comparison after store/load: " + rs.equals(loadedRs)); + } catch (Throwable t) { + _log.error("b0rk", t); + } + try { Thread.sleep(5000); } catch (InterruptedException ie) {} + } +} diff --git a/core/java/src/net/i2p/stat/SimpleStatDumper.java b/core/java/src/net/i2p/stat/SimpleStatDumper.java new file mode 100644 index 000000000..632cade51 --- /dev/null +++ b/core/java/src/net/i2p/stat/SimpleStatDumper.java @@ -0,0 +1,62 @@ +package net.i2p.stat; + +import net.i2p.util.Log; + +import java.util.Set; +import java.util.TreeSet; +import java.util.Iterator; +import java.util.Arrays; + +public class SimpleStatDumper { + private final static Log _log = new Log(SimpleStatDumper.class); + + public static void dumpStats(int logLevel) { + if (!_log.shouldLog(logLevel)) return; + + StringBuffer buf = new StringBuffer(4*1024); + dumpFrequencies(buf); + dumpRates(buf); + _log.log(logLevel, buf.toString()); + } + + private static void dumpFrequencies(StringBuffer buf) { + Set frequencies = new TreeSet(StatManager.getInstance().getFrequencyNames()); + for (Iterator iter = frequencies.iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + FrequencyStat freq = StatManager.getInstance().getFrequency(name); + buf.append('\n'); + buf.append(freq.getGroupName()).append('.').append(freq.getName()).append(": ").append(freq.getDescription()).append('\n'); + long periods[] = freq.getPeriods(); + Arrays.sort(periods); + for (int i = 0; i < periods.length; i++) { + buf.append('\t').append(periods[i]).append(':'); + Frequency curFreq = freq.getFrequency(periods[i]); + buf.append(" average interval: ").append(curFreq.getAverageInterval()); + buf.append(" min average interval: ").append(curFreq.getMinAverageInterval()); + buf.append('\n'); + } + } + } + + private static void dumpRates(StringBuffer buf) { + Set rates = new TreeSet(StatManager.getInstance().getRateNames()); + for (Iterator iter = rates.iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + RateStat rate = StatManager.getInstance().getRate(name); + buf.append('\n'); + buf.append(rate.getGroupName()).append('.').append(rate.getName()).append(": ").append(rate.getDescription()).append('\n'); + long periods[] = rate.getPeriods(); + Arrays.sort(periods); + for (int i = 0; i < periods.length; i++) { + buf.append('\t').append(periods[i]).append(':'); + Rate curRate = rate.getRate(periods[i]); + dumpRate(curRate, buf); + buf.append('\n'); + } + } + } + + static void dumpRate(Rate curRate, StringBuffer buf) { + buf.append(curRate.toString()); + } +} diff --git a/core/java/src/net/i2p/stat/SizeTest.java b/core/java/src/net/i2p/stat/SizeTest.java new file mode 100644 index 000000000..147c37243 --- /dev/null +++ b/core/java/src/net/i2p/stat/SizeTest.java @@ -0,0 +1,55 @@ +package net.i2p.stat; + + +public class SizeTest { + public static void main(String args[]) { + testRateSize(100); //117KB + testRateSize(100000); // 4.5MB + testRateSize(440000); // 44MB + //testFrequencySize(100); // 114KB + //testFrequencySize(100000); // 5.3MB + //testFrequencySize(1000000); // 52MB + } + + private static void testRateSize(int num) { + Runtime.getRuntime().gc(); + Rate rate[] = new Rate[num]; + long used = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); + long usedPer = used / num; + System.out.println(num+ ": create array - Used: " + used + " bytes (or " + usedPer + " bytes per array entry)"); + + int i = 0; + try { + for (; i < num; i++) + rate[i] = new Rate(1234); + } catch (OutOfMemoryError oom) { + rate = null; + Runtime.getRuntime().gc(); + System.out.println("Ran out of memory when creating rate " + i); + return; + } + Runtime.getRuntime().gc(); + long usedObjects = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); + usedPer = usedObjects / num; + System.out.println(num + ": create objects - Used: " + usedObjects + " bytes (or " + usedPer + " bytes per rate)"); + rate = null; + Runtime.getRuntime().gc(); + } + + private static void testFrequencySize(int num) { + Runtime.getRuntime().gc(); + Frequency freq[] = new Frequency[num]; + long used = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); + long usedPer = used / num; + System.out.println(num+ ": create array - Used: " + used + " bytes (or " + usedPer + " bytes per array entry)"); + + for (int i = 0; i < num; i++) + freq[i] = new Frequency(1234); + Runtime.getRuntime().gc(); + long usedObjects = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory(); + usedPer = usedObjects / num; + System.out.println(num + ": create objects - Used: " + usedObjects + " bytes (or " + usedPer + " bytes per frequency)"); + freq = null; + Runtime.getRuntime().gc(); + } +} diff --git a/core/java/src/net/i2p/stat/StatManager.java b/core/java/src/net/i2p/stat/StatManager.java new file mode 100644 index 000000000..ed50c540e --- /dev/null +++ b/core/java/src/net/i2p/stat/StatManager.java @@ -0,0 +1,118 @@ +package net.i2p.stat; + +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; +import java.util.TreeSet; +import java.util.TreeMap; +import java.util.Iterator; +import java.util.Collections; + +import net.i2p.util.Log; + +/** + * Coordinate the management of various frequencies and rates within I2P components, + * both allowing central update and retrieval, as well as distributed creation and + * use. This does not provide any persistence, but the data structures exposed can be + * read and updated to manage the complete state. + * + */ +public class StatManager { + private final static Log _log = new Log(StatManager.class); + private final static StatManager _instance = new StatManager(); + public final static StatManager getInstance() { return _instance; } + /** stat name to FrequencyStat */ + private Map _frequencyStats; + /** stat name to RateStat */ + private Map _rateStats; + + private StatManager() { + _frequencyStats = Collections.synchronizedMap(new HashMap(128)); + _rateStats = Collections.synchronizedMap(new HashMap(128)); + } + + /** + * Create a new statistic to monitor the frequency of some event. + * + * @param name unique name of the statistic + * @param description simple description of the statistic + * @param group used to group statistics together + * @param periods array of period lengths (in milliseconds) + */ + public void createFrequencyStat(String name, String description, String group, long periods[]) { + _frequencyStats.put(name, new FrequencyStat(name, description, group, periods)); + } + + /** + * Create a new statistic to monitor the average value and confidence of some action. + * + * @param name unique name of the statistic + * @param description simple description of the statistic + * @param group used to group statistics together + * @param periods array of period lengths (in milliseconds) + */ + public void createRateStat(String name, String description, String group, long periods[]) { + _rateStats.put(name, new RateStat(name, description, group, periods)); + } + + /** update the given frequency statistic, taking note that an event occurred (and recalculating all frequencies) */ + public void updateFrequency(String name) { + FrequencyStat freq = (FrequencyStat)_frequencyStats.get(name); + if (freq != null) + freq.eventOccurred(); + } + + /** update the given rate statistic, taking note that the given data point was received (and recalculating all rates) */ + public void addRateData(String name, long data, long eventDuration) { + RateStat stat = (RateStat)_rateStats.get(name); + if (stat != null) + stat.addData(data, eventDuration); + } + + public void coallesceStats() { + for (Iterator iter = getFrequencyNames().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + FrequencyStat stat = getFrequency(name); + if (stat != null) { + stat.coallesceStats(); + } + } + for (Iterator iter = getRateNames().iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + RateStat stat = getRate(name); + if (stat != null) { + stat.coallesceStats(); + } + } + } + + public FrequencyStat getFrequency(String name) { return (FrequencyStat)_frequencyStats.get(name); } + public RateStat getRate(String name) { return (RateStat)_rateStats.get(name); } + public Set getFrequencyNames() { return Collections.unmodifiableSet(new HashSet(_frequencyStats.keySet())); } + public Set getRateNames() { return Collections.unmodifiableSet(new HashSet(_rateStats.keySet())); } + /** is the given stat a monitored rate? */ + public boolean isRate(String statName) { return _rateStats.containsKey(statName); } + /** is the given stat a monitored frequency? */ + public boolean isFrequency(String statName) { return _frequencyStats.containsKey(statName); } + + /** Group name (String) to a Set of stat names */ + public Map getStatsByGroup() { + Map groups = new TreeMap(); + for (Iterator iter = _frequencyStats.values().iterator(); iter.hasNext();) { + FrequencyStat stat = (FrequencyStat)iter.next(); + if (!groups.containsKey(stat.getGroupName())) + groups.put(stat.getGroupName(), new TreeSet()); + Set names = (Set)groups.get(stat.getGroupName()); + names.add(stat.getName()); + } + for (Iterator iter = _rateStats.values().iterator(); iter.hasNext();) { + RateStat stat = (RateStat)iter.next(); + if (!groups.containsKey(stat.getGroupName())) + groups.put(stat.getGroupName(), new TreeSet()); + Set names = (Set)groups.get(stat.getGroupName()); + names.add(stat.getName()); + } + return groups; + } +} diff --git a/core/java/src/net/i2p/util/Clock.java b/core/java/src/net/i2p/util/Clock.java new file mode 100644 index 000000000..3a22bb201 --- /dev/null +++ b/core/java/src/net/i2p/util/Clock.java @@ -0,0 +1,92 @@ +package net.i2p.util; + +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; + +/** + * Alternate location for determining the time which takes into account an offset. + * This offset will ideally be periodically updated so as to serve as the difference + * between the local computer's current time and the time as known by some reference + * (such as an NTP synchronized clock). + * + */ +public class Clock { + private final static Log _log = new Log(Clock.class); + private final static Clock _instance = new Clock(); + public final static Clock getInstance() { return _instance; } + private volatile long _offset; + private boolean _alreadyChanged; + private Set _listeners; + + /** if the clock is skewed by 3+ days, fuck 'em */ + public final static long MAX_OFFSET = 3*24*60*60*1000; + /** if the clock skewed changes by less than 1s, ignore the update (so we don't slide all over the place) */ + public final static long MIN_OFFSET_CHANGE = 30*1000; + + private Clock() { + _offset = 0; + _alreadyChanged = false; + _listeners = new HashSet(64); + } + + /** + * Specify how far away from the "correct" time the computer is - a positive + * value means that we are slow, while a negative value means we are fast. + * + */ + public void setOffset(long offsetMs) { + if ( (offsetMs > MAX_OFFSET) || (offsetMs < 0-MAX_OFFSET) ) { + _log.error("Maximum offset shift exceeded [" + offsetMs + "], NOT HONORING IT"); + return; + } + long delta = offsetMs - _offset; + if ( (delta < MIN_OFFSET_CHANGE) && (delta > 0 - MIN_OFFSET_CHANGE) ) { + _log.debug("Not changing offset since it is only " + delta + "ms"); + return; + } + if (_alreadyChanged) + _log.log(Log.CRIT, "Updating clock offset to " + offsetMs + "ms from " + _offset + "ms"); + else + _log.log(Log.INFO, "Initializing clock offset to " + offsetMs + "ms from " + _offset + "ms"); + _alreadyChanged = true; + _offset = offsetMs; + fireOffsetChanged(delta); + } + public long getOffset() { return _offset; } + + public void setNow(long realTime) { + long diff = realTime - System.currentTimeMillis(); + setOffset(diff); + } + + /** + * Retrieve the current time synchronized with whatever reference clock is in + * use. + * + */ + public long now() { return _offset + System.currentTimeMillis(); } + + public void addUpdateListener(ClockUpdateListener lsnr) { + synchronized (_listeners) { + _listeners.add(lsnr); + } + } + public void removeUpdateListener(ClockUpdateListener lsnr) { + synchronized (_listeners) { + _listeners.remove(lsnr); + } + } + + private void fireOffsetChanged(long delta) { + synchronized (_listeners) { + for (Iterator iter = _listeners.iterator(); iter.hasNext(); ) { + ClockUpdateListener lsnr = (ClockUpdateListener)iter.next(); + lsnr.offsetChanged(delta); + } + } + } + public static interface ClockUpdateListener { + public void offsetChanged(long delta); + } +} diff --git a/core/java/src/net/i2p/util/EventDispatcher.java b/core/java/src/net/i2p/util/EventDispatcher.java new file mode 100644 index 000000000..83525943e --- /dev/null +++ b/core/java/src/net/i2p/util/EventDispatcher.java @@ -0,0 +1,103 @@ +package net.i2p.util; +/* + * free (adj.): unencumbered; not under the control of others Written + * by human in 2004 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.util.Set; + +/** + * Event dispatching interface. It allows objects to receive and + * notify data events (basically String->Object associations) and + * create notification chains. To ease the usage of this interface, + * you could define an EventDispatcherImpl attribute called + * _event (as suggested in EventDispatcherImpl documentation) + * and cut'n'paste the following default implementation: + * + * + * public EventDispatcher getEventDispatcher() { return _event; } + * public void attachEventDispatcher(IEventDispatcher e) { _event.attachEventDispatcher(e.getEventDispatcher()); } + * public void detachEventDispatcher(IEventDispatcher e) { _event.detachEventDispatcher(e.getEventDispatcher()); } + * public void notifyEvent(String e, Object a) { _event.notifyEvent(e,a); } + * public Object getEventValue(String n) { return _event.getEventValue(n); } + * public Set getEvents() { return _event.getEvents(); } + * public void ignoreEvents() { _event.ignoreEvents(); } + * public void unIgnoreEvents() { _event.unIgnoreEvents(); } + * public Object waitEventValue(String n) { return _event.waitEventValue(n); } + * + * + * @author human + */ +public interface EventDispatcher { + + /** + * Get an object to be used to deliver events (usually + * this, but YMMV). + */ + public EventDispatcher getEventDispatcher(); + + /** + * Attach an EventDispatcher object to the events dispatching chain. Note + * that notification is not bidirectional (i.e. events notified to + * ev won't reach the object calling this method). + * Good luck, and beware of notification loops! :-) + * + * @param iev Event object to be attached + */ + public void attachEventDispatcher(EventDispatcher iev); + + /** + * Detach the specified EventDispatcher object from the events dispatching chain. + * + * @param iev Event object to be detached + */ + public void detachEventDispatcher(EventDispatcher iev); + + /** + * Deliver an event + * + * @param event name of the event + * @param args data being stored for that event + */ + public void notifyEvent(String event, Object args); + + /** + * Retrieve the value currently associated with the specified + * event value + * + * @param name name of the event to query for + * @return value (or null if none are available) + */ + public Object getEventValue(String name); + + /** + * Retrieve the names of all the events that have been received + * + * @return A set of event names + */ + public Set getEvents(); + + /** + * Ignore further event notifications + * + */ + public void ignoreEvents(); + + /** + * Almost like the method above :-) + * + */ + public void unIgnoreEvents(); + + /** + * Wait until the given event has received a value + * + * @param name name of the event to wait for + * @return value specified for that event + */ + public Object waitEventValue(String name); +} diff --git a/core/java/src/net/i2p/util/EventDispatcherImpl.java b/core/java/src/net/i2p/util/EventDispatcherImpl.java new file mode 100644 index 000000000..c872bb64e --- /dev/null +++ b/core/java/src/net/i2p/util/EventDispatcherImpl.java @@ -0,0 +1,144 @@ +package net.i2p.util; +/* + * free (adj.): unencumbered; not under the control of others Written + * by human & jrandom in 2004 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.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Set; + +/** + * An implementation of the EventDispatcher interface. Since Java + * doesn't support multiple inheritance, you could follow the Log.java + * style: this class should be instantiated and kept as a variable by + * each object it is used by, ala: + * private final EventDispatcher _event = new EventDispatcher(); + * + * If there is anything in here that doesn't make sense, turn off + * your computer and go fly a kite - (c) 2004 by jrandom + + * @author human + * @author jrandom + */ +public class EventDispatcherImpl implements EventDispatcher { + + private final static Log _log = new Log(EventDispatcherImpl.class); + + private boolean _ignore = false; + private HashMap _events = new HashMap(4); + private ArrayList _attached = new ArrayList(); + + public EventDispatcher getEventDispatcher() { + return this; + } + + public void attachEventDispatcher(EventDispatcher ev) { + if (ev == null) return; + synchronized (_attached) { + _log.debug(this.hashCode() + ": attaching EventDispatcher " + + ev.hashCode()); + _attached.add(ev); + } + } + + public void detachEventDispatcher(EventDispatcher ev) { + if (ev == null) return; + synchronized (_attached) { + ListIterator it = _attached.listIterator(); + while (it.hasNext()) { + if (((EventDispatcher)it.next()) == ev) { + it.remove(); + break; + } + } + } + } + + public void notifyEvent(String eventName, Object args) { + if (_ignore) return; + if (args == null) { + args = "[null value]"; + } + _log.debug(this.hashCode() + ": got notification [" + + eventName + "] = [" + args + "]"); + synchronized (_events) { + _events.put(eventName, args); + _events.notifyAll(); + synchronized (_attached) { + Iterator it = _attached.iterator(); + EventDispatcher e; + while (it.hasNext()) { + e = (EventDispatcher)it.next(); + _log.debug(this.hashCode() + + ": notifying attached EventDispatcher " + + e.hashCode() + ": [" + + eventName + "] = [" + args + "]"); + e.notifyEvent(eventName, args); + } + } + } + } + + public Object getEventValue(String name) { + if (_ignore) return null; + Object val; + + synchronized (_events) { + val = _events.get(name); + } + + return val; + } + + public Set getEvents() { + if (_ignore) return Collections.EMPTY_SET; + Set set; + + synchronized (_events) { + set = new HashSet(_events.keySet()); + } + + return set; + } + + public void ignoreEvents() { + _ignore = true; + synchronized (_events) { + _events.clear(); + } + _events = null; + } + + public void unIgnoreEvents() { + _ignore = false; + } + + public Object waitEventValue(String name) { + if (_ignore) return null; + Object val; + + _log.debug(this.hashCode() + ": waiting for [" + name + "]"); + do { + synchronized (_events) { + if (_events.containsKey(name)) { + val = _events.get(name); + break; + } + try { + _events.wait(1*1000); + } catch (InterruptedException e) {} + } + } while (true); + + return val; + } +} diff --git a/core/java/src/net/i2p/util/HTTPSendData.java b/core/java/src/net/i2p/util/HTTPSendData.java new file mode 100644 index 000000000..782da58cf --- /dev/null +++ b/core/java/src/net/i2p/util/HTTPSendData.java @@ -0,0 +1,87 @@ +package net.i2p.util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.zip.GZIPOutputStream; + +/** + * Simple helper class to submit data via HTTP Post + * + */ +public class HTTPSendData { + private final static Log _log = new Log(HTTPSendData.class); + + /** + * Submit an HTTP post to the given url, sending the data specified + * + * @param url url to post to + * @param length number of bytes in the dataToSend to, er, send + * @param dataToSend stream of bytes to be sent + * @return true if the data was posted successfully + */ + public static boolean postData(String url, long length, InputStream dataToSend) { + try { + URL rurl = new URL(url); + return postData(rurl, length, dataToSend); + } catch (MalformedURLException mue) { + return false; + } + } + + /** + * Submit an HTTP post to the given url, sending the data specified + * + * @param url url to post to + * @param length number of bytes in the dataToSend to, er, send + * @param dataToSend stream of bytes to be sent + */ + public static boolean postData(URL url, long length, InputStream dataToSend) { + try { + HttpURLConnection con = (HttpURLConnection)url.openConnection(); + con.setDoInput(true); + con.setDoOutput(true); + con.setUseCaches(false); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-length", ""+length); + + OutputStream out = con.getOutputStream(); + byte buf[] = new byte[64*1024]; + int read; + long sent = 0; + GZIPOutputStream zipOut = new GZIPOutputStream(out); + while ( (read = dataToSend.read(buf)) != -1) { + zipOut.write(buf, 0, read); + sent += read; + if (sent >= length) break; + } + + zipOut.flush(); + zipOut.finish(); + zipOut.close(); + out.close(); + + + int rv = con.getResponseCode(); + _log.debug("Posted "+sent + " bytes: " + rv); + return length == sent; + } catch (IOException ioe) { + _log.error("Error posting the data", ioe); + return false; + } + } + + public static void main(String args[]) { + byte data[] = new byte[4096]; + for (int i = 0; i < data.length; i++) + data[i] = (byte)((i%26)+'a'); + + boolean sent = HTTPSendData.postData("http://i2p.dnsalias.net/cgi-bin/submitMessageHistory", data.length, new ByteArrayInputStream(data)); + _log.debug("Sent? " + sent); + try { Thread.sleep(2000); } catch (InterruptedException ie) {} + } +} diff --git a/core/java/src/net/i2p/util/HexDump.java b/core/java/src/net/i2p/util/HexDump.java new file mode 100644 index 000000000..c5013e0d6 --- /dev/null +++ b/core/java/src/net/i2p/util/HexDump.java @@ -0,0 +1,136 @@ +package net.i2p.util; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 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.io.IOException; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; + +/** + * Hexdump class (well, it's actually a namespace with some functions, + * but let's stick with java terminology :-). These methods generate + * an output that resembles `hexdump -C` (Windows users: do you + * remember `debug` in the DOS age?). + * + * @author human + */ +public class HexDump { + + private static final int FORMAT_OFFSET_PADDING = 8; + private static final int FORMAT_BYTES_PER_ROW = 16; + private static final byte[] HEXCHARS = "0123456789abcdef".getBytes(); + + /** + * Dump a byte array in a String. + * + * @param data Data to be dumped + */ + public static String dump(byte[] data) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + dump(data, 0, data.length, out); + } catch (IOException e) { + e.printStackTrace(); + } + + return out.toString(); + } + + /** + * Dump a byte array in a String. + * + * @param data Data to be dumped + * @param off Offset from the beginning of data + * @param len Number of bytes of data to be dumped + */ + public static String dump(byte[] data, int off, int len) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + dump(data, off, len, out); + } catch (IOException e) { + e.printStackTrace(); + } + + return out.toString(); + } + + /** + * Dump a byte array through a stream. + * + * @param data Data to be dumped + * @param out Output stream + */ + public static void dump(byte data[], OutputStream out) throws IOException { + dump(data, 0, data.length, out); + } + + /** + * Dump a byte array through a stream. + * + * @param data Data to be dumped + * @param off Offset from the beginning of data + * @param len Number of bytes of data to be dumped + * @param out Output stream + */ + public static void dump(byte[] data, int off, int len, OutputStream out) throws IOException { + String hexoff; + int dumpoff, hexofflen, i, nextbytes, end = len+off; + int val; + + for (dumpoff = off; dumpoff < end; dumpoff += FORMAT_BYTES_PER_ROW) { + // Pad the offset with 0's (i miss my beloved sprintf()...) + hexoff = Integer.toString(dumpoff, 16); + hexofflen = hexoff.length(); + for (i = 0; i < FORMAT_OFFSET_PADDING - hexofflen; ++i) { + hexoff = "0" + hexoff; + } + out.write((hexoff + " ").getBytes()); + + // Bytes to be printed in the current line + nextbytes = (FORMAT_BYTES_PER_ROW < (end - dumpoff) + ? FORMAT_BYTES_PER_ROW + : (end - dumpoff)); + + for (i = 0; i < FORMAT_BYTES_PER_ROW; ++i) { + // Put two spaces to separate 8-bytes blocks + if ((i % 8) == 0) { + out.write(" ".getBytes()); + } + if (i >= nextbytes) { + out.write(" ".getBytes()); + } else { + val = ((int)data[dumpoff + i]) & 0xff; + out.write(HEXCHARS[val >>> 4]); + out.write(HEXCHARS[val & 0xf]); + out.write(" ".getBytes()); + } + } + + out.write(" |".getBytes()); + + for (i = 0; i < FORMAT_BYTES_PER_ROW; ++i) { + if (i >= nextbytes) { + out.write(" ".getBytes()); + } else { + val = data[i + dumpoff]; + // Is it a printable character? + if ((val > 31) && (val < 127)) { + out.write(val); + } else { + out.write(".".getBytes()); + } + } + } + + out.write("|\n".getBytes()); + } + } +} diff --git a/core/java/src/net/i2p/util/I2PThread.java b/core/java/src/net/i2p/util/I2PThread.java new file mode 100644 index 000000000..894959936 --- /dev/null +++ b/core/java/src/net/i2p/util/I2PThread.java @@ -0,0 +1,59 @@ +package net.i2p.util; +/* + * 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. + * + */ + +/** + * In case its useful later... + * (e.g. w/ native programatic thread dumping, etc) + * + */ +public class I2PThread extends Thread { + private static Log _log; + private static OOMEventListener _lsnr; + + public I2PThread() { + super(); + } + public I2PThread(String name) { + super(name); + } + public I2PThread(Runnable r) { + super(r); + } + public I2PThread(Runnable r, String name) { + super(r, name); + } + + public void run() { + try { + super.run(); + } catch (Throwable t) { + if ( (t instanceof OutOfMemoryError) && (_lsnr != null) ) + _lsnr.outOfMemory((OutOfMemoryError)t); + // we cant assume log is created + if (_log == null) _log = new Log(I2PThread.class); + _log.log(Log.CRIT, "Killing thread " + getName(), t); + } + } + + public static void setOOMEventListener(OOMEventListener lsnr) { _lsnr = lsnr; } + public static OOMEventListener getOOMEventListener() { return _lsnr; } + + public interface OOMEventListener { + public void outOfMemory(OutOfMemoryError err); + } + + public static void main(String args[]) { + I2PThread t = new I2PThread(new Runnable() { + public void run() { throw new NullPointerException("blah"); } + }); + t.start(); + try { Thread.sleep(10000); } catch (Throwable tt) {} + } +} diff --git a/core/java/src/net/i2p/util/Log.java b/core/java/src/net/i2p/util/Log.java new file mode 100644 index 000000000..e192bc4bb --- /dev/null +++ b/core/java/src/net/i2p/util/Log.java @@ -0,0 +1,107 @@ +package net.i2p.util; +/* + * 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. + * + */ + + +/** + * Wrapper class for whatever logging system I2P uses. This class should be + * instantiated and kept as a variable for each class it is used by, ala: + * private final static Log _log = new Log(MyClassName.class); + * + * If there is anything in here that doesn't make sense, turn off your computer + * and go fly a kite. + * + * + * @author jrandom + */ +public class Log { + private Class _class; + private String _name; + private int _minPriority; + + public final static int DEBUG = 10; + public final static int INFO = 20; + public final static int WARN = 30; + public final static int ERROR = 40; + public final static int CRIT = 50; + + public final static String STR_DEBUG = "DEBUG"; + public final static String STR_INFO = "INFO"; + public final static String STR_WARN = "WARN"; + public final static String STR_ERROR = "ERROR"; + public final static String STR_CRIT = "CRIT"; + + public static int getLevel(String level) { + if (level == null) return Log.CRIT; + level = level.toUpperCase(); + if (STR_DEBUG.startsWith(level)) return DEBUG; + if (STR_INFO.startsWith(level)) return INFO; + if (STR_WARN.startsWith(level)) return WARN; + if (STR_ERROR.startsWith(level)) return ERROR; + if (STR_CRIT.startsWith(level)) return CRIT; + return CRIT; + } + + public static String toLevelString(int level) { + switch (level) { + case DEBUG: return STR_DEBUG; + case INFO: return STR_INFO; + case WARN: return STR_WARN; + case ERROR: return STR_ERROR; + case CRIT: return STR_CRIT; + } + return (level > CRIT ? STR_CRIT : STR_DEBUG); + } + + public Log(Class cls) { + this(cls, null); + } + public Log(String name) { + this(null, name); + } + public Log(Class cls, String name) { + _class = cls; + _name = name; + _minPriority = DEBUG; + LogManager.getInstance().registerLog(this); + } + + public void log(int priority, String msg) { + if (priority >= _minPriority) { + LogManager.getInstance().addRecord(new LogRecord(_class, _name, Thread.currentThread().getName(), priority, msg, null)); + } + } + public void log(int priority, String msg, Throwable t) { + if (priority >= _minPriority) { + LogManager.getInstance().addRecord(new LogRecord(_class, _name, Thread.currentThread().getName(), priority, msg, t)); + } + } + + public void debug(String msg) { log(DEBUG, msg); } + public void debug(String msg, Throwable t) { log(DEBUG, msg, t); } + public void info(String msg) { log(INFO, msg); } + public void info(String msg, Throwable t) { log(INFO, msg, t); } + public void warn(String msg) { log(WARN, msg); } + public void warn(String msg, Throwable t) { log(WARN, msg, t); } + public void error(String msg) { log(ERROR, msg); } + public void error(String msg, Throwable t) { log(ERROR, msg, t); } + + public int getMinimumPriority() { return _minPriority; } + public void setMinimumPriority(int priority) { _minPriority = priority; } + + public boolean shouldLog(int priority) { return priority >= _minPriority; } + + String getName() { + if (_class != null) + return _class.getName(); + else + return _name; + } + +} diff --git a/core/java/src/net/i2p/util/LogConsoleBuffer.java b/core/java/src/net/i2p/util/LogConsoleBuffer.java new file mode 100644 index 000000000..ce78cce2c --- /dev/null +++ b/core/java/src/net/i2p/util/LogConsoleBuffer.java @@ -0,0 +1,39 @@ +package net.i2p.util; + +import java.util.List; +import java.util.LinkedList; + +/** + * Offer a glimpse into the last few console messages generated + * + */ +public class LogConsoleBuffer { + private final static LogConsoleBuffer _instance = new LogConsoleBuffer(); + public final static LogConsoleBuffer getInstance() { return _instance; } + private List _buffer; + + private LogConsoleBuffer() { + _buffer = new LinkedList(); + } + + void add(String msg) { + int lim = LogManager.getInstance().getConsoleBufferSize(); + synchronized (_buffer) { + while (_buffer.size() >= lim) + _buffer.remove(0); + _buffer.add(msg); + } + } + + /** + * Retrieve the currently bufferd messages, earlier values were generated... + * earlier. All values are strings with no formatting (as they are written + * in the logs) + * + */ + public List getMostRecentMessages() { + synchronized (_buffer) { + return new LinkedList(_buffer); + } + } +} diff --git a/core/java/src/net/i2p/util/LogLimit.java b/core/java/src/net/i2p/util/LogLimit.java new file mode 100644 index 000000000..cab603dc1 --- /dev/null +++ b/core/java/src/net/i2p/util/LogLimit.java @@ -0,0 +1,32 @@ +package net.i2p.util; +/* + * 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. + * + */ + +/** + * Defines the log limit for a particular set of logs + * + */ +class LogLimit { + private String _rootName; + private int _limit; + + public LogLimit(String name, int limit) { + _rootName = name; + _limit = limit; + } + + public String getRootName() { return _rootName; } + public int getLimit() { return _limit; } + + public boolean matches(Log log) { + String name = log.getName(); + if (name == null) return false; + return name.startsWith(_rootName); + } +} diff --git a/core/java/src/net/i2p/util/LogManager.java b/core/java/src/net/i2p/util/LogManager.java new file mode 100644 index 000000000..75497dce9 --- /dev/null +++ b/core/java/src/net/i2p/util/LogManager.java @@ -0,0 +1,367 @@ +package net.i2p.util; +/* + * 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.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +/** + * Manages the logging system, loading (and reloading) the configuration file, + * coordinating the log limits, and storing the set of log records pending. + * This also fires off a LogWriter thread that pulls pending records off and + * writes them where appropriate. + * + */ +public class LogManager { + public final static String CONFIG_LOCATION_PROP = "loggerConfigLocation"; + public final static String FILENAME_OVERRIDE_PROP = "loggerFilenameOverride"; + public final static String CONFIG_LOCATION_DEFAULT = "logger.config"; + public static final LogManager getInstance() { return _instance; } + private static final LogManager _instance = new LogManager(System.getProperty(CONFIG_LOCATION_PROP, CONFIG_LOCATION_DEFAULT)); + private static final Log _log = new Log(LogManager.class); + + /** + * These define the characters in the format line of the config file + */ + public static final char DATE = 'd', + CLASS = 'c', + THREAD = 't', + PRIORITY = 'p', + MESSAGE = 'm'; + + public final static String PROP_FORMAT = "logger.format"; + public final static String PROP_DATEFORMAT = "logger.dateFormat"; + public final static String PROP_FILENAME = "logger.logFileName"; + public final static String PROP_FILESIZE = "logger.logFileSize"; + public final static String PROP_ROTATIONLIMIT = "logger.logRotationLimit"; + public final static String PROP_DISPLAYONSCREEN = "logger.displayOnScreen"; + public final static String PROP_CONSOLEBUFFERSIZE = "logger.consoleBufferSize"; + public final static String PROP_DISPLAYONSCREENLEVEL = "logger.minimumOnScreenLevel"; + public final static String PROP_DEFALTLEVEL = "logger.defaultLevel"; + public final static String PROP_RECORD_PREFIX = "logger.record."; + + public final static String DEFAULT_FORMAT = DATE + ' ' + PRIORITY + " [" + THREAD + "] " + CLASS + ": " + MESSAGE; + public final static String DEFAULT_DATEFORMAT = "hh:mm:ss.SSS"; + public final static String DEFAULT_FILENAME = "log-#.txt"; + public final static String DEFAULT_FILESIZE = "10m"; + public final static boolean DEFAULT_DISPLAYONSCREEN = true; + public final static int DEFAULT_CONSOLEBUFFERSIZE = 20; + public final static String DEFAULT_ROTATIONLIMIT = "2"; + public final static String DEFAULT_DEFALTLEVEL = Log.STR_DEBUG; + public final static String DEFAULT_ONSCREENLEVEL = Log.STR_DEBUG; + + private long _configLastRead; + + private String _location; + private List _records; + private Set _limits; + private Set _logs; + private LogWriter _writer; + + private int _defaultLimit; + private char[] _format; + private SimpleDateFormat _dateFormat; + private String _baseLogfilename; + private int _fileSize; + private int _rotationLimit; + private int _onScreenLimit; + + private boolean _displayOnScreen; + private int _consoleBufferSize; + + public void setDisplayOnScreen(boolean yes) { _displayOnScreen = yes; } + public boolean displayOnScreen() { return _displayOnScreen; } + public int getDisplayOnScreenLevel() { return _onScreenLimit; } + public void setDisplayOnScreenLevel(int level) { _onScreenLimit = level; } + public int getConsoleBufferSize() { return _consoleBufferSize; } + public void setConsoleBufferSize(int numRecords) { _consoleBufferSize = numRecords; } + + public void setConfig(String filename) { + _log.debug("Config filename set to " + filename); + _location = filename; + loadConfig(); + } + + /** + * Used by Log to add records to the queue + * + */ + void addRecord(LogRecord record) { + synchronized (_records) { + _records.add(record); + } + } + + /** + * Called during Log construction + * + */ + void registerLog(Log log) { + synchronized (_logs) { + _logs.add(log); + } + updateLimit(log); + } + + /** + * Called periodically by the log writer's thread + * + */ + void rereadConfig() { + // perhaps check modification time + _log.debug("Rereading configuration file"); + loadConfig(); + } + + /// + /// + + private LogManager(String location) { + _displayOnScreen = true; + _location = location; + _records = new ArrayList(); + _limits = new HashSet(); + _logs = new HashSet(); + _defaultLimit = Log.DEBUG; + _configLastRead = 0; + loadConfig(); + _writer = new LogWriter(); + Thread t = new I2PThread(_writer); + t.setName("LogWriter"); + t.setDaemon(true); + t.start(); + Runtime.getRuntime().addShutdownHook(new ShutdownHook()); + } + + // + // + // + + private void loadConfig() { + Properties p = new Properties(); + File cfgFile = new File(_location); + if ( (_configLastRead > 0) && (_configLastRead > cfgFile.lastModified()) ) { + _log.debug("Short circuiting config read"); + return; + } + FileInputStream fis = null; + try { + fis = new FileInputStream(cfgFile); + p.load(fis); + _configLastRead = cfgFile.lastModified(); + } catch (IOException ioe) { + System.err.println("Error loading logger config from " + new File(_location).getAbsolutePath()); + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + parseConfig(p); + updateLimits(); + } + + private void parseConfig(Properties config) { + String fmt = config.getProperty(PROP_FORMAT, new String(DEFAULT_FORMAT)); + _format = fmt.toCharArray(); + + String df = config.getProperty(PROP_DATEFORMAT, DEFAULT_DATEFORMAT); + _dateFormat = new SimpleDateFormat(df); + + String disp = config.getProperty(PROP_DISPLAYONSCREEN); + if (disp == null) + _displayOnScreen = DEFAULT_DISPLAYONSCREEN; + else { + if ("TRUE".equals(disp.toUpperCase().trim())) + _displayOnScreen = true; + else if ("YES".equals(disp.toUpperCase().trim())) + _displayOnScreen = true; + else + _displayOnScreen = false; + } + + String filenameOverride = System.getProperty(FILENAME_OVERRIDE_PROP); + if (filenameOverride != null) + _baseLogfilename = filenameOverride; + else + _baseLogfilename = config.getProperty(PROP_FILENAME, DEFAULT_FILENAME); + + _fileSize = getFilesize(config.getProperty(PROP_FILESIZE, DEFAULT_FILESIZE)); + _rotationLimit = -1; + try { + String str = config.getProperty(PROP_ROTATIONLIMIT); + if (str == null) System.err.println("Rotation limit not specified"); + _rotationLimit = Integer.parseInt(config.getProperty(PROP_ROTATIONLIMIT, DEFAULT_ROTATIONLIMIT)); + } catch (NumberFormatException nfe) { + System.err.println("Invalid rotation limit"); + nfe.printStackTrace(); + } + _defaultLimit = Log.getLevel(config.getProperty(PROP_DEFALTLEVEL, DEFAULT_DEFALTLEVEL)); + + _onScreenLimit = Log.getLevel(config.getProperty(PROP_DISPLAYONSCREENLEVEL, DEFAULT_ONSCREENLEVEL)); + + try { + String str = config.getProperty(PROP_CONSOLEBUFFERSIZE); + if (str == null) + _consoleBufferSize = DEFAULT_CONSOLEBUFFERSIZE; + else + _consoleBufferSize = Integer.parseInt(str); + } catch (NumberFormatException nfe) { + System.err.println("Invalid console buffer size"); + nfe.printStackTrace(); + _consoleBufferSize = DEFAULT_CONSOLEBUFFERSIZE; + } + + parseLimits(config); + } + + private void parseLimits(Properties config) { + synchronized (_limits) { + _limits.clear(); + } + for (Iterator iter = config.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = config.getProperty(key); + if (key.startsWith(PROP_RECORD_PREFIX)) { + String name = key.substring(PROP_RECORD_PREFIX.length()); + LogLimit lim = new LogLimit(name, Log.getLevel(val)); + //_log.debug("Limit found for " + name + " as " + val); + synchronized (_limits) { + _limits.add(lim); + } + } + } + updateLimits(); + } + + private int getFilesize(String size) { + int sz = -1; + try { + String v = size; + char mod = size.toUpperCase().charAt(size.length()-1); + if (!Character.isDigit(mod)) + v = size.substring(0, size.length()-1); + int val = Integer.parseInt(v); + switch ((int)mod) { + case 'K': + val *= 1024; + break; + case 'M': + val *= 1024*1024; + break; + case 'G': + val *= 1024*1024*1024; + break; + case 'T': // because we can + val *= 1024*1024*1024*1024; + break; + } + return val; + } catch (Throwable t) { + System.err.println("Error parsing config for filesize: [" + size + "]"); + t.printStackTrace(); + return -1; + } + } + + private void updateLimits() { + Set logs = new HashSet(); + synchronized (_logs) { + logs.addAll(_logs); + } + for (Iterator iter = logs.iterator(); iter.hasNext();) { + Log log = (Log)iter.next(); + updateLimit(log); + } + } + + private void updateLimit(Log log) { + List limits = getLimits(log); + LogLimit max = null; + LogLimit notMax = null; + for (int i = 0; i < limits.size(); i++) { + LogLimit cur = (LogLimit)limits.get(i); + if (max == null) + max = cur; + else { + if (cur.getRootName().length() > max.getRootName().length()) { + notMax = max; + max = cur; + } + } + } + if (max != null) + log.setMinimumPriority(max.getLimit()); + else + log.setMinimumPriority(_defaultLimit); + } + + private List getLimits(Log log) { + ArrayList limits = new ArrayList(); + synchronized (_limits) { + for (Iterator iter = _limits.iterator(); iter.hasNext(); ) { + LogLimit limit = (LogLimit)iter.next(); + if (limit.matches(log)) + limits.add(limit); + } + } + return limits; + } + + /// + /// would be friend methods for LogWriter... + /// + String _getBaseLogfilename() { return _baseLogfilename; } + int _getFileSize() { return _fileSize; } + int _getRotationLimit() { return _rotationLimit; } + //List _getRecords() { return _records; } + List _removeAll() { + List vals = null; + synchronized (_records) { + vals = new ArrayList(_records); + _records.clear(); + } + return vals; + } + char[] _getFormat() { return _format; } + SimpleDateFormat _getDateFormat() { return _dateFormat; } + + public static void main(String args[]) { + Log l1 = new Log("test.1"); + Log l2 = new Log("test.2"); + Log l21 = new Log("test.2.1"); + Log l = new Log("test"); + l.debug("this should fail"); + l.info("this should pass"); + l1.warn("this should pass"); + l1.info("this should fail"); + l2.error("this should fail"); + l21.debug("this should pass"); + l1.error("test exception", new Exception("test")); + l1.error("test exception", new Exception("test")); + try { Thread.sleep(2*1000); } catch (Throwable t) {} + System.exit(0); + } + + public void shutdown() { + _log.log(Log.CRIT, "Shutting down logger", new Exception("Shutdown")); + _writer.flushRecords(); + } + + private class ShutdownHook extends Thread { + public void run() { + shutdown(); + } + } +} diff --git a/core/java/src/net/i2p/util/LogRecord.java b/core/java/src/net/i2p/util/LogRecord.java new file mode 100644 index 000000000..45e6b729c --- /dev/null +++ b/core/java/src/net/i2p/util/LogRecord.java @@ -0,0 +1,41 @@ +package net.i2p.util; +/* + * 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. + * + */ + +/** + * Frisbee + * + */ +class LogRecord { + private long _date; + private Class _source; + private String _name; + private String _threadName; + private int _priority; + private String _message; + private Throwable _throwable; + + public LogRecord(Class src, String name, String threadName, int priority, String msg, Throwable t) { + _date = Clock.getInstance().now(); + _source = src; + _name = name; + _threadName = threadName; + _priority = priority; + _message = msg; + _throwable = t; + } + + public long getDate() { return _date; } + public Class getSource() { return _source; } + public String getSourceName() { return _name; } + public String getThreadName() { return _threadName; } + public int getPriority() { return _priority; } + public String getMessage() { return _message; } + public Throwable getThrowable() { return _throwable; } +} diff --git a/core/java/src/net/i2p/util/LogRecordFormatter.java b/core/java/src/net/i2p/util/LogRecordFormatter.java new file mode 100644 index 000000000..24fe736f2 --- /dev/null +++ b/core/java/src/net/i2p/util/LogRecordFormatter.java @@ -0,0 +1,77 @@ +package net.i2p.util; +/* + * 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.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.io.IOException; + +import java.util.Date; + +/** + * Render a log record according to the log manager's settings + * + */ +class LogRecordFormatter { + private final static String NL = System.getProperty("line.separator"); + // arbitrary max length for the classname property (this makes is it lines up nicely) + private final static int MAX_WHERE_LENGTH = 30; + // if we're going to have one for where... be consistent + private final static int MAX_THREAD_LENGTH = 12; + private final static int MAX_PRIORITY_LENGTH = 5; + + + public static String formatRecord(LogRecord rec) { + StringBuffer buf = new StringBuffer(); + char format[] = LogManager.getInstance()._getFormat(); + for (int i=0; i size) + str = str.substring(str.length() - size); + buf.append(str); + while (buf.length() < size) buf.append(' '); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/util/LogWriter.java b/core/java/src/net/i2p/util/LogWriter.java new file mode 100644 index 000000000..d295d8b02 --- /dev/null +++ b/core/java/src/net/i2p/util/LogWriter.java @@ -0,0 +1,178 @@ +package net.i2p.util; +/* + * 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.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** + * Log writer thread that pulls log records from the LogManager, writes them to + * the current logfile, and rotates the logs as necessary. This also periodically + * instructs the LogManager to reread its config file. + * + */ +class LogWriter implements Runnable { + private final static long CONFIG_READ_ITERVAL = 10*1000; + private long _lastReadConfig = 0; + private long _numBytesInCurrentFile = 0; + private OutputStream _currentOut; // = System.out + private int _rotationNum = -1; + private String _logFilenamePattern; + private File _currentFile; + + private boolean _write; + public void stopWriting() { _write = false; } + + public void run() { + _write = true; + rotateFile(); + while (_write) { + flushRecords(); + } + } + + public void flushRecords() { + try { + List records = LogManager.getInstance()._removeAll(); + for (int i = 0; i < records.size(); i++) { + LogRecord rec = (LogRecord)records.get(i); + writeRecord(rec); + } + if (records.size() > 0) { + try { + _currentOut.flush(); + } catch (IOException ioe) { + System.err.println("Error flushing the records"); + } + } + records.clear(); + try { Thread.sleep(30); } catch (InterruptedException ie) {} + } catch (Throwable t) { + t.printStackTrace(); + } + long now = Clock.getInstance().now(); + if (now - _lastReadConfig > CONFIG_READ_ITERVAL) { + LogManager.getInstance().rereadConfig(); + _lastReadConfig = now; + } + } + + private void writeRecord(LogRecord rec) { + String val = LogRecordFormatter.formatRecord(rec); + writeRecord(val); + + if (LogManager.getInstance().getDisplayOnScreenLevel() <= rec.getPriority()) { + // we always add to the console buffer, but only sometimes write to stdout + LogConsoleBuffer.getInstance().add(val); + if (LogManager.getInstance().displayOnScreen()) { + System.out.print(val); + } + } + } + + private void writeRecord(String val) { + if (val == null) return; + if (_currentOut == null) rotateFile(); + + byte b[] = val.getBytes(); + try { + _currentOut.write(b); + _numBytesInCurrentFile += b.length; + } catch (Throwable t) { + System.err.println("Error writing record, disk full?"); + t.printStackTrace(); + } + if (_numBytesInCurrentFile >= LogManager.getInstance()._getFileSize()) { + rotateFile(); + } + } + + /** + * Rotate to the next file (or the first file if this is the first call) + * + */ + private void rotateFile() { + String pattern = LogManager.getInstance()._getBaseLogfilename(); + File f = getNextFile(pattern); + _currentFile = f; + _numBytesInCurrentFile = 0; + try { + _currentOut = new FileOutputStream(f); + } catch (IOException ioe) { + System.err.println("Error rotating into [" + f.getAbsolutePath() + "]"); + ioe.printStackTrace(); + } + } + + /** + * Get the next file in the rotation + * + */ + private File getNextFile(String pattern) { + File f = null; + if (pattern.indexOf('#') < 0) { + return new File(pattern); + } else { + int max = LogManager.getInstance()._getRotationLimit(); + if (_rotationNum == -1) { + return getFirstFile(pattern, max); + } else { + // we're in rotation, just go to the next + _rotationNum++; + if (_rotationNum > max) + _rotationNum = 0; + return new File(replace(pattern, _rotationNum)); + } + } + } + + /** + * Retrieve the first file, updating the rotation number accordingly + * + */ + private File getFirstFile(String pattern, int max) { + for (int i = 0; i < max; i++) { + File f = new File(replace(pattern, i)); + if (!f.exists()) { + _rotationNum = i; + return f; + } + } + + // all exist, pick the oldest to replace + File oldest = null; + for (int i = 0; i < max; i++) { + File f = new File(replace(pattern, i)); + if (oldest == null) { + oldest = f; + } else { + if (f.lastModified() < oldest.lastModified()) { + _rotationNum = i; + oldest = f; + } + } + } + return oldest; + } + + private static final String replace(String pattern, int num) { + char c[] = pattern.toCharArray(); + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < c.length; i++) { + if (c[i] != '#') + buf.append(c[i]); + else + buf.append(num); + } + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/util/NativeBigInteger.java b/core/java/src/net/i2p/util/NativeBigInteger.java new file mode 100644 index 000000000..97fe536a1 --- /dev/null +++ b/core/java/src/net/i2p/util/NativeBigInteger.java @@ -0,0 +1,122 @@ +package net.i2p.util; +/* + * 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 net.i2p.crypto.CryptoConstants; + +import java.math.BigInteger; +import java.util.Random; + +public class NativeBigInteger extends BigInteger { + private final static Log _log = new Log(NativeBigInteger.class); + private static boolean _nativeOk = false; + static { + try { + System.loadLibrary("jbigi"); + _nativeOk = true; + _log.info("Native BigInteger library jbigi loaded"); + } catch (UnsatisfiedLinkError ule) { + _nativeOk = false; + _log.warn("Native BigInteger library jbigi not loaded - using pure java", ule); + } + } + + /** + * calculate (base ^ exponent) % modulus. + * @param base big endian twos complement representation of the base (but it must be positive) + * @param exponent big endian twos complement representation of the exponent + * @param modulus big endian twos complement representation of the modulus + * @return big endian twos complement representation of (base ^ exponent) % modulus + */ + public native static byte[] nativeModPow(byte base[], byte exponent[], byte modulus[]); + + public NativeBigInteger(byte val[]) { + super(val); + } + public NativeBigInteger(int signum, byte magnitude[]) { + super(signum, magnitude); + } + public NativeBigInteger(int bitlen, int certainty, Random rnd) { + super(bitlen, certainty, rnd); + } + public NativeBigInteger(int numbits, Random rnd) { + super(numbits, rnd); + } + public NativeBigInteger(String val) { + super(val); + } + public NativeBigInteger(String val, int radix) { + super(val, radix); + } + + public BigInteger modPow(BigInteger exponent, BigInteger m) { + if (_nativeOk) + return new NativeBigInteger(nativeModPow(toByteArray(), exponent.toByteArray(), m.toByteArray())); + else + return super.modPow(exponent, m); + } + + public static void main(String args[]) { + if (_nativeOk) System.out.println("Native library loaded"); + else System.out.println("Native library NOT loaded"); + System.out.println("Warming up the random number generator..."); + RandomSource.getInstance().nextBoolean(); + System.out.println("Random number generator warmed up"); + + int numRuns = 100; + if (args.length == 1) { + try { numRuns = Integer.parseInt(args[0]); } catch (NumberFormatException nfe) {} + } + BigInteger jg = new BigInteger(CryptoConstants.elgg.toByteArray()); + BigInteger jp = new BigInteger(CryptoConstants.elgp.toByteArray()); + + long totalTime = 0; + long javaTime = 0; + + int runsProcessed = 0; + for (runsProcessed = 0; runsProcessed < numRuns; runsProcessed++) { + BigInteger bi = new BigInteger(2048, RandomSource.getInstance()); + NativeBigInteger g = new NativeBigInteger(CryptoConstants.elgg.toByteArray()); + NativeBigInteger p = new NativeBigInteger(CryptoConstants.elgp.toByteArray()); + NativeBigInteger k = new NativeBigInteger(1, bi.toByteArray()); + long beforeModPow = System.currentTimeMillis(); + BigInteger myValue = g.modPow(k, p); + long afterModPow = System.currentTimeMillis(); + BigInteger jval = jg.modPow(bi, jp); + long afterJavaModPow = System.currentTimeMillis(); + + totalTime += (afterModPow - beforeModPow); + javaTime += (afterJavaModPow - afterModPow); + if (!myValue.equals(jval)) { + _log.error("[" + runsProcessed + "]\tnative modPow != java modPow"); + _log.error("native modPow value: " + myValue.toString()); + _log.error("java modPow value: " + jval.toString()); + _log.error("run time: " + totalTime + "ms (" + (totalTime / (runsProcessed + 1)) + "ms each)"); + System.err.println("[" + runsProcessed + "]\tnative modPow != java modPow"); + break; + } else { + _log.debug("current run time: " + (afterModPow-beforeModPow) + "ms (total: " + totalTime + "ms, " + (totalTime / (runsProcessed + 1)) + "ms each)"); + } + } + _log.info(numRuns + " runs complete without any errors"); + _log.info("run time: " + totalTime + "ms (" + (totalTime / (runsProcessed + 1)) + "ms each)"); + if (numRuns == runsProcessed) + System.out.println(runsProcessed + " runs complete without any errors"); + else + System.out.println(runsProcessed + " runs until we got an error"); + + if (_nativeOk) { + System.out.println("native run time: \t" + totalTime + "ms (" + (totalTime / (runsProcessed + 1)) + "ms each)"); + System.out.println("java run time: \t" + javaTime + "ms (" + (javaTime / (runsProcessed + 1)) + "ms each)"); + System.out.println("native = " + ((totalTime*100.0d) / (double)javaTime) + "% of pure java time"); + } else { + System.out.println("java run time: \t" + javaTime + "ms (" + (javaTime / (runsProcessed + 1)) + "ms each)"); + } + } +} diff --git a/core/java/src/net/i2p/util/OrderedProperties.java b/core/java/src/net/i2p/util/OrderedProperties.java new file mode 100644 index 000000000..1b5695020 --- /dev/null +++ b/core/java/src/net/i2p/util/OrderedProperties.java @@ -0,0 +1,314 @@ +package net.i2p.util; +/* + * 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.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; + +import net.i2p.data.DataHelper; + +/** + * Properties map that has its keySet ordered consistently (via the key's lexicographical ordering). + * This is useful in environments where maps must stay the same order (e.g. for signature verification) + * This does NOT support remove against the iterators / etc. + * + */ +public class OrderedProperties extends Properties { + private final static Log _log = new Log(OrderedProperties.class); + /** ordered set of keys (strings) stored in the properties */ + private TreeSet _order; + /** simple key=value mapping of the actual data */ + private Map _data; + + /** lock this before touching _order or _data */ + private Object _lock = new Object(); + + public OrderedProperties() { + super(); + _order = new TreeSet(); + _data = new HashMap(); + } + + public boolean contains(Object value) { return containsValue(value); } + public boolean containsKey(Object key) { + synchronized (_lock) { + return _data.containsKey(key); + } + } + public boolean containsValue(Object value) { + synchronized (_lock) { + return _data.containsValue(value); + } + } + + public boolean equals(Object obj) { + if ( (obj != null) && (obj instanceof OrderedProperties) ) { + synchronized (_lock) { + return _data.equals(obj); + } + } else { + return false; + } + } + public int hashCode() { + synchronized (_lock) { + return _data.hashCode(); + } + } + public boolean isEmpty() { return size() == 0; } + + public String getProperty(String key) { return getProperty((Object)key); } + public Object get(Object key) { return getProperty(key); } + private String getProperty(Object key) { + if (key == null) return null; + synchronized (_lock) { + Object rv = _data.get(key); + if ( (rv != null) && (rv instanceof String) ) + return (String)rv; + else + return null; + } + } + + public Object setProperty(String key, String val) { + if ( (key == null) || (val == null) ) + throw new IllegalArgumentException("Null values are not supported"); + synchronized (_lock) { + _order.add(key); + Object rv = _data.put(key, val); + return rv; + } + } + + public Object put(Object key, Object val) { + if ( (key == null) || (val == null) ) + throw new NullPointerException("Null values or keys are not allowed"); + if ( !(key instanceof String) || !(val instanceof String) ) + throw new IllegalArgumentException("Key or value is not a string"); + return setProperty((String)key, (String)val); + } + + public void putAll(Map data) { + if (data == null) return; + for (Iterator iter = data.keySet().iterator(); iter.hasNext(); ) { + Object key = iter.next(); + Object val = data.get(key); + put(key, val); + } + } + + public Object clone() { + synchronized (_lock) { + OrderedProperties rv = new OrderedProperties(); + rv.putAll(this); + return rv; + } + } + + public void clear() { + synchronized (_lock) { + _order.clear(); + _data.clear(); + } + } + + public int size() { + synchronized (_lock) { return _order.size(); } + } + + public Object remove(Object key) { + synchronized (_lock) { + _order.remove(key); + Object rv = _data.remove(key); + return rv; + } + } + + public Set keySet() { + synchronized (_lock) { + return Collections.unmodifiableSortedSet((TreeSet)_order.clone()); + } + } + + public Set entrySet() { + synchronized (_lock) { + return Collections.unmodifiableSet(buildEntrySet((TreeSet)_order.clone())); + } + } + + public Collection values() { + synchronized (_lock) { + Collection values = new ArrayList(_data.size()); + for (Iterator iter = _data.values().iterator(); iter.hasNext();) { + values.add(iter.next()); + } + return values; + } + } + + public Enumeration elements() { return Collections.enumeration(values()); } + public Enumeration keys() { return Collections.enumeration(keySet()); } + public Enumeration propertyNames() { return Collections.enumeration(keySet()); } + + public void list(PrintStream out) {} + public void list(PrintWriter out) {} + public void load(InputStream in) {} + //public void save(OutputStream out, String header) {} + public void store(OutputStream out, String header) {} + + private Set buildEntrySet(Set data) { + TreeSet ts = new TreeSet(); + for (Iterator iter = data.iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = getProperty(key); + ts.add(new StringMapEntry(key, val)); + } + return ts; + } + + private static class StringMapEntry implements Map.Entry, Comparable { + private Object _key; + private Object _value; + public StringMapEntry(String key, String val) { + _key = key; + _value = val; + } + public Object getKey() { return _key; } + public Object getValue() { return _value; } + public Object setValue(Object value) { + Object old = _value; + _value = value; + return old; + } + + public int compareTo(Object o) { + if (o == null) return -1; + if (o instanceof StringMapEntry) + return ((String)getKey()).compareTo(((StringMapEntry)o).getKey()); + if (o instanceof String) + return ((String)getKey()).compareTo(o); + return -2; + } + public boolean equals(Object o) { + if (o == null) return false; + if (!(o instanceof StringMapEntry)) return false; + StringMapEntry e = (StringMapEntry)o; + return DataHelper.eq(e.getKey(), getKey()) && + DataHelper.eq(e.getValue(), getValue()); + } + } + + /// + /// tests + /// + + public static void main(String args[]) { + test(new OrderedProperties()); + _log.debug("After ordered"); + //test(new Properties()); + //System.out.println("After normal"); + test2(); + testThrash(); + } + + private static void test2() { + OrderedProperties p = new OrderedProperties(); + p.setProperty("a", "b"); + p.setProperty("c", "d"); + OrderedProperties p2 = new OrderedProperties(); + try { + p2.putAll(p); + } catch (Throwable t) { + t.printStackTrace(); + } + _log.debug("After test2"); + } + + private static void test(Properties p) { + for (int i = 0; i < 10; i++) + p.setProperty(i + "asdfasdfasdf", "qwerasdfqwer"); + for (Iterator iter = p.keySet().iterator(); iter.hasNext();) { + String key = (String)iter.next(); + String val = p.getProperty(key); + _log.debug("[" + key + "] = [" + val + "]"); + } + p.remove(4 + "asdfasdfasdf"); + _log.debug("After remove"); + for (Iterator iter = p.keySet().iterator(); iter.hasNext();) { + String key = (String)iter.next(); + String val = p.getProperty(key); + _log.debug("[" + key + "] = [" + val + "]"); + } + try { + p.put("nullVal", null); + _log.debug("Null put did NOT fail!"); + } catch (NullPointerException npe) { + _log.debug("Null put failed correctly"); + } + } + + /** + * Set 100 concurrent threads trying to do some operations against a single + * OrderedProperties object a thousand times. Hopefully this will help + * flesh out any synchronization issues. + * + */ + private static void testThrash() { + OrderedProperties prop = new OrderedProperties(); + for (int i = 0; i < 100; i++) + prop.setProperty(i+"", i+" value"); + _log.debug("Thrash properties built"); + for (int i = 0; i < 100; i++) + thrash(prop, i); + } + + private static void thrash(Properties props, int i) { + I2PThread t = new I2PThread(new Thrash(props)); + t.setName("Thrash" + i); + t.start(); + } + + private static class Thrash implements Runnable { + private Properties _props; + public Thrash(Properties props) { + _props = props; + } + public void run() { + int numRuns = 1000; + _log.debug("Begin thrashing " + numRuns + " times"); + for (int i = 0; i < numRuns; i++) { + Set keys = _props.keySet(); + //_log.debug("keySet fetched"); + int cur = 0; + for (Iterator iter = keys.iterator(); iter.hasNext(); ) { + Object o = iter.next(); + Object val = _props.get(o); + //_log.debug("Value " + cur + " fetched"); + cur++; + } + //_log.debug("Values fetched"); + int size = _props.size(); + _log.debug("Size calculated"); + } + _log.debug("Done thrashing " + numRuns + " times"); + } + } +} diff --git a/core/java/src/net/i2p/util/RandomSource.java b/core/java/src/net/i2p/util/RandomSource.java new file mode 100644 index 000000000..47932ae61 --- /dev/null +++ b/core/java/src/net/i2p/util/RandomSource.java @@ -0,0 +1,60 @@ +package net.i2p.util; +/* + * 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.SecureRandom; + +/** + * Singleton for whatever PRNG i2p uses. + * + * @author jrandom + */ +public class RandomSource extends SecureRandom { + private final static RandomSource _random = new RandomSource(); + private RandomSource() { + super(); + } + public static RandomSource getInstance() { return _random; } + + /** + * According to the java docs (http://java.sun.com/j2se/1.4.1/docs/api/java/util/Random.html#nextInt(int)) + * nextInt(n) should return a number between 0 and n inclusive. However, their pseudocode, + * as well as sun's, kaffe's, and classpath's implementation INCLUDES NEGATIVE VALUES. + * WTF. Ok, so we're going to have it return between 0 and n, since thats what + * it has been used for. + * + */ + public int nextInt(int n) { + if (n == 0) return 0; + int val = super.nextInt(n); + if (val < 0) + val = 0 - val; + return val; + } + + /** + * Like the modified nextInt, nextLong(n) returns a random number from 0 through n, + * inclusive. + */ + public long nextLong(long n) { + long v = super.nextLong(); + if (v < 0) + v = 0 - v; + if (v > n) + v = v % n; + return v; + } + + /** synchronized for older versions of kaffe */ + public void nextBytes(byte bytes[]) { + synchronized (this) { + super.nextBytes(bytes); + } + } +}