diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java
index 076ee99fab457ab68b32abe0d0381a871ac99853..6a206924b4c9d462c741f49c3b46ccdd77f18a2b 100644
--- a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java
+++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java
@@ -26,6 +26,7 @@ public class I2PSocketManagerFactory {
 
     public static final String PROP_MANAGER = "i2p.streaming.manager";
     public static final String DEFAULT_MANAGER = "net.i2p.client.streaming.I2PSocketManagerImpl";
+    //public static final String DEFAULT_MANAGER = "net.i2p.client.streaming.I2PSocketManagerFull";
     
     /**
      * Create a socket manager using a brand new destination connected to the
@@ -83,9 +84,11 @@ public class I2PSocketManagerFactory {
         if (true) {
             // for the old streaming lib
             opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_GUARANTEED);
+            //opts.setProperty("tunnels.depthInbound", "0");
         } else {
             // for new streaming lib:
             opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT);
+            //opts.setProperty("tunnels.depthInbound", "0");
         }
 
         opts.setProperty(I2PClient.PROP_TCP_HOST, i2cpHost);
diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkClient.java b/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkClient.java
index 4ef7f2cfc51905e1e4c31cc1234206d7f9845754..0a8d29cae9c01a5254dc5f5066e517464118ead8 100644
--- a/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkClient.java
+++ b/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkClient.java
@@ -8,6 +8,7 @@ import java.io.OutputStream;
 import java.net.ConnectException;
 import java.net.NoRouteToHostException;
 
+import java.util.Properties;
 import java.util.Random;
 
 import net.i2p.I2PAppContext;
@@ -26,6 +27,8 @@ public class StreamSinkClient {
     private int _sendSize;
     private int _writeDelay;
     private String _peerDestFile;
+    private String _i2cpHost;
+    private int _i2cpPort;
 
     
     /**
@@ -35,6 +38,11 @@ public class StreamSinkClient {
      * @param serverDestFile file containing the StreamSinkServer's binary Destination
      */
     public StreamSinkClient(int sendSize, int writeDelayMs, String serverDestFile) {
+        this(null, -1, sendSize, writeDelayMs, serverDestFile);
+    }
+    public StreamSinkClient(String i2cpHost, int i2cpPort, int sendSize, int writeDelayMs, String serverDestFile) {
+        _i2cpHost = i2cpHost;
+        _i2cpPort = i2cpPort;
         _sendSize = sendSize;
         _writeDelay = writeDelayMs;
         _peerDestFile = serverDestFile;
@@ -46,7 +54,11 @@ public class StreamSinkClient {
      *
      */
     public void runClient() {
-        I2PSocketManager mgr = I2PSocketManagerFactory.createManager();
+        I2PSocketManager mgr = null;
+        if (_i2cpHost != null)
+            mgr = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, new Properties());
+        else
+            mgr = I2PSocketManagerFactory.createManager();
         Destination peer = null;
         FileInputStream fis = null;
         try { 
@@ -81,9 +93,9 @@ public class StreamSinkClient {
                     try { Thread.sleep(_writeDelay); } catch (InterruptedException ie) {}
                 }   
             }
+            sock.close();
             long afterSending = System.currentTimeMillis();
             System.out.println("Sent " + _sendSize + "KB in " + (afterSending-beforeSending) + "ms");
-            sock.close();
         } catch (InterruptedIOException iie) {
             _log.error("Timeout connecting to the peer", iie);
             return;
@@ -103,7 +115,7 @@ public class StreamSinkClient {
     }
 
     /**
-     * Fire up the client.  <code>Usage: StreamSinkClient sendSizeKB writeDelayMs serverDestFile</code> <br />
+     * Fire up the client.  <code>Usage: StreamSinkClient [i2cpHost i2cpPort] sendSizeKB writeDelayMs serverDestFile</code> <br />
      * <ul>
      *  <li><b>sendSizeKB</b>: how many KB to send</li>
      *  <li><b>writeDelayMs</b>: how long to wait between each .write (0 for no delay)</li>
@@ -111,25 +123,40 @@ public class StreamSinkClient {
      * </ul>
      */
     public static void main(String args[]) {
-        if (args.length != 3) {
-            System.out.println("Usage: StreamSinkClient sendSizeKB writeDelayMs serverDestFile");
-        } else {
-            int sendSizeKB = -1;
-            int writeDelayMs = -1;
-            try {
-                sendSizeKB = Integer.parseInt(args[0]);
-            } catch (NumberFormatException nfe) {
-                System.err.println("Send size invalid [" + args[0] + "]");
-                return;
-            }
-            try {
-                writeDelayMs = Integer.parseInt(args[1]);
-            } catch (NumberFormatException nfe) {
-                System.err.println("Write delay ms invalid [" + args[1] + "]");
-                return;
-            }
-            StreamSinkClient client = new StreamSinkClient(sendSizeKB, writeDelayMs, args[2]);
-            client.runClient();
+        StreamSinkClient client = null;
+        int sendSizeKB = -1;
+        int writeDelayMs = -1;
+        
+        switch (args.length) {
+            case 3:
+                try {
+                    sendSizeKB = Integer.parseInt(args[0]);
+                } catch (NumberFormatException nfe) {
+                    System.err.println("Send size invalid [" + args[0] + "]");
+                    return;
+                }
+                try {
+                    writeDelayMs = Integer.parseInt(args[1]);
+                } catch (NumberFormatException nfe) {
+                    System.err.println("Write delay ms invalid [" + args[1] + "]");
+                    return;
+                }
+                client = new StreamSinkClient(sendSizeKB, writeDelayMs, args[2]);
+                break;
+            case 5:
+                try { 
+                    int port = Integer.parseInt(args[1]);
+                    sendSizeKB = Integer.parseInt(args[2]);
+                    writeDelayMs = Integer.parseInt(args[3]);
+                    client = new StreamSinkClient(args[0], port, sendSizeKB, writeDelayMs, args[4]);
+                } catch (NumberFormatException nfe) {
+                    System.err.println("arg error");
+                }
+                break;
+            default: 
+                System.out.println("Usage: StreamSinkClient [i2cpHost i2cpPort] sendSizeKB writeDelayMs serverDestFile");
         }
+        if (client != null)
+            client.runClient();
     }
 }
diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkSend.java b/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkSend.java
index e7398e5b64dcd3434f7d232ee84a1e59f925c454..6b43fb0c4fef4e1838dd2d3aa354e0534acf6413 100644
--- a/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkSend.java
+++ b/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkSend.java
@@ -85,9 +85,9 @@ public class StreamSinkSend {
                 }   
             }
             fis.close();
+            sock.close();
             long afterSending = System.currentTimeMillis();
             System.out.println("Sent " + (size / 1024) + "KB in " + (afterSending-beforeSending) + "ms");
-            sock.close();
         } catch (InterruptedIOException iie) {
             _log.error("Timeout connecting to the peer", iie);
             return;
diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkServer.java b/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkServer.java
index 8567f29765a74ae8f7bfcc3fb1e305b76f683886..f9aae66e161b46b220521dd396c4d27c1146f69c 100644
--- a/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkServer.java
+++ b/apps/ministreaming/java/src/net/i2p/client/streaming/StreamSinkServer.java
@@ -6,6 +6,7 @@ import java.io.IOException;
 import java.io.InputStream;
 
 import java.net.ConnectException;
+import java.util.Properties;
 
 import net.i2p.I2PAppContext;
 import net.i2p.I2PException;
@@ -23,6 +24,8 @@ public class StreamSinkServer {
     private Log _log;
     private String _sinkDir;
     private String _destFile;
+    private String _i2cpHost;
+    private int _i2cpPort;
     
     /**
      * Create but do not start the streaming server.  
@@ -31,8 +34,13 @@ public class StreamSinkServer {
      * @param ourDestFile filename to write our binary destination to
      */
     public StreamSinkServer(String sinkDir, String ourDestFile) {
+        this(sinkDir, ourDestFile, null, -1);
+    }
+    public StreamSinkServer(String sinkDir, String ourDestFile, String i2cpHost, int i2cpPort) {
         _sinkDir = sinkDir;
         _destFile = ourDestFile;
+        _i2cpHost = i2cpHost;
+        _i2cpPort = i2cpPort;
         _log = I2PAppContext.getGlobalContext().logManager().getLog(StreamSinkServer.class);
     }
     
@@ -42,7 +50,11 @@ public class StreamSinkServer {
      * 
      */
     public void runServer() {
-        I2PSocketManager mgr = I2PSocketManagerFactory.createManager();
+        I2PSocketManager mgr = null;
+        if (_i2cpHost != null)
+            mgr = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, new Properties());
+        else 
+            mgr = I2PSocketManagerFactory.createManager();
         Destination dest = mgr.getSession().getMyDestination();
         System.out.println("Listening for connections on: " + dest.calculateHash().toBase64());
         FileOutputStream fos = null;
@@ -95,6 +107,7 @@ public class StreamSinkServer {
                     sink.mkdirs();
                 File cur = File.createTempFile("clientSink", ".dat", sink);
                 _fos = new FileOutputStream(cur);
+                System.out.println("Writing to " + cur.getAbsolutePath());
             } catch (IOException ioe) {
                 _log.error("Error creating sink", ioe);
                 _fos = null;
@@ -121,18 +134,30 @@ public class StreamSinkServer {
     }
     
     /**
-     * Fire up the streaming server.  <code>Usage: StreamSinkServer sinkDir ourDestFile</code><br />
+     * Fire up the streaming server.  <code>Usage: StreamSinkServer [i2cpHost i2cpPort] sinkDir ourDestFile</code><br />
      * <ul>
      *  <li><b>sinkDir</b>: Directory to store received files in</li>
      *  <li><b>ourDestFile</b>: filename to write our binary destination to</li>
      * </ul>
      */
     public static void main(String args[]) {
-        if (args.length != 2) {
-            System.out.println("Usage: StreamSinkServer sinkDir ourDestFile");
-        } else {
-            StreamSinkServer server = new StreamSinkServer(args[0], args[1]);
-            server.runServer();
+        StreamSinkServer server = null;
+        switch (args.length) {
+            case 2:
+                server = new StreamSinkServer(args[0], args[1]);
+                break;
+            case 4:
+                try { 
+                    int port = Integer.parseInt(args[1]);
+                    server = new StreamSinkServer(args[2], args[3], args[0], port);
+                } catch (NumberFormatException nfe) {
+                    System.out.println("Usage: StreamSinkServer [i2cpHost i2cpPort] sinkDir ourDestFile");
+                }
+                break;
+            default:
+                System.out.println("Usage: StreamSinkServer [i2cpHost i2cpPort] sinkDir ourDestFile");
         }
+        if (server != null)
+            server.runServer();
     }
 }
diff --git a/core/java/src/net/i2p/client/I2CPMessageProducer.java b/core/java/src/net/i2p/client/I2CPMessageProducer.java
index f074e6e7e3187e8c3a60c4db0bb114bbf342ae36..c9b16177c89017ce7f61f436552ba2cfa51f0c1d 100644
--- a/core/java/src/net/i2p/client/I2CPMessageProducer.java
+++ b/core/java/src/net/i2p/client/I2CPMessageProducer.java
@@ -109,8 +109,8 @@ class I2CPMessageProducer {
         // generateNewTags would only generate tags if necessary
 
         data.setEncryptedData(encr);
-        _log.debug("Encrypting the payload to public key " + dest.getPublicKey().toBase64() + "\nPayload: "
-                   + data.calculateHash());
+        //_log.debug("Encrypting the payload to public key " + dest.getPublicKey().toBase64() + "\nPayload: "
+        //           + data.calculateHash());
         return data;
     }
 
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java
index 7d9ed1e707f6c41f58f71fce62139dece9694c3c..0e5724616bdd84f2e880ed7d17889708c174287f 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl2.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java
@@ -114,17 +114,21 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         Set sentTags = null;
         int oldTags = _context.sessionKeyManager().getAvailableTags(dest.getPublicKey(), key);
         long availTimeLeft = _context.sessionKeyManager().getAvailableTimeLeft(dest.getPublicKey(), key);
-        if (oldTags < 10) {
-            sentTags = createNewTags(50);
-            //_log.error("** sendBestEffort only had " + oldTags + " adding 50");
-        } else if (availTimeLeft < 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(getPrefix() + "Tags are almost expired, adding 50 new ones");
-            //_log.error("** sendBestEffort available time left " + availTimeLeft);
-        } else {
-            //_log.error("sendBestEffort old tags: " + oldTags + " available time left: " + availTimeLeft);
+        
+        if ( (tagsSent == null) || (tagsSent.size() <= 0) ) {
+            if (oldTags < 10) {
+                sentTags = createNewTags(50);
+                //_log.error("** sendBestEffort only had " + oldTags + " adding 50");
+            } else if (availTimeLeft < 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(getPrefix() + "Tags are almost expired, adding 50 new ones");
+                //_log.error("** sendBestEffort available time left " + availTimeLeft);
+            } else {
+                //_log.error("sendBestEffort old tags: " + oldTags + " available time left: " + availTimeLeft);
+            }
         }
+        
         SessionKey newKey = null;
         if (false) // rekey
             newKey = _context.keyGenerator().generateSessionKey();
diff --git a/core/java/src/net/i2p/client/MessageStatusMessageHandler.java b/core/java/src/net/i2p/client/MessageStatusMessageHandler.java
index e16bccc56456c5e2138b04eed6de8033a2c1ec99..e32f14e7245e7f10cc6cc58f79973836ff2c29e8 100644
--- a/core/java/src/net/i2p/client/MessageStatusMessageHandler.java
+++ b/core/java/src/net/i2p/client/MessageStatusMessageHandler.java
@@ -13,13 +13,13 @@ import net.i2p.I2PAppContext;
 import net.i2p.data.i2cp.I2CPMessage;
 import net.i2p.data.i2cp.MessageStatusMessage;
 import net.i2p.data.i2cp.ReceiveMessageBeginMessage;
+import net.i2p.util.Log;
 
 /**
  * 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(I2PAppContext context) {
@@ -28,41 +28,44 @@ class MessageStatusMessageHandler extends HandlerImpl {
 
     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)))
+        if (I2PClient.PROP_RELIABILITY_GUARANTEED.equals(session.getOptions().getProperty(I2PClient.PROP_RELIABILITY,
+                                                                                          I2PClient.PROP_RELIABILITY_BEST_EFFORT)))
             skipStatus = false;
-        _log.debug("Handle message " + message);
+        if (_log.shouldLog(Log.DEBUG))
+            _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());
+            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:
+                if (_log.shouldLog(Log.INFO))
+                    _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:
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Message delivery FAILED for message " + msg.getMessageId());
+                //if (!skipStatus)
+                session.receiveStatus(msg.getMessageId().getMessageId(), msg.getNonce(), msg.getStatus());
+                return;
+            default:
+                if (_log.shouldLog(Log.ERROR))
+                    _log.error("Invalid message delivery status received: " + msg.getStatus());
         }
     }
 }
\ No newline at end of file
diff --git a/core/java/src/net/i2p/crypto/AESEngine.java b/core/java/src/net/i2p/crypto/AESEngine.java
index 2d474bffb026310271faa069a98932fbc03853ed..3b6b4e9eea0dca2dbaf45773339198f459844d8d 100644
--- a/core/java/src/net/i2p/crypto/AESEngine.java
+++ b/core/java/src/net/i2p/crypto/AESEngine.java
@@ -9,8 +9,6 @@ package net.i2p.crypto;
  *
  */
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 
 import net.i2p.I2PAppContext;
@@ -52,25 +50,26 @@ public class AESEngine {
     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);
+        int size = Hash.HASH_LENGTH 
+                 + 4 // sizeof(payload)
+                 + payload.length;
+        int padding = ElGamalAESEngine.getPaddingSize(size, paddedSize);
+        
+        byte data[] = new byte[size + padding];
         Hash h = _context.sha().calculateHash(iv);
-        try {
-            h.writeBytes(baos);
-            DataHelper.writeLong(baos, 4, payload.length);
-            baos.write(payload);
-            byte tv[] = baos.toByteArray();
-            baos.write(ElGamalAESEngine.getPadding(_context, 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;
-        }
-        byte orig[] = baos.toByteArray();
-        byte rv[] = new byte[orig.length];
-        encrypt(orig, 0, rv, 0, sessionKey, iv, rv.length);
-        return rv;
+        
+        int cur = 0;
+        System.arraycopy(h.getData(), 0, data, cur, Hash.HASH_LENGTH);
+        cur += Hash.HASH_LENGTH;
+        DataHelper.toLong(data, cur, 4, payload.length);
+        cur += 4;
+        System.arraycopy(payload, 0, data, cur, payload.length);
+        cur += payload.length;
+        byte paddingData[] = ElGamalAESEngine.getPadding(_context, size, paddedSize);
+        System.arraycopy(paddingData, 0, data, cur, paddingData.length);
+        
+        encrypt(data, 0, data, 0, sessionKey, iv, data.length);
+        return data;
     }
 
     public byte[] safeDecrypt(byte payload[], SessionKey sessionKey, byte iv[]) {
@@ -82,31 +81,29 @@ public class AESEngine {
             _log.error("Error decrypting the data - payload " + payload.length + " decrypted to null");
             return null;
         }
-        ByteArrayInputStream bais = new ByteArrayInputStream(decr);
-        Hash h = _context.sha().calculateHash(iv);
-        try {
-            Hash rh = new Hash();
-            rh.readBytes(bais);
-            if (!h.equals(rh)) {
+
+        int cur = 0;
+        byte h[] = _context.sha().calculateHash(iv).getData();
+        for (int i = 0; i < Hash.HASH_LENGTH; i++) {
+            if (decr[i] != h[i]) {
                 _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);
+        }
+        cur += Hash.HASH_LENGTH;
+        
+        long len = DataHelper.fromLong(decr, cur, 4);
+        cur += 4;
+        
+        if (cur + len > decr.length) {
+            _log.error("Not enough to read");
             return null;
         }
+        
+        byte data[] = new byte[(int)len];
+        System.arraycopy(decr, cur, data, 0, (int)len);
+        return data;
     }
 
 
diff --git a/core/java/src/net/i2p/crypto/AESOutputStream.java b/core/java/src/net/i2p/crypto/AESOutputStream.java
index 3b28aac0e4a2af730ba6d308b988ab0eb7b99fb0..3deea7f496d36604c61716003ad1adea63aea1ed 100644
--- a/core/java/src/net/i2p/crypto/AESOutputStream.java
+++ b/core/java/src/net/i2p/crypto/AESOutputStream.java
@@ -9,7 +9,6 @@ package net.i2p.crypto;
  *
  */
 
-import java.io.ByteArrayOutputStream;
 import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
diff --git a/core/java/src/net/i2p/crypto/CryptixAESEngine.java b/core/java/src/net/i2p/crypto/CryptixAESEngine.java
index 8b1454fef649f93d92b2f99b22e4e89e050a353d..7cad00105a16ff1da92a917417ba08adea7d4004 100644
--- a/core/java/src/net/i2p/crypto/CryptixAESEngine.java
+++ b/core/java/src/net/i2p/crypto/CryptixAESEngine.java
@@ -29,10 +29,12 @@ public class CryptixAESEngine extends AESEngine {
     private final static CryptixRijndael_Algorithm _algo = new CryptixRijndael_Algorithm();
     private final static boolean USE_FAKE_CRYPTO = false;
     private final static byte FAKE_KEY = 0x2A;
+    private CryptixAESKeyCache _cache;
     
     public CryptixAESEngine(I2PAppContext context) {
         super(context);
         _log = context.logManager().getLog(CryptixAESEngine.class);
+        _cache = new CryptixAESKeyCache();
     }
     
     public void encrypt(byte payload[], int payloadIndex, byte out[], int outIndex, SessionKey sessionKey, byte iv[], int length) {
@@ -65,8 +67,13 @@ public class CryptixAESEngine extends AESEngine {
 
     public void decrypt(byte payload[], int payloadIndex, byte out[], int outIndex, SessionKey sessionKey, byte iv[], int length) {
         if ((iv== null) || (payload == null) || (payload.length <= 0) || (sessionKey == null)
-            || (iv.length != 16)) 
+            || (iv.length != 16) ) 
             throw new IllegalArgumentException("bad setup");
+        else if (out == null)
+            throw new IllegalArgumentException("out is null");
+        else if (out.length - outIndex < length)
+            throw new IllegalArgumentException("out is too small (out.length=" + out.length 
+                                               + " outIndex=" + outIndex + " length=" + length);
 
         if (USE_FAKE_CRYPTO) {
             _log.warn("AES Crypto disabled!  Using trivial XOR");
@@ -74,23 +81,26 @@ public class CryptixAESEngine extends AESEngine {
             return ;
         }
 
-        int numblock = payload.length / 16;
-        if (payload.length % 16 != 0) numblock++;
+        int numblock = length / 16;
+        if (length % 16 != 0) numblock++;
 
-        decryptBlock(payload, 0, sessionKey, out, 0);
-		DataHelper.xor(out, 0, iv, 0, out, 0, 16);
+        decryptBlock(payload, payloadIndex, sessionKey, out, outIndex);
+		DataHelper.xor(out, outIndex, iv, 0, out, outIndex, 16);
         for (int x = 1; x < numblock; x++) {
-            decryptBlock(payload, x * 16, sessionKey, out, x * 16);
-            DataHelper.xor(out, x * 16, payload, (x - 1) * 16, out, x * 16, 16);
+            decryptBlock(payload, payloadIndex + (x * 16), sessionKey, out, outIndex + (x * 16));
+            DataHelper.xor(out, outIndex + x * 16, payload, payloadIndex + (x - 1) * 16, out, outIndex + x * 16, 16);
         }
     }
 
     final void encryptBlock(byte payload[], int inIndex, SessionKey sessionKey, byte out[], int outIndex) {
+        CryptixAESKeyCache.KeyCacheEntry keyData = _cache.acquireKey();
         try {
-            Object key = CryptixRijndael_Algorithm.makeKey(sessionKey.getData(), 16);
+            Object key = CryptixRijndael_Algorithm.makeKey(sessionKey.getData(), 16, keyData);
             CryptixRijndael_Algorithm.blockEncrypt(payload, out, inIndex, outIndex, key, 16);
         } catch (InvalidKeyException ike) {
             _log.error("Invalid key", ike);
+        } finally {
+            _cache.releaseKey(keyData);
         }
     }
 
@@ -100,11 +110,20 @@ public class CryptixAESEngine extends AESEngine {
      * @return unencrypted data
      */
     final void decryptBlock(byte payload[], int inIndex, SessionKey sessionKey, byte rv[], int outIndex) {
+        if ( (payload == null) || (rv == null) )
+            throw new IllegalArgumentException("null block args [payload=" + payload + " rv="+rv);
+        if (payload.length - inIndex > rv.length - outIndex)
+            throw new IllegalArgumentException("bad block args [payload.len=" + payload.length 
+                                               + " inIndex=" + inIndex + " rv.len=" + rv.length 
+                                               + " outIndex="+outIndex);
+		CryptixAESKeyCache.KeyCacheEntry keyData = _cache.acquireKey();
         try {
-            Object key = CryptixRijndael_Algorithm.makeKey(sessionKey.getData(), 16);
+            Object key = CryptixRijndael_Algorithm.makeKey(sessionKey.getData(), 16, keyData);
             CryptixRijndael_Algorithm.blockDecrypt(payload, rv, inIndex, outIndex, key, 16);
         } catch (InvalidKeyException ike) {
             _log.error("Invalid key", ike);
+        } finally {
+            _cache.releaseKey(keyData);
         }
     }
     
@@ -113,6 +132,8 @@ public class CryptixAESEngine extends AESEngine {
         try {
             testEDBlock(ctx);
             testED(ctx);
+            testFake(ctx);
+            testNull(ctx);
         } catch (Exception e) {
             e.printStackTrace();
         }
@@ -134,6 +155,42 @@ public class CryptixAESEngine extends AESEngine {
         else
             System.out.println("full D(E(orig)) == orig");
     }
+    private static void testFake(I2PAppContext ctx) {
+        SessionKey key = ctx.keyGenerator().generateSessionKey();
+        SessionKey wrongKey = ctx.keyGenerator().generateSessionKey();
+        byte iv[] = new byte[16];
+        byte orig[] = new byte[128];
+        byte encrypted[] = new byte[128];
+        byte decrypted[] = new byte[128];
+        ctx.random().nextBytes(iv);
+        ctx.random().nextBytes(orig);
+        CryptixAESEngine aes = new CryptixAESEngine(ctx);
+        aes.encrypt(orig, 0, encrypted, 0, key, iv, orig.length);
+        aes.decrypt(encrypted, 0, decrypted, 0, wrongKey, iv, encrypted.length);
+        if (DataHelper.eq(decrypted,orig))
+            throw new RuntimeException("full D(E(orig)) == orig when we used the wrong key!");
+        else
+            System.out.println("full D(E(orig)) != orig when we used the wrong key");
+    }
+    private static void testNull(I2PAppContext ctx) {
+        SessionKey key = ctx.keyGenerator().generateSessionKey();
+        SessionKey wrongKey = ctx.keyGenerator().generateSessionKey();
+        byte iv[] = new byte[16];
+        byte orig[] = new byte[128];
+        byte encrypted[] = new byte[128];
+        byte decrypted[] = new byte[128];
+        ctx.random().nextBytes(iv);
+        ctx.random().nextBytes(orig);
+        CryptixAESEngine aes = new CryptixAESEngine(ctx);
+        aes.encrypt(orig, 0, encrypted, 0, key, iv, orig.length);
+        try { 
+            aes.decrypt(null, 0, null, 0, wrongKey, iv, encrypted.length);
+        } catch (IllegalArgumentException iae) {
+            return;
+        } 
+        
+        throw new RuntimeException("full D(E(orig)) didn't fail when we used null!");
+    }
     private static void testEDBlock(I2PAppContext ctx) {
         SessionKey key = ctx.keyGenerator().generateSessionKey();
         byte iv[] = new byte[16];
diff --git a/core/java/src/net/i2p/crypto/CryptixAESKeyCache.java b/core/java/src/net/i2p/crypto/CryptixAESKeyCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..780360ed19a7352032f539ef60f058dbd48d5122
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/CryptixAESKeyCache.java
@@ -0,0 +1,70 @@
+package net.i2p.crypto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Cache the objects used in CryptixRijndael_Algorithm.makeKey to reduce
+ * memory churn.  The KeyCacheEntry should be held onto as long as the 
+ * data referenced in it is needed (which often is only one or two lines
+ * of code)
+ *
+ */
+final class CryptixAESKeyCache {
+    private List _availableKeys;
+    
+    private static final int KEYSIZE = 32; // 256bit AES
+    private static final int BLOCKSIZE = 16;
+    private static final int ROUNDS = CryptixRijndael_Algorithm.getRounds(KEYSIZE, BLOCKSIZE);
+    private static final int BC = BLOCKSIZE / 4;
+    private static final int KC = KEYSIZE / 4; 
+    
+    public CryptixAESKeyCache() {
+        _availableKeys = new ArrayList(64);
+        for (int i = 0; i < 64; i++) {
+            _availableKeys.add(createNew());
+        }
+    }
+    
+    /**
+     * Get the next available structure, either from the cache or a brand new one
+     *
+     */
+    public final KeyCacheEntry acquireKey() {
+        synchronized (_availableKeys) {
+            if (_availableKeys.size() > 0)
+                return (KeyCacheEntry)_availableKeys.remove(0);
+        }
+        return createNew();
+    }
+    
+    /**
+     * Put this structure back onto the available cache for reuse
+     *
+     */
+    public final void releaseKey(KeyCacheEntry key) {
+        synchronized (_availableKeys) {
+            _availableKeys.add(key);
+        }
+    }
+    
+    private static final KeyCacheEntry createNew() {
+        KeyCacheEntry e = new KeyCacheEntry();
+        e.Ke = new int[ROUNDS + 1][BC]; // encryption round keys
+        e.Kd = new int[ROUNDS + 1][BC]; // decryption round keys
+        e.tk = new int[KC];
+        e.key = new Object[] { e.Ke, e.Kd };
+        return e;
+    }
+    
+    /**
+     * all the data alloc'ed in a makeKey call
+     */
+    public static final class KeyCacheEntry {
+        int[][] Ke;
+        int[][] Kd;
+        int[]   tk;
+        
+        Object[] key;
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/CryptixRijndael_Algorithm.java b/core/java/src/net/i2p/crypto/CryptixRijndael_Algorithm.java
index 3546d29f7f4a5e5d9ce6f20c603624c54e5575b4..58444d0236f9c4f4770a909afeedcbda76b8b3a3 100644
--- a/core/java/src/net/i2p/crypto/CryptixRijndael_Algorithm.java
+++ b/core/java/src/net/i2p/crypto/CryptixRijndael_Algorithm.java
@@ -453,6 +453,11 @@ public final class CryptixRijndael_Algorithm // implicit no-argument constructor
      * @param  sessionKey The session key to use for decryption.
      */
     public static final void blockDecrypt(byte[] in, byte[] result, int inOffset, int outOffset, Object sessionKey) {
+        if (in.length - inOffset > result.length - outOffset)
+            throw new IllegalArgumentException("result too small: in.len=" + in.length + " in.offset=" + inOffset
+                                               + " result.len=" + result.length + " result.offset=" + outOffset);
+        if (in.length - inOffset <= 15)
+            throw new IllegalArgumentException("data too small: " + in.length + " inOffset: " + inOffset);
         if (_RDEBUG) trace(_IN, "blockDecrypt(" + in + ", " + inOffset + ", " + sessionKey + ")");
         int[][] Kd = (int[][]) ((Object[]) sessionKey)[1]; // extract decryption round keys
         int ROUNDS = Kd.length - 1;
@@ -534,18 +539,31 @@ public final class CryptixRijndael_Algorithm // implicit no-argument constructor
      * @exception  InvalidKeyException  If the key is invalid.
      */
     public static final/* synchronized */Object makeKey(byte[] k, int blockSize) throws InvalidKeyException {
+        return makeKey(k, blockSize, null);
+    }
+    public static final/* synchronized */Object makeKey(byte[] k, int blockSize, CryptixAESKeyCache.KeyCacheEntry keyData) 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[][] Ke = null; // new int[ROUNDS + 1][BC]; // encryption round keys
+        int[][] Kd = null; // 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[] tk = null; // new int[KC];
         int i, j;
+        
+        if (keyData == null) {
+            Ke = new int[ROUNDS + 1][BC];
+            Kd = new int[ROUNDS + 1][BC];
+            tk = new int[KC];
+        } else {
+            Ke = keyData.Ke;
+            Kd = keyData.Kd;
+            tk = keyData.tk;
+        }
 
         // copy user material bytes into temporary ints
         for (i = 0, j = 0; i < KC;)
@@ -604,7 +622,11 @@ public final class CryptixRijndael_Algorithm // implicit no-argument constructor
             }
         // assemble the encryption (Ke) and decryption (Kd) round keys into
         // one sessionKey object
-        Object[] sessionKey = new Object[] { Ke, Kd};
+        Object[] sessionKey = null;
+        if (keyData == null)
+            sessionKey = new Object[] { Ke, Kd};
+        else
+            sessionKey = keyData.key;
         if (_RDEBUG) trace(_OUT, "makeKey()");
         return sessionKey;
     }
diff --git a/core/java/src/net/i2p/crypto/ElGamalAESEngine.java b/core/java/src/net/i2p/crypto/ElGamalAESEngine.java
index 1b6e501ee3d84a2084d4547b857cd58b496b89a9..dc6f19a95dd467199c96abfaaae1c89cb3cfa9fb 100644
--- a/core/java/src/net/i2p/crypto/ElGamalAESEngine.java
+++ b/core/java/src/net/i2p/crypto/ElGamalAESEngine.java
@@ -10,7 +10,6 @@ package net.i2p.crypto;
  */
 
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -147,25 +146,19 @@ public class ElGamalAESEngine {
             System.arraycopy(data, 0, elgEncr, 514 - data.length, data.length);
         }
         byte elgDecr[] = _context.elGamalEngine().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);
+        if (elgDecr == null) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("decrypt returned null", new Exception("decrypt failed"));
             return null;
         }
-        // ignore the next 192 bytes
-        byte aesEncr[] = new byte[data.length - 514];
-        System.arraycopy(data, 514, aesEncr, 0, aesEncr.length);
+
+        byte preIV[] = null;
+        
+        byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
+        System.arraycopy(elgDecr, 0, key, 0, SessionKey.KEYSIZE_BYTES);
+        usedKey.setData(key);
+        preIV = new byte[32];
+        System.arraycopy(elgDecr, SessionKey.KEYSIZE_BYTES, preIV, 0, 32);
 
         //_log.debug("Pre IV for decryptNewSession: " + DataHelper.toString(preIV, 32));
         //_log.debug("SessionKey for decryptNewSession: " + DataHelper.toString(key.getData(), 32));
@@ -173,7 +166,7 @@ public class ElGamalAESEngine {
         byte iv[] = new byte[16];
         System.arraycopy(ivHash.getData(), 0, iv, 0, 16);
 
-        byte aesDecr[] = decryptAESBlock(aesEncr, usedKey, iv, null, foundTags, foundKey);
+        byte aesDecr[] = decryptAESBlock(data, 514, data.length-514, usedKey, iv, null, foundTags, foundKey);
 
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Decrypt with a NEW session successfull: # tags read = " + foundTags.size(),
@@ -203,8 +196,6 @@ public class ElGamalAESEngine {
                                          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 = _context.sha().calculateHash(preIV);
         byte iv[] = new byte[16];
         System.arraycopy(ivHash.getData(), 0, iv, 0, 16);
@@ -213,7 +204,7 @@ public class ElGamalAESEngine {
 
         //_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);
+        byte decrypted[] = decryptAESBlock(data, 32, data.length-32, 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))
@@ -244,12 +235,16 @@ public class ElGamalAESEngine {
      * @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
      */
-    byte[] decryptAESBlock(byte encrypted[], SessionKey key, byte iv[], byte sentTag[], Set foundTags,
-                                  SessionKey foundKey) throws DataFormatException {
+    byte[] decryptAESBlock(byte encrypted[], SessionKey key, byte iv[], 
+                           byte sentTag[], Set foundTags, SessionKey foundKey) throws DataFormatException {
+        return decryptAESBlock(encrypted, 0, encrypted.length, key, iv, sentTag, foundTags, foundKey);
+    }
+    byte[] decryptAESBlock(byte encrypted[], int offset, int encryptedLen, 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[] = new byte[encrypted.length];
-        _context.aes().decrypt(encrypted, 0, decrypted, 0, key, iv, encrypted.length);
+        byte decrypted[] = new byte[encryptedLen];
+        _context.aes().decrypt(encrypted, offset, decrypted, 0, key, iv, encryptedLen);
         //Hash h = _context.sha().calculateHash(decrypted);
         //_log.debug("Hash of entire aes block after decryption: \n" + DataHelper.toString(h.getData(), 32));
         try {
@@ -271,7 +266,7 @@ public class ElGamalAESEngine {
             }
             long len = DataHelper.readLong(bais, 4);
             //_log.debug("len: " + len);
-            if ((len < 0) || (len > encrypted.length)) throw new Exception("Invalid size of payload");
+            if ((len < 0) || (len > encryptedLen)) 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");
@@ -371,54 +366,45 @@ public class ElGamalAESEngine {
     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];
-            _context.random().nextBytes(preIV);
-            elgSrc.write(preIV);
-            byte rnd[] = new byte[158];
-            _context.random().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 = _context.clock().now();
-            byte elgEncr[] = _context.elGamalEngine().encrypt(elgSrc.toByteArray(), target);
-            long after = _context.clock().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 = _context.sha().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 = _context.clock().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;
+        byte elgSrcData[] = new byte[SessionKey.KEYSIZE_BYTES+32+158];
+        System.arraycopy(key.getData(), 0, elgSrcData, 0, SessionKey.KEYSIZE_BYTES);
+        byte preIV[] = new byte[32];
+        _context.random().nextBytes(preIV);
+        System.arraycopy(preIV, 0, elgSrcData, SessionKey.KEYSIZE_BYTES, 32);
+        byte rnd[] = new byte[158];
+        _context.random().nextBytes(rnd);
+        System.arraycopy(rnd, 0, elgSrcData, SessionKey.KEYSIZE_BYTES+32, 158);
+
+        //_log.debug("Pre IV for encryptNewSession: " + DataHelper.toString(preIV, 32));
+        //_log.debug("SessionKey for encryptNewSession: " + DataHelper.toString(key.getData(), 32));
+        long before = _context.clock().now();
+        byte elgEncr[] = _context.elGamalEngine().encrypt(elgSrcData, target);
+        long after = _context.clock().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 = _context.sha().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 = _context.clock().now();
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("after the elgEngine.encrypt took a total of " + (finish - after) + "ms");
+        return rv;
     }
 
     /**
@@ -445,11 +431,10 @@ public class ElGamalAESEngine {
         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;
+        byte aesEncr[] = encryptAESBlock(data, key, iv, tagsForDelivery, newKey, paddedSize, SessionTag.BYTE_LENGTH);
+        // that prepended SessionTag.BYTE_LENGTH bytes at the beginning of the buffer
+        System.arraycopy(rawTag, 0, aesEncr, 0, rawTag.length);
+        return aesEncr;
     }
 
     private final static Set EMPTY_SET = new HashSet();
@@ -469,52 +454,64 @@ public class ElGamalAESEngine {
      */
     final byte[] encryptAESBlock(byte data[], SessionKey key, byte[] iv, Set tagsForDelivery, SessionKey newKey,
                                         long paddedSize) {
+        return encryptAESBlock(data, key, iv, tagsForDelivery, newKey, paddedSize, 0);
+    }
+    final byte[] encryptAESBlock(byte data[], SessionKey key, byte[] iv, Set tagsForDelivery, SessionKey newKey,
+                                        long paddedSize, int prefixBytes) {
         //_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 = _context.sha().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(_context, len, paddedSize);
-            //_log.debug("padding length: " + padding.length);
-            aesSrc.write(padding);
-
-            byte aesUnencr[] = aesSrc.toByteArray();
-            //Hash h = _context.sha().calculateHash(aesUnencr);
-            //_log.debug("Hash of entire aes block before encryption: (len=" + aesUnencr.length + ")\n" + DataHelper.toString(h.getData(), 32));
-            byte aesEncr[] = new byte[aesUnencr.length];
-            _context.aes().encrypt(aesUnencr, 0, aesEncr, 0, key, iv, aesEncr.length);
-            //_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;
+        if (tagsForDelivery == null) tagsForDelivery = EMPTY_SET;
+        int size = 2 // sizeof(tags)
+                 + tagsForDelivery.size()
+                 + SessionTag.BYTE_LENGTH*tagsForDelivery.size()
+                 + 4 // payload length
+                 + Hash.HASH_LENGTH
+                 + (newKey == null ? 1 : 1 + SessionKey.KEYSIZE_BYTES)
+                 + data.length;
+        int totalSize = size + getPaddingSize(size, paddedSize);
+
+        byte aesData[] = new byte[totalSize + prefixBytes];
+
+        int cur = prefixBytes;
+        DataHelper.toLong(aesData, cur, 2, tagsForDelivery.size());
+        cur += 2;
+        for (Iterator iter = tagsForDelivery.iterator(); iter.hasNext();) {
+            SessionTag tag = (SessionTag) iter.next();
+            System.arraycopy(tag.getData(), 0, aesData, cur, SessionTag.BYTE_LENGTH);
+            cur += SessionTag.BYTE_LENGTH;
+        }
+        //_log.debug("# tags created, registered, and written: " + tags.size());
+        DataHelper.toLong(aesData, cur, 4, data.length);
+        cur += 4;
+        //_log.debug("data length: " + data.length);
+        Hash hash = _context.sha().calculateHash(data);
+        System.arraycopy(hash.getData(), 0, aesData, cur, Hash.HASH_LENGTH);
+        cur += Hash.HASH_LENGTH;
+
+        //_log.debug("hash of data: " + DataHelper.toString(hash.getData(), 32));
+        if (newKey == null) {
+            aesData[cur++] = 0x00; // don't rekey
+            //_log.debug("flag written");
+        } else {
+            aesData[cur++] = 0x01; // rekey
+            System.arraycopy(newKey.getData(), 0, aesData, cur, SessionKey.KEYSIZE_BYTES);
+            cur += SessionKey.KEYSIZE_BYTES;
         }
+        System.arraycopy(data, 0, aesData, cur, data.length);
+        cur += data.length;
+
+        //_log.debug("raw data written: " + len);
+        byte padding[] = getPadding(_context, size, paddedSize);
+        //_log.debug("padding length: " + padding.length);
+        System.arraycopy(padding, 0, aesData, cur, padding.length);
+        cur += padding.length;
+
+        //Hash h = _context.sha().calculateHash(aesUnencr);
+        //_log.debug("Hash of entire aes block before encryption: (len=" + aesUnencr.length + ")\n" + DataHelper.toString(h.getData(), 32));
+        _context.aes().encrypt(aesData, prefixBytes, aesData, prefixBytes, key, iv, aesData.length - prefixBytes);
+        //_log.debug("Encrypted length: " + aesEncr.length);
+        //return aesEncr;
+        return aesData;
     }
 
     /**
@@ -523,6 +520,12 @@ public class ElGamalAESEngine {
      *
      */
     final static byte[] getPadding(I2PAppContext context, int curSize, long minPaddedSize) {
+        int size = getPaddingSize(curSize, minPaddedSize);
+        byte rv[] = new byte[size];
+        context.random().nextBytes(rv);
+        return rv;
+    }
+    final static int getPaddingSize(int curSize, long minPaddedSize) {
         int diff = 0;
         if (curSize < minPaddedSize) {
             diff = (int) minPaddedSize - curSize;
@@ -530,9 +533,7 @@ public class ElGamalAESEngine {
 
         int numPadding = diff;
         if (((curSize + diff) % 16) != 0) numPadding += (16 - ((curSize + diff) % 16));
-        byte rv[] = new byte[numPadding];
-        context.random().nextBytes(rv);
-        return rv;
+        return numPadding;
     }
 
 }
\ 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
index b6f0cf97150137e1c4d621f079c5d9aa083563ba..b9c35138833bf8d2c5713964fb5f845e27922d44 100644
--- a/core/java/src/net/i2p/crypto/ElGamalEngine.java
+++ b/core/java/src/net/i2p/crypto/ElGamalEngine.java
@@ -30,7 +30,6 @@ package net.i2p.crypto;
  */
 
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.math.BigInteger;
 
 import net.i2p.I2PAppContext;
@@ -98,19 +97,12 @@ public class ElGamalEngine {
 
         long start = _context.clock().now();
 
-        ByteArrayOutputStream baos = new ByteArrayOutputStream(256);
-        try {
-            baos.write(0xFF);
-            Hash hash = _context.sha().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();
+        byte d2[] = new byte[1+Hash.HASH_LENGTH+data.length];
+        d2[0] = (byte)0xFF;
+        Hash hash = _context.sha().calculateHash(data);
+        System.arraycopy(hash.getData(), 0, d2, 1, Hash.HASH_LENGTH);
+        System.arraycopy(data, 0, d2, 1+Hash.HASH_LENGTH, data.length);
+        
         long t0 = _context.clock().now();
         BigInteger m = new NativeBigInteger(1, d2);
         long t1 = _context.clock().now();
diff --git a/core/java/src/net/i2p/data/Payload.java b/core/java/src/net/i2p/data/Payload.java
index 4c4bbf58572f7cafb8b4bdce35594260ff3bf02c..ac3db075214160746e352f4a2d61f02e2c8dd5f9 100644
--- a/core/java/src/net/i2p/data/Payload.java
+++ b/core/java/src/net/i2p/data/Payload.java
@@ -81,6 +81,13 @@ public class Payload extends DataStructureImpl {
         out.write(_encryptedData);
         _log.debug("wrote payload: " + _encryptedData.length);
     }
+    public int writeBytes(byte target[], int offset) {
+        if (_encryptedData == null) throw new IllegalStateException("Not yet encrypted.  Please set the encrypted data");
+        DataHelper.toLong(target, offset, 4, _encryptedData.length);
+        offset += 4;
+        System.arraycopy(_encryptedData, 0, target, offset, _encryptedData.length);
+        return 4 + _encryptedData.length;
+    }
 
     public boolean equals(Object object) {
         if ((object == null) || !(object instanceof Payload)) return false;
@@ -94,6 +101,7 @@ public class Payload extends DataStructureImpl {
     }
 
     public String toString() {
+        if (true) return "[Payload]";
         StringBuffer buf = new StringBuffer(128);
         buf.append("[Payload: ");
         if (getUnencryptedData() != null)
diff --git a/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java
index d25fa30960580a9995c66f80a8611c93e52bb144..357bff1d9dab704717e81c3096ec7a917106278f 100644
--- a/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java
@@ -12,11 +12,13 @@ package net.i2p.data.i2cp;
 import java.io.ByteArrayOutputStream;
 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.Destination;
 import net.i2p.data.Payload;
+import net.i2p.data.i2cp.I2CPMessageException;
 import net.i2p.util.Log;
 
 /**
@@ -87,20 +89,31 @@ public class SendMessageMessage extends I2CPMessageImpl {
     }
 
     protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
+        throw new RuntimeException("wtf, dont run me");
+    }
+
+    /**
+     * Write out the full message to the stream, including the 4 byte size and 1 
+     * byte type header.  Override the parent so we can be more mem efficient
+     *
+     */
+    public void writeMessage(OutputStream out) 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);
+        int len = 2 + _destination.size() + _payload.getSize() + 4 + 4;
+        
         try {
-            _sessionId.writeBytes(os);
-            _destination.writeBytes(os);
-            _payload.writeBytes(os);
-            DataHelper.writeLong(os, 4, _nonce);
+            DataHelper.writeLong(out, 4, len);
+            DataHelper.writeLong(out, 1, getType());
+            _sessionId.writeBytes(out);
+            _destination.writeBytes(out);
+            _payload.writeBytes(out);
+            DataHelper.writeLong(out, 4, _nonce);
         } catch (DataFormatException dfe) {
-            throw new I2CPMessageException("Error writing out the message data", dfe);
+            throw new I2CPMessageException("Error writing the msg", dfe);
         }
-        return os.toByteArray();
     }
-
+    
     public int getType() {
         return MESSAGE_TYPE;
     }
diff --git a/core/java/test/net/i2p/crypto/ElGamalAESEngineTest.java b/core/java/test/net/i2p/crypto/ElGamalAESEngineTest.java
index 7bf7c90b84358f906407cc8da881d87413e70c88..0ee419165a8b7aa51b4cd012138317b01ef80463 100644
--- a/core/java/test/net/i2p/crypto/ElGamalAESEngineTest.java
+++ b/core/java/test/net/i2p/crypto/ElGamalAESEngineTest.java
@@ -123,7 +123,7 @@ class ElGamalAESEngineTest {
             _log.debug("** Encryption complete.  Beginning decryption");
             Set foundTags = new HashSet();
             SessionKey foundKey = new SessionKey();
-            byte decrypted[] = _context.elGamalAESEngine().decryptAESBlock(encrypted, sessionKey, iv, null, foundTags, foundKey);
+            byte decrypted[] = _context.elGamalAESEngine().decryptAESBlock(encrypted, 0, encrypted.length, sessionKey, iv, null, foundTags, foundKey);
             if (decrypted == null) throw new Exception("Decryption failed");
             String read = new String(decrypted);
             _log.debug("read: " + read);
diff --git a/history.txt b/history.txt
index c8bdd19a1b88bde22f55337ab2d4a3313088fba7..ba6aa69c72bd46ad29debd4556fecee6ed09c2a4 100644
--- a/history.txt
+++ b/history.txt
@@ -1,4 +1,18 @@
-$Id: history.txt,v 1.57 2004/10/27 21:11:52 jrandom Exp $
+$Id: history.txt,v 1.58 2004/10/29 21:40:52 jrandom Exp $
+
+2004-10-30  jrandom
+    * Cache the temporary objects used in the AES encryption/decryption
+      process so that AES doesn't require any memory allocation to process
+      data.
+    * Dramatically reduce memory usage within various crypto implementations
+      by avoiding unnecessary (though simplifying) buffers.
+    * If we specify some tags to be sent in an I2CP message explicitly, use
+      only those, not those plus a new set (otherwise we aren't sure on ACK
+      which set was delivered)
+    * Allow configuration for the partial send timeout (how long before 
+      resending a message down a different tunnel in a lease).  This can be
+      updated with the "router.clientPartialSendTimeout" router config prop.
+    * Logging
 
 2004-10-29  jrandom
     * Strip the Referer, Via, and From headers completely, rather than 
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index da11ca1fda201dfa64c313ab95bb1b21dae18ef1..2b5d0e2ed4ab4bdd69abd7f415780bccc5250f18 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -15,9 +15,9 @@ import net.i2p.CoreVersion;
  *
  */
 public class RouterVersion {
-    public final static String ID = "$Revision: 1.63 $ $Date: 2004/10/27 21:11:52 $";
+    public final static String ID = "$Revision: 1.64 $ $Date: 2004/10/29 21:40:52 $";
     public final static String VERSION = "0.4.1.3";
-    public final static long BUILD = 4;
+    public final static long BUILD = 5;
     public static void main(String args[]) {
         System.out.println("I2P Router version: " + VERSION);
         System.out.println("Router ID: " + RouterVersion.ID);
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java
index 5253cda0f8fd25341bd99023423e1b061c9757e1..9051f13880c88ef05a5f7a959d990d4a78c99c85 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJob.java
@@ -36,7 +36,7 @@ import net.i2p.util.Log;
 /**
  * Send a client message, taking into consideration the fact that there may be
  * multiple inbound tunnels that the target provides.  This job sends it to one
- * of them and if it doesnt get a confirmation within 15 seconds (SEND_TIMEOUT_MS),
+ * of them and if it doesnt get a confirmation within a few seconds (getSendTimeout()),
  * it tries the next, continuing on until a confirmation is received, the full
  * timeout has been reached (60 seconds, or the ms defined in the client's or
  * router's "clientMessageTimeout" option).
@@ -63,7 +63,9 @@ public class OutboundClientMessageJob extends JobImpl {
     private final static long OVERALL_TIMEOUT_MS_DEFAULT = 60*1000;
     
     /** how long for each send do we allow before going on to the next? */
-    private final static long SEND_TIMEOUT_MS = 10*1000;
+    private final static long DEFAULT_SEND_PARTIAL_TIMEOUT = 10*1000;
+    private static final String PROP_SEND_PARTIAL_TIMEOUT = "router.clientPartialSendTimeout";
+    
     /** priority of messages, that might get honored some day... */
     private final static int SEND_PRIORITY = 500;
     
@@ -132,6 +134,15 @@ public class OutboundClientMessageJob extends JobImpl {
         _shouldBundle = getShouldBundle();
     }
     
+    private long getSendTimeout() { 
+        String timeout = getContext().getProperty(PROP_SEND_PARTIAL_TIMEOUT, ""+DEFAULT_SEND_PARTIAL_TIMEOUT);
+        try {
+            return Long.parseLong(timeout);
+        } catch (NumberFormatException nfe) {
+            return DEFAULT_SEND_PARTIAL_TIMEOUT;
+        }
+    }
+    
     public String getName() { return "Outbound client message"; }
     
     public void runJob() {
@@ -375,7 +386,7 @@ public class OutboundClientMessageJob extends JobImpl {
             SendTunnelMessageJob j = new SendTunnelMessageJob(getContext(), msg, outTunnelId, 
                                                               lease.getRouterIdentity().getHash(), 
                                                               lease.getTunnelId(), null, onReply, 
-                                                              onFail, selector, SEND_TIMEOUT_MS, 
+                                                              onFail, selector, getSendTimeout(), 
                                                               SEND_PRIORITY);
             getContext().jobQueue().addJob(j);
         } else {