diff --git a/LICENSE.txt b/LICENSE.txt
index 7a6775bc7eceb76b175f381ae81c4ba4869b3ce6..0c63d86ed7d79fb793f13b5b770046ca694292a3 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -40,6 +40,10 @@ Public domain except as listed below:
    Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle
    See licenses/LICENSE-SHA256.txt
 
+   ElGamal:
+   Copyright (c) 2000 - 2013 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org)
+   See licenses/LICENSE-SHA256.txt
+
    AES code:
    Copyright (c) 1995-2005 The Cryptix Foundation Limited.
    See licenses/LICENSE-Cryptix.txt
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
index f400352f4d3f90ae154e5a11f96774cfe141c2de..48bbca328c167253b4b329f66f3cbf3d5c797215 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
@@ -471,10 +471,10 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
                     portNum = "7654";
                 String msg;
                 if (getTunnel().getContext().isRouterContext())
+                    msg = "Unable to build tunnels for the client";
+                else
                     msg = "Unable to connect to the router at " + getTunnel().host + ':' + portNum +
                              " and build tunnels for the client";
-                else
-                    msg = "Unable to build tunnels for the client";
                 if (++retries < MAX_RETRIES) {
                     if (log != null)
                         log.log(msg + ", retrying in " + (RETRY_DELAY / 1000) + " seconds");
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/CodedIconRendererServlet.java b/apps/routerconsole/java/src/net/i2p/router/web/CodedIconRendererServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d71421bf11bac8f486a19d8a5de4fce4f80489d
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/CodedIconRendererServlet.java
@@ -0,0 +1,93 @@
+package net.i2p.router.web;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.GenericServlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.util.FileUtil;
+ 
+
+/**
+ * Serve plugin icons, at /Plugins/pluginicon?plugin=foo
+ *
+ * @author cacapo
+ * @since 0.9.25
+ */
+public class CodedIconRendererServlet extends HttpServlet {
+ 
+    private static final long serialVersionUID = 16851750L;
+    
+    private static final String base = I2PAppContext.getGlobalContext().getBaseDir().getAbsolutePath();
+    private static final String file = "docs" + File.separatorChar + "themes" + File.separatorChar + "console" +  File.separatorChar + "images" + File.separatorChar + "plugin.png";
+
+
+     @Override
+     protected void service(HttpServletRequest srq, HttpServletResponse srs) throws ServletException, IOException {
+         byte[] data;
+         String name = srq.getParameter("plugin");
+         data  = NavHelper.getBinary(name);
+         
+         //set as many headers as are common to any outcome
+         
+         srs.setContentType("image/png");
+         srs.setDateHeader("Expires", I2PAppContext.getGlobalContext().clock().now() + 86400000l);
+         srs.setHeader("Cache-Control", "public, max-age=86400");
+         OutputStream os = srs.getOutputStream();
+         
+         //Binary data is present
+         if(data != null){
+             srs.setHeader("Content-Length", Integer.toString(data.length));
+             int content = Arrays.hashCode(data);
+             int chksum = srq.getIntHeader("If-None-Match");//returns -1 if no such header
+             //Don't render if icon already present
+             if(content != chksum){
+                 srs.setIntHeader("ETag", content);
+                 try{
+                     os.write(data);
+                     os.flush();
+                     os.close();
+                 }catch(IOException e){
+                     I2PAppContext.getGlobalContext().logManager().getLog(getClass()).warn("Error writing binary image data for plugin", e);
+                 }
+             } else {
+                 srs.sendError(304, "Not Modified");
+             }
+         } else {
+             //Binary data is not present but must be substituted by file on disk
+             File pfile = new File(base, file);
+             srs.setHeader("Content-Length", Long.toString(pfile.length()));
+             try{
+                 long lastmod = pfile.lastModified();
+                 if(lastmod > 0){
+                     long iflast = srq.getDateHeader("If-Modified-Since");
+                     if(iflast >= ((lastmod/1000) * 1000)){
+                         srs.sendError(304, "Not Modified");
+                     } else {
+                         srs.setDateHeader("Last-Modified", lastmod);
+                         FileUtil.readFile(file, base, os); 
+                     }
+                     
+                 }
+             } catch(IOException e) {
+                 if (!srs.isCommitted()) {
+                     srs.sendError(403, e.toString());
+                 } else {
+                     I2PAppContext.getGlobalContext().logManager().getLog(getClass()).warn("Error serving plugin.png", e);
+                     throw e;
+                 }
+             }
+             
+         }
+     }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigFamilyHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigFamilyHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c472752c9c3a424d5b9133173251744272c2548
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigFamilyHandler.java
@@ -0,0 +1,109 @@
+package net.i2p.router.web;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.i2p.crypto.CertUtil;
+import net.i2p.crypto.KeyStoreUtil;
+import net.i2p.router.crypto.FamilyKeyCrypto;
+import net.i2p.util.SecureDirectory;
+
+/**
+ *  @since 0.9.25
+ */
+public class ConfigFamilyHandler extends FormHandler {
+    
+    @Override
+    protected void processForm() {
+
+        if (_action.equals(_t("Create Router Family"))) {
+            String family = getJettyString("family");
+            String old = _context.getProperty(FamilyKeyCrypto.PROP_FAMILY_NAME);
+            if (family == null || family.trim().length() <= 0) {
+                addFormError(_t("You must enter a family name"));
+            } else if (old != null) {
+                addFormError("Family already configured: " + family);
+            } else if (family.contains("/") || family.contains("\\")) {
+                addFormError("Bad characters in Family: " + family);
+            } else if (_context.router().saveConfig(FamilyKeyCrypto.PROP_FAMILY_NAME, family.trim())) {
+                addFormNotice(_t("Configuration saved successfully."));
+                addFormError(_t("Restart required to take effect"));
+            } else {
+                addFormError(_t("Error saving the configuration (applied but not saved) - please see the error logs"));
+            }
+        } else if (_action.equals(_t("Join Router Family"))) {
+            InputStream in = _requestWrapper.getInputStream("file");
+            try {
+                // non-null but zero bytes if no file entered, don't know why
+                if (in == null || in.available() <= 0) {
+                    addFormError(_t("You must enter a file"));
+                    return;
+                }
+                // load data
+                PrivateKey pk = CertUtil.loadPrivateKey(in);
+                List<X509Certificate> certs = CertUtil.loadCerts(in);
+                String family = CertUtil.getSubjectValue(certs.get(0), "CN");
+                if (family == null) {
+                    addFormError("Bad certificate - No Subject CN");
+                }
+                if (family.endsWith(FamilyKeyCrypto.CN_SUFFIX) && family.length() > FamilyKeyCrypto.CN_SUFFIX.length())
+                    family = family.substring(0, family.length() - FamilyKeyCrypto.CN_SUFFIX.length());
+                // store to keystore
+                File ks = new SecureDirectory(_context.getConfigDir(), "keystore");
+                if (!ks.exists());
+                    ks.mkdirs();
+                ks = new File(ks, FamilyKeyCrypto.KEYSTORE_PREFIX + family + FamilyKeyCrypto.KEYSTORE_SUFFIX);
+                String keypw = KeyStoreUtil.randomString();
+                KeyStoreUtil.storePrivateKey(ks, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD, family, keypw, pk, certs);
+                // store certificate
+                File cf = new SecureDirectory(_context.getConfigDir(), "certificates");
+                if (!cf.exists());
+                    cf.mkdirs();
+                cf = new SecureDirectory(cf, "family");
+                if (!ks.exists());
+                    ks.mkdirs();
+                cf = new File(cf, family + FamilyKeyCrypto.CERT_SUFFIX);
+                // ignore failure
+                KeyStoreUtil.exportCert(ks, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD, family, cf);
+                // save config
+                Map<String, String> changes = new HashMap<String, String>();
+                changes.put(FamilyKeyCrypto.PROP_FAMILY_NAME, family);
+                changes.put(FamilyKeyCrypto.PROP_KEY_PASSWORD, keypw);
+                changes.put(FamilyKeyCrypto.PROP_KEYSTORE_PASSWORD, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD);
+                if (_context.router().saveConfig(changes, null)) {
+                    addFormNotice("Family key configured for router family: " + family);
+                    addFormError(_t("Restart required to take effect"));
+                } else {
+                    addFormError(_t("Error saving the configuration (applied but not saved) - please see the error logs"));
+                }
+            } catch (GeneralSecurityException gse) {
+                addFormError(_t("Load from file failed") + " - " + gse);
+            } catch (IOException ioe) {
+                addFormError(_t("Load from file failed") + " - " + ioe);
+            } finally {
+                // it's really a ByteArrayInputStream but we'll play along...
+                try { in.close(); } catch (IOException ioe) {}
+            }
+        } else if (_action.equals(_t("Leave Router Family"))) {
+            List<String> removes = new ArrayList<String>();
+            removes.add(FamilyKeyCrypto.PROP_FAMILY_NAME);
+            removes.add(FamilyKeyCrypto.PROP_KEY_PASSWORD);
+            removes.add(FamilyKeyCrypto.PROP_KEYSTORE_PASSWORD);
+            if (_context.router().saveConfig(null, removes)) {
+                addFormNotice(_t("Configuration saved successfully."));
+                addFormError(_t("Restart required to take effect"));
+            } else {
+                addFormError(_t("Error saving the configuration (applied but not saved) - please see the error logs"));
+            }
+        }
+        //addFormError(_t("Unsupported") + ' ' + _action + '.');
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigFamilyHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigFamilyHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..ede566ec63f8a81a05a9d9132138ca885981a451
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigFamilyHelper.java
@@ -0,0 +1,17 @@
+package net.i2p.router.web;
+
+import net.i2p.router.crypto.FamilyKeyCrypto;
+
+/**
+ *  @since 0.9.25
+ */
+public class ConfigFamilyHelper extends HelperBase {
+
+    public String getFamily() {
+        return _context.getProperty(FamilyKeyCrypto.PROP_FAMILY_NAME, "");
+    }
+
+    public String getKeyPW() {
+        return _context.getProperty(FamilyKeyCrypto.PROP_KEY_PASSWORD, "");
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNavHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNavHelper.java
index f731f0017b6eb7e1bf550f786a9c0d8c0deec7f3..d9f39227542df2f5e25fc60c47b4cc437fe44207 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNavHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNavHelper.java
@@ -19,13 +19,13 @@ public class ConfigNavHelper extends HelperBase {
     private static final String pages[] =
                                           {"", "net", "ui", "sidebar", "home", "service", "update", "tunnels",
                                            "clients", "peer", "keyring", "logging", "stats",
-                                           "reseed", "advanced" };
+                                           "reseed", "advanced", "family" };
 
     private static final String titles[] =
                                           {_x("Bandwidth"), _x("Network"), _x("UI"), _x("Summary Bar"), _x("Home Page"),
                                            _x("Service"), _x("Update"), _x("Tunnels"),
                                            _x("Clients"), _x("Peers"), _x("Keyring"), _x("Logging"), _x("Stats"),
-                                           _x("Reseeding"), _x("Advanced") };
+                                           _x("Reseeding"), _x("Advanced"), _x("Router Family") };
 
     /** @since 0.9.19 */
     private static class Tab {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/HomeHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/HomeHelper.java
index cc6f017a5b7545ceafb16a9704aa1aa63050bbfe..b8c91d19e449fb1dbeb976460e4acbdefe5273ba 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/HomeHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/HomeHelper.java
@@ -67,6 +67,7 @@ public class HomeHelper extends HelperBase {
         //"Salt" + S + "salt.i2p" + S + "http://salt.i2p/" + S + I + "salt_console.png" + S +
         "stats.i2p" + S + _x("I2P Network Statistics") + S + "http://stats.i2p/cgi-bin/dashboard.cgi" + S + I + "chart_line.png" + S +
         _x("Technical Docs") + S + _x("Technical documentation") + S + "http://i2p-projekt.i2p/how" + S + I + "education.png" + S +
+        _x("The Tin Hat") + S + _x("Privacy guides and tutorials") + S + "http://secure.thetinhat.i2p/" + S + I + "thetinhat.png" + S +
         _x("Trac Wiki") + S + S + "http://trac.i2p2.i2p/" + S + I + "billiard_marker.png" + S +
         //_x("Ugha's Wiki") + S + S + "http://ugha.i2p/" + S + I + "billiard_marker.png" + S +
         _x("Sponge's main site") + S + _x("Seedless and the Robert BitTorrent applications") + S + "http://sponge.i2p/" + S + I + "user_astronaut.png" + S +
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
index 713520c94a0f3352372f2f96721137e64ef8fcd7..9e1393c233953387d363ecd4f96d4d55ce0b4fda 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
@@ -13,6 +13,7 @@ public class NavHelper {
     private static final Map<String, String> _apps = new ConcurrentHashMap<String, String>(4);
     private static final Map<String, String> _tooltips = new ConcurrentHashMap<String, String>(4);
     private static final Map<String, String> _icons = new ConcurrentHashMap<String, String>(4);
+    private static final Map<String, byte[]> _binary = new ConcurrentHashMap<String, byte[]>(4);
     
     /**
      * To register a new client application so that it shows up on the router
@@ -40,6 +41,29 @@ public class NavHelper {
         _icons.remove(name);
     }
     
+    /**
+     *  Retrieve binary icon for a plugin
+     *  @param name plugin name
+     *  @return null if not found
+     *  @since 0.9.25
+     */
+    public static byte[] getBinary(String name){
+        if(name != null)
+            return _binary.get(name);
+        else
+            return null;
+    }
+
+    /**
+     *  Store binary icon for a plugin
+     *  @param name plugin name
+     *  @since 0.9.25
+     */
+    public static void setBinary(String name, byte[] arr){
+        _binary.put(name, arr);
+    }
+
+
     /**
      *  Translated string is loaded by PluginStarter
      *  @param ctx unused
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
index 2af0d5df46e56c33bcccf93d1afb0a4b3e6e6e9b..1d6c0fa21711a44a45ea1b2cc8b29437a16e264e 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
@@ -22,6 +22,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.app.ClientApp;
 import net.i2p.app.ClientAppState;
 import net.i2p.data.DataHelper;
+import net.i2p.data.Base64;
 import net.i2p.router.RouterContext;
 import net.i2p.router.RouterVersion;
 import net.i2p.router.startup.ClientAppConfig;
@@ -353,6 +354,18 @@ public class PluginStarter implements Runnable {
             }
         }
 
+        //handle console icons for plugins without web-resources through prop icon-code
+        String fullprop = props.getProperty("icon-code");
+        if(fullprop != null && fullprop.length() > 1){
+            byte[] decoded = Base64.decode(fullprop);
+            if(decoded != null) {
+                NavHelper.setBinary(appName, decoded);
+                iconfile = "/Plugins/pluginicon?plugin=" + appName;
+            } else {
+                iconfile = "/themes/console/images/plugin.png";
+            }
+        }
+
         // load and start things in clients.config
         File clientConfig = new File(pluginDir, "clients.config");
         if (clientConfig.exists()) {
diff --git a/apps/routerconsole/jsp/configfamily.jsp b/apps/routerconsole/jsp/configfamily.jsp
new file mode 100644
index 0000000000000000000000000000000000000000..7a4ddb03d9a0d977bbb56b7c767795306962a9e3
--- /dev/null
+++ b/apps/routerconsole/jsp/configfamily.jsp
@@ -0,0 +1,88 @@
+<%@page contentType="text/html"%>
+<%@page pageEncoding="UTF-8"%>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+
+<html><head>
+<%@include file="css.jsi" %>
+<%=intl.title("config router family")%>
+<script src="/js/ajax.js" type="text/javascript"></script>
+<%@include file="summaryajax.jsi" %>
+</head><body onload="initAjax()">
+
+<%@include file="summary.jsi" %>
+
+<jsp:useBean class="net.i2p.router.web.ConfigFamilyHelper" id="familyHelper" scope="request" />
+<jsp:setProperty name="familyHelper" property="contextId" value="<%=(String)session.getAttribute(\"i2p.contextId\")%>" />
+<h1><%=intl._t("I2P Router Family Configuration")%></h1>
+<div class="main" id="main">
+<%@include file="confignav.jsi" %>
+
+<jsp:useBean class="net.i2p.router.web.ConfigFamilyHandler" id="formhandler" scope="request" />
+<%@include file="formhandler.jsi" %>
+
+<p><%=intl._t("Routers in the same family share a family key.")%>
+<%=intl._t("To start a new family, enter a family name.")%>
+<%=intl._t("To join an existing family, import the private key you exported from a router in the family.")%>
+</p>
+
+<%
+   String family = familyHelper.getFamily();
+   if (family.length() <= 0) {
+       // no family yet
+%>
+<div class="configure"><form action="" method="POST">
+<input type="hidden" name="nonce" value="<%=pageNonce%>" >
+<h3><%=intl._t("Create Router Family")%></h3>
+<p><%=intl._t("Family Name")%> :
+<input name="family" type="text" size="30" value="" />
+</p>
+<div class="formaction">
+<input type="submit" name="action" class="accept" value="<%=intl._t("Create Router Family")%>" />
+</div></form></div>
+
+<div class="configure">
+<form action="" method="POST" enctype="multipart/form-data" accept-charset="UTF-8">
+<input type="hidden" name="nonce" value="<%=pageNonce%>" >
+<h3><%=intl._t("Join Router Family")%></h3>
+<p><%=intl._t("Import the secret family key that you exported from an existing router in the family.")%>
+<p><%=intl._t("Select secret key file")%> :
+<input name="file" type="file" value="" />
+</p>
+<div class="formaction">
+<input type="submit" name="action" class="download" value="<%=intl._t("Join Router Family")%>" />
+</div></form></div>
+<%
+   } else {
+       // family is configured
+       String keypw = familyHelper.getKeyPW();
+       if (keypw.length() > 0) {
+           // family is active
+%>
+<div class="configure">
+<form action="/exportfamily" method="GET">
+<h3><%=intl._t("Export Family Key")%></h3>
+<p><%=intl._t("Export the secret family key to be imported into other routers you control.")%>
+</p>
+<div class="formaction">
+<input type="submit" name="action" class="go" value="<%=intl._t("Export Family Key")%>" />
+</div></form></div>
+<%
+       } else {
+           // family is not active
+%>
+<p><b><%=intl._t("Restart required to activate family {0}.", '"' + family + '"')%>
+<%=intl._t("After restarting, you may export the family key.")%></b></p>
+<%
+       }
+%>
+<div class="configure"><form action="" method="POST">
+<input type="hidden" name="nonce" value="<%=pageNonce%>" >
+<h3><%=intl._t("Leave Router Family")%></h3>
+<p><%=intl._t("No longer be a member of the family {0}.", '"' + family + '"')%>
+<div class="formaction">
+<input type="submit" name="action" class="delete" value="<%=intl._t("Leave Router Family")%>" />
+</div></form></div>
+<%
+   }
+%>
+</div></body></html>
diff --git a/apps/routerconsole/jsp/configreseed.jsp b/apps/routerconsole/jsp/configreseed.jsp
index ebc94b9c9fd13a6c21ae27179d156852da16ae96..25b6704f9fac9a57f5f51f89a8521222d481e0d4 100644
--- a/apps/routerconsole/jsp/configreseed.jsp
+++ b/apps/routerconsole/jsp/configreseed.jsp
@@ -78,7 +78,7 @@
 <b><%=intl._t("Use non-SSL only")%></b></td></tr>
 <tr><td class="mediumtags" align="right"><b><%=intl._t("Reseed URLs")%>:</b></td>
 <td><textarea wrap="off" name="reseedURL" cols="60" rows="7" spellcheck="false"><jsp:getProperty name="reseedHelper" property="reseedURL" /></textarea>
-<div class="formaction"><input type="submit" name="action" value="<%=intl._t("Reset URL list")%>" /></div>
+<div class="formaction"><input type="submit" name="action" class="reload" value="<%=intl._t("Reset URL list")%>" /></div>
 </td></tr>
 
 <tr><td class="mediumtags" align="right"><b><%=intl._t("Enable HTTP Proxy?")%></b></td>
diff --git a/apps/routerconsole/jsp/exportfamily.jsp b/apps/routerconsole/jsp/exportfamily.jsp
new file mode 100644
index 0000000000000000000000000000000000000000..00ce9dc556689f623887cff9f2cbfb6dd74d1928
--- /dev/null
+++ b/apps/routerconsole/jsp/exportfamily.jsp
@@ -0,0 +1,35 @@
+<%
+try {
+    net.i2p.I2PAppContext ctx = net.i2p.I2PAppContext.getGlobalContext();
+    String family = ctx.getProperty("netdb.family.name");
+    String keypw = ctx.getProperty("netdb.family.keyPassword");
+    String kspw = ctx.getProperty("netdb.family.keystorePassword", "changeit");
+    if (family == null || keypw == null) {
+        response.sendError(404);
+        return;
+    }
+    try {
+        response.setDateHeader("Expires", 0);
+        response.addHeader("Cache-Control", "no-store, max-age=0, no-cache, must-revalidate");
+        response.addHeader("Pragma", "no-cache");
+        String name = "family-" + family + "-secret.crt";
+        response.setContentType("application/x-x509-ca-cert; name=\"" + name + '"');
+        response.addHeader("Content-Disposition", "attachment; filename=\"" + name + '"');
+        java.io.File ks = new java.io.File(ctx.getConfigDir(), "keystore");
+        ks = new java.io.File(ks, "family-" + family + ".ks");
+        java.io.OutputStream cout = response.getOutputStream();
+        net.i2p.crypto.KeyStoreUtil.exportPrivateKey(ks, kspw, family, keypw, cout);
+    } catch (java.security.GeneralSecurityException gse) {
+        throw new java.io.IOException("key error", gse);
+    }
+} catch (java.io.IOException ioe) {
+    // prevent 'Committed' IllegalStateException from Jetty
+    if (!response.isCommitted()) {
+        response.sendError(403, ioe.toString());
+    }  else {
+        // Jetty doesn't log this
+        throw ioe;
+    }
+}
+// don't worry about a newline after this
+%>
diff --git a/apps/routerconsole/jsp/web.xml b/apps/routerconsole/jsp/web.xml
index ea183c83564b125d4de28deb2d93f9492cb848e3..44e40c86e516820b97d21b4044c0973d63bd0128 100644
--- a/apps/routerconsole/jsp/web.xml
+++ b/apps/routerconsole/jsp/web.xml
@@ -14,6 +14,18 @@
     </filter-mapping>
 
     <!-- precompiled servlets -->
+
+    <servlet>
+      <servlet-name>net.i2p.router.web.CodedIconRendererServlet</servlet-name>
+      <servlet-class>net.i2p.router.web.CodedIconRendererServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+      <servlet-name>net.i2p.router.web.CodedIconRendererServlet</servlet-name>
+      <url-pattern>/Plugins/*</url-pattern>
+    </servlet-mapping>
+
+
     
     <!-- yeah, i'm lazy, using a jsp instead of a servlet.. -->
     <servlet-mapping> 
diff --git a/build.xml b/build.xml
index 65bda9ed8269321bf02bad071fe2538f7d9c8589..cb46589a2763f29f1fa7cba016d7ba178f0f7429 100644
--- a/build.xml
+++ b/build.xml
@@ -883,6 +883,11 @@
                     <not><contains string="${javac.compilerargs}" substring="-bootclasspath"/></not>
                 </condition>
             </fail>
+            <fail message="javac.compilerargs7 must contain a -bootclasspath option in override.properties">
+                <condition>
+                    <not><contains string="${javac.compilerargs7}" substring="-bootclasspath"/></not>
+                </condition>
+            </fail>
             <fail message="build.built-by must be set in override.properties">
                 <condition>
                     <equals arg1="${build.built-by}" arg2="unknown"/>
diff --git a/core/java/src/gnu/crypto/hash/BaseHashStandalone.java b/core/java/src/gnu/crypto/hash/BaseHashStandalone.java
index 26d51158f17a35974365496ed84e49d78fcede8f..4c77ff9d3f9ca95582a0fc2df1d9ce39e44bc6d7 100644
--- a/core/java/src/gnu/crypto/hash/BaseHashStandalone.java
+++ b/core/java/src/gnu/crypto/hash/BaseHashStandalone.java
@@ -51,7 +51,9 @@ package gnu.crypto.hash;
  * See SHA256Generator for more information.
  *
  * @version $Revision: 1.1 $
+ * @deprecated to be removed in 0.9.27
  */
+@Deprecated
 public abstract class BaseHashStandalone implements IMessageDigestStandalone {
 
    // Constants and variables
diff --git a/core/java/src/gnu/crypto/hash/IMessageDigestStandalone.java b/core/java/src/gnu/crypto/hash/IMessageDigestStandalone.java
index dbba10bb87193f80b2718720b883d0219437104f..b19b3ac8b24cd5983548dcd30715b60453873685 100644
--- a/core/java/src/gnu/crypto/hash/IMessageDigestStandalone.java
+++ b/core/java/src/gnu/crypto/hash/IMessageDigestStandalone.java
@@ -54,7 +54,9 @@ package gnu.crypto.hash;
  * See SHA256Generator for more information.
  *
  * @version $Revision: 1.1 $
+ * @deprecated to be removed in 0.9.27
  */
+@Deprecated
 public interface IMessageDigestStandalone extends Cloneable {
 
    // Constants
diff --git a/core/java/src/gnu/crypto/hash/Sha256Standalone.java b/core/java/src/gnu/crypto/hash/Sha256Standalone.java
index 465675cd3349cb02d6719bc740b1880fd4467662..a966b20cf6022fc0d45131bf05a28b4128903def 100644
--- a/core/java/src/gnu/crypto/hash/Sha256Standalone.java
+++ b/core/java/src/gnu/crypto/hash/Sha256Standalone.java
@@ -64,7 +64,9 @@ package gnu.crypto.hash;
  * See SHA256Generator for more information.
  *
  * @version $Revision: 1.2 $
+ * @deprecated to be removed in 0.9.27
  */
+@Deprecated
 public class Sha256Standalone extends BaseHashStandalone {
    // Constants and variables
    // -------------------------------------------------------------------------
diff --git a/core/java/src/net/i2p/crypto/CertUtil.java b/core/java/src/net/i2p/crypto/CertUtil.java
index 0b5dfe66926c48c99564d59c999b0be67a4291a3..ecb6a5f5597ea7dcce8099985f2bab4f5b895b64 100644
--- a/core/java/src/net/i2p/crypto/CertUtil.java
+++ b/core/java/src/net/i2p/crypto/CertUtil.java
@@ -9,12 +9,21 @@ import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
+import java.security.KeyFactory;
 import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateFactory;
 import java.security.cert.CertificateEncodingException;
+import java.security.cert.CRLException;
 import java.security.cert.X509Certificate;
+import java.security.cert.X509CRL;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.KeySpec;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
 import java.util.Locale;
 
 import javax.naming.InvalidNameException;
@@ -24,6 +33,7 @@ import javax.security.auth.x500.X500Principal;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
 import net.i2p.util.Log;
 import net.i2p.util.SecureFileOutputStream;
 import net.i2p.util.SystemVersion;
@@ -33,7 +43,7 @@ import net.i2p.util.SystemVersion;
  *
  *  @since 0.9.9
  */
-public class CertUtil {
+public final class CertUtil {
         
     private static final int LINE_LENGTH = 64;
 
@@ -93,16 +103,7 @@ public class CertUtil {
                                                 throws IOException, CertificateEncodingException {
         // Get the encoded form which is suitable for exporting
         byte[] buf = cert.getEncoded();
-        PrintWriter wr = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
-        wr.println("-----BEGIN CERTIFICATE-----");
-        String b64 = Base64.encode(buf, true);     // true = use standard alphabet
-        for (int i = 0; i < b64.length(); i += LINE_LENGTH) {
-            wr.println(b64.substring(i, Math.min(i + LINE_LENGTH, b64.length())));
-        }
-        wr.println("-----END CERTIFICATE-----");
-        wr.flush();
-        if (wr.checkError())
-            throw new IOException("Failed write to " + out);
+        writePEM(buf, "CERTIFICATE", out);
     }
 
     /**
@@ -121,13 +122,27 @@ public class CertUtil {
         byte[] buf = pk.getEncoded();
         if (buf == null)
             throw new InvalidKeyException("encoding unsupported for this key");
+        writePEM(buf, "PRIVATE KEY", out);
+    }
+
+    /**
+     *  Modified from:
+     *  http://www.exampledepot.com/egs/java.security.cert/ExportCert.html
+     *
+     *  Writes data in base64 format.
+     *  Does NOT close the stream. Throws on all errors.
+     *
+     *  @since 0.9.25 consolidated from other methods
+     */
+    private static void writePEM(byte[] buf, String what, OutputStream out)
+                                                throws IOException {
         PrintWriter wr = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
-        wr.println("-----BEGIN PRIVATE KEY-----");
+        wr.println("-----BEGIN " + what + "-----");
         String b64 = Base64.encode(buf, true);     // true = use standard alphabet
         for (int i = 0; i < b64.length(); i += LINE_LENGTH) {
             wr.println(b64.substring(i, Math.min(i + LINE_LENGTH, b64.length())));
         }
-        wr.println("-----END PRIVATE KEY-----");
+        wr.println("-----END " + what + "-----");
         wr.flush();
         if (wr.checkError())
             throw new IOException("Failed write to " + out);
@@ -235,4 +250,145 @@ public class CertUtil {
             try { if (fis != null) fis.close(); } catch (IOException foo) {}
         }
     }
+
+    /**
+     *  Get a single Private Key from an input stream.
+     *  Does NOT close the stream.
+     *
+     *  @return non-null, non-empty, throws on all errors including certificate invalid
+     *  @since 0.9.25
+     */
+    public static PrivateKey loadPrivateKey(InputStream in) throws IOException, GeneralSecurityException {
+        try {
+            String line;
+            while ((line = DataHelper.readLine(in)) != null) {
+                if (line.startsWith("---") && line.contains("BEGIN") && line.contains("PRIVATE"))
+                    break;
+            }
+            if (line == null)
+                throw new IOException("no private key found");
+            StringBuilder buf = new StringBuilder(128);
+            while ((line = DataHelper.readLine(in)) != null) {
+                if (line.startsWith("---"))
+                    break;
+                buf.append(line.trim());
+            }
+            if (buf.length() <= 0)
+                throw new IOException("no private key found");
+            byte[] data = Base64.decode(buf.toString(), true);
+            if (data == null)
+                throw new CertificateEncodingException("bad base64 cert");
+            PrivateKey rv = null;
+            // try all the types
+            for (SigAlgo algo : EnumSet.allOf(SigAlgo.class)) {
+                try {
+                    KeySpec ks = new PKCS8EncodedKeySpec(data);
+                    String alg = algo.getName();
+                    KeyFactory kf = KeyFactory.getInstance(alg);
+                    rv = kf.generatePrivate(ks);
+                    break;
+                } catch (GeneralSecurityException gse) {
+                    //gse.printStackTrace();
+                }
+            }
+            if (rv == null)
+                throw new InvalidKeyException("unsupported key type");
+            return rv;
+        } catch (IllegalArgumentException iae) {
+            // java 1.8.0_40-b10, openSUSE
+            // Exception in thread "main" java.lang.IllegalArgumentException: Input byte array has wrong 4-byte ending unit
+            // at java.util.Base64$Decoder.decode0(Base64.java:704)
+            throw new GeneralSecurityException("key error", iae);
+        }
+    }
+
+    /**
+     *  Get one or more certificates from an input stream.
+     *  Throws if any certificate is invalid (e.g. expired).
+     *  Does NOT close the stream.
+     *
+     *  @return non-null, non-empty, throws on all errors including certificate invalid
+     *  @since 0.9.25
+     */
+    public static List<X509Certificate> loadCerts(InputStream in) throws IOException, GeneralSecurityException {
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            Collection<? extends Certificate> certs = cf.generateCertificates(in);
+            List<X509Certificate> rv = new ArrayList<X509Certificate>(certs.size());
+            for (Certificate cert : certs) {
+                if (!(cert instanceof X509Certificate))
+                    throw new GeneralSecurityException("not a X.509 cert");
+                X509Certificate xcert = (X509Certificate) cert;
+                xcert.checkValidity();
+                rv.add(xcert);
+            }
+            if (rv.isEmpty())
+                throw new IOException("no certs found");
+            return rv;
+        } catch (IllegalArgumentException iae) {
+            // java 1.8.0_40-b10, openSUSE
+            // Exception in thread "main" java.lang.IllegalArgumentException: Input byte array has wrong 4-byte ending unit
+            // at java.util.Base64$Decoder.decode0(Base64.java:704)
+            throw new GeneralSecurityException("cert error", iae);
+        } finally {
+            try { in.close(); } catch (IOException foo) {}
+        }
+    }
+
+    /**
+     *  Write a CRL to a file in base64 format.
+     *
+     *  @return success
+     *  @since 0.9.25
+     */
+    public static boolean saveCRL(X509CRL crl, File file) {
+        OutputStream os = null;
+        try {
+           os = new SecureFileOutputStream(file);
+           exportCRL(crl, os);
+           return true;
+        } catch (CRLException ce) {
+            error("Error writing X509 CRL " + file.getAbsolutePath(), ce);
+           return false;
+        } catch (IOException ioe) {
+            error("Error writing X509 CRL " + file.getAbsolutePath(), ioe);
+           return false;
+        } finally {
+            try { if (os != null) os.close(); } catch (IOException foo) {}
+        }
+    }
+
+    /**
+     *  Writes a CRL in base64 format.
+     *  Does NOT close the stream. Throws on all errors.
+     *
+     *  @throws CRLException if the crl does not support encoding
+     *  @since 0.9.25
+     */
+    private static void exportCRL(X509CRL crl, OutputStream out)
+                                                throws IOException, CRLException {
+        byte[] buf = crl.getEncoded();
+        writePEM(buf, "X509 CRL", out);
+    }
+
+/****
+    public static final void main(String[] args) {
+        if (args.length < 2) {
+            System.out.println("Usage: [loadcert | loadcrl | loadprivatekey] file");
+            System.exit(1);
+        }
+        try {
+            File f = new File(args[1]);
+            if (args[0].equals("loadcert")) {
+                loadCert(f);
+            } else if (args[0].equals("loadcrl")) {
+            } else {
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.exit(1);
+        }
+    }
+****/
 }
diff --git a/core/java/src/net/i2p/crypto/CryptixAESEngine.java b/core/java/src/net/i2p/crypto/CryptixAESEngine.java
index e5cf7cb92532ad97eef08ca000a969536d580854..61b5386de77d8016ae422e955c73bd1f7b31fff2 100644
--- a/core/java/src/net/i2p/crypto/CryptixAESEngine.java
+++ b/core/java/src/net/i2p/crypto/CryptixAESEngine.java
@@ -36,7 +36,7 @@ import net.i2p.util.SystemVersion;
  *
  * @author jrandom, thecrypto
  */
-public class CryptixAESEngine extends AESEngine {
+public final class CryptixAESEngine extends AESEngine {
     private final static CryptixRijndael_Algorithm _algo = new CryptixRijndael_Algorithm();
     // keys are now cached in the SessionKey objects
     //private CryptixAESKeyCache _cache;
diff --git a/core/java/src/net/i2p/crypto/CryptoConstants.java b/core/java/src/net/i2p/crypto/CryptoConstants.java
index b9e0327dd385009896a94bf651046dde3348e763..00994d210ad1a91f0bace0f6afa9e4862b162dd6 100644
--- a/core/java/src/net/i2p/crypto/CryptoConstants.java
+++ b/core/java/src/net/i2p/crypto/CryptoConstants.java
@@ -34,6 +34,7 @@ import java.math.BigInteger;
 import java.security.spec.AlgorithmParameterSpec;
 import java.security.spec.DSAParameterSpec;
 
+import net.i2p.crypto.elgamal.spec.ElGamalParameterSpec;
 import net.i2p.util.NativeBigInteger;
 
 /**
@@ -43,7 +44,7 @@ import net.i2p.util.NativeBigInteger;
  * See also: ECConstants, RSAConstants
  *
  */
-public class CryptoConstants {
+public final class CryptoConstants {
     public static final BigInteger dsap = new NativeBigInteger(
                                                                "9c05b2aa960d9b97b8931963c9cc9e8c3026e9b8ed92fad0a69cc886d5bf8015fcadae31"
                                                              + "a0ad18fab3f01b00a358de237655c4964afaa2b337e96ad316b9fb1cc564b5aec5b69a9f"
@@ -78,6 +79,11 @@ public class CryptoConstants {
      */
     public static final DSAParameterSpec DSA_SHA1_SPEC = new DSAParameterSpec(dsap, dsaq, dsag);
 
+    /**
+     *  @since 0.9.25
+     */
+    public static final ElGamalParameterSpec I2P_ELGAMAL_2048_SPEC = new ElGamalParameterSpec(elgp, elgg);
+
     /**
      *  This will be org.bouncycastle.jce.spec.ElgamalParameterSpec
      *  if BC is available, otherwise it
@@ -98,11 +104,11 @@ public class CryptoConstants {
             } catch (Exception e) {
                 //System.out.println("BC ElG spec failed");
                 //e.printStackTrace();
-                spec = new ElGamalParameterSpec(elgp, elgg);
+                spec = I2P_ELGAMAL_2048_SPEC;
             }
         } else {
             //System.out.println("BC not available");
-            spec = new ElGamalParameterSpec(elgp, elgg);
+            spec = I2P_ELGAMAL_2048_SPEC;
         }
         ELGAMAL_2048_SPEC = spec;
     }
diff --git a/core/java/src/net/i2p/crypto/DSAEngine.java b/core/java/src/net/i2p/crypto/DSAEngine.java
index 76f2004f8b01c1c9ef0777531f0505102d2ea1eb..2b66887bbf0302eedfef0a9b20bbb83d11b5abd7 100644
--- a/core/java/src/net/i2p/crypto/DSAEngine.java
+++ b/core/java/src/net/i2p/crypto/DSAEngine.java
@@ -72,7 +72,7 @@ import net.i2p.util.NativeBigInteger;
  *
  *  EdDSA support added in 0.9.15
  */
-public class DSAEngine {
+public final class DSAEngine {
     private final Log _log;
     private final I2PAppContext _context;
 
@@ -234,7 +234,7 @@ public class DSAEngine {
             BigInteger s = new NativeBigInteger(1, sbytes);
             BigInteger r = new NativeBigInteger(1, rbytes);
             BigInteger y = new NativeBigInteger(1, verifyingKey.getData());
-            BigInteger w = null;
+            BigInteger w;
             try {
                 w = s.modInverse(CryptoConstants.dsaq);
             } catch (ArithmeticException ae) {
@@ -402,8 +402,7 @@ public class DSAEngine {
         long start = _context.clock().now();
 
         BigInteger k;
-
-        boolean ok = false;
+        boolean ok;
         do {
             k = new BigInteger(160, _context.random());
             ok = k.compareTo(CryptoConstants.dsaq) != 1;
@@ -516,15 +515,20 @@ public class DSAEngine {
         if (type == SigType.DSA_SHA1)
             return altVerifySigSHA1(signature, data, offset, len, verifyingKey);
 
-        java.security.Signature jsig;
-        if (type.getBaseAlgorithm() == SigAlgo.EdDSA)
-            jsig = new EdDSAEngine(type.getDigestInstance());
-        else
-            jsig = java.security.Signature.getInstance(type.getAlgorithmName());
         PublicKey pubKey = SigUtil.toJavaKey(verifyingKey);
-        jsig.initVerify(pubKey);
-        jsig.update(data, offset, len);
-        boolean rv = jsig.verify(SigUtil.toJavaSig(signature));
+        byte[] sigbytes = SigUtil.toJavaSig(signature);
+        boolean rv;
+        if (type.getBaseAlgorithm() == SigAlgo.EdDSA) {
+            // take advantage of one-shot mode
+            EdDSAEngine jsig = new EdDSAEngine(type.getDigestInstance());
+            jsig.initVerify(pubKey);
+            rv = jsig.verifyOneShot(data, offset, len, sigbytes);
+        } else {
+            java.security.Signature jsig = java.security.Signature.getInstance(type.getAlgorithmName());
+            jsig.initVerify(pubKey);
+            jsig.update(data, offset, len);
+            rv = jsig.verify(sigbytes);
+        }
         return rv;
     }
 
@@ -564,15 +568,21 @@ public class DSAEngine {
         if (type.getHashLen() != hashlen)
             throw new IllegalArgumentException("type mismatch hash=" + hash.getClass() + " key=" + type);
 
-        String algo = getRawAlgo(type);
-        java.security.Signature jsig;
-        if (type.getBaseAlgorithm() == SigAlgo.EdDSA)
-            jsig = new EdDSAEngine(); // Ignore algo, EdDSAKey includes a hash specification.
-        else
-            jsig = java.security.Signature.getInstance(algo);
-        jsig.initVerify(pubKey);
-        jsig.update(hash.getData());
-        boolean rv = jsig.verify(SigUtil.toJavaSig(signature));
+        byte[] sigbytes = SigUtil.toJavaSig(signature);
+        boolean rv;
+        if (type.getBaseAlgorithm() == SigAlgo.EdDSA) {
+            // take advantage of one-shot mode
+            // Ignore algo, EdDSAKey includes a hash specification.
+            EdDSAEngine jsig = new EdDSAEngine();
+            jsig.initVerify(pubKey);
+            rv = jsig.verifyOneShot(hash.getData(), sigbytes);
+        } else {
+            String algo = getRawAlgo(type);
+            java.security.Signature jsig = java.security.Signature.getInstance(algo);
+            jsig.initVerify(pubKey);
+            jsig.update(hash.getData());
+            rv = jsig.verify(sigbytes);
+        }
         return rv;
     }
 
@@ -607,15 +617,20 @@ public class DSAEngine {
         if (type == SigType.DSA_SHA1)
             return altSignSHA1(data, offset, len, privateKey);
 
-        java.security.Signature jsig;
-        if (type.getBaseAlgorithm() == SigAlgo.EdDSA)
-            jsig = new EdDSAEngine(type.getDigestInstance());
-        else
-            jsig = java.security.Signature.getInstance(type.getAlgorithmName());
         PrivateKey privKey = SigUtil.toJavaKey(privateKey);
-        jsig.initSign(privKey, _context.random());
-        jsig.update(data, offset, len);
-        return SigUtil.fromJavaSig(jsig.sign(), type);
+        byte[] sigbytes;
+        if (type.getBaseAlgorithm() == SigAlgo.EdDSA) {
+            // take advantage of one-shot mode
+            EdDSAEngine jsig = new EdDSAEngine(type.getDigestInstance());
+            jsig.initSign(privKey);
+            sigbytes = jsig.signOneShot(data, offset, len);
+        } else {
+            java.security.Signature jsig = java.security.Signature.getInstance(type.getAlgorithmName());
+            jsig.initSign(privKey, _context.random());
+            jsig.update(data, offset, len);
+            sigbytes = jsig.sign();
+        }
+        return SigUtil.fromJavaSig(sigbytes, type);
     }
 
     /**
@@ -650,14 +665,20 @@ public class DSAEngine {
         if (type.getHashLen() != hashlen)
             throw new IllegalArgumentException("type mismatch hash=" + hash.getClass() + " key=" + type);
 
-        java.security.Signature jsig;
-        if (type.getBaseAlgorithm() == SigAlgo.EdDSA)
-            jsig = new EdDSAEngine(); // Ignore algo, EdDSAKey includes a hash specification.
-        else
-            jsig = java.security.Signature.getInstance(algo);
-        jsig.initSign(privKey, _context.random());
-        jsig.update(hash.getData());
-        return SigUtil.fromJavaSig(jsig.sign(), type);
+        byte[] sigbytes;
+        if (type.getBaseAlgorithm() == SigAlgo.EdDSA) {
+            // take advantage of one-shot mode
+            // Ignore algo, EdDSAKey includes a hash specification.
+            EdDSAEngine jsig = new EdDSAEngine();
+            jsig.initSign(privKey);
+            sigbytes = jsig.signOneShot(hash.getData());
+        } else {
+            java.security.Signature jsig = java.security.Signature.getInstance(type.getAlgorithmName());
+            jsig.initSign(privKey, _context.random());
+            jsig.update(hash.getData());
+            sigbytes = jsig.sign();
+        }
+        return SigUtil.fromJavaSig(sigbytes, type);
     }
 
     /**
diff --git a/core/java/src/net/i2p/crypto/ECConstants.java b/core/java/src/net/i2p/crypto/ECConstants.java
index 8bca0b0ac04ce46de69afe762eefc0d15d565b8f..2518e34f9ed2c18afe02e3a1ccca20fbf771fba6 100644
--- a/core/java/src/net/i2p/crypto/ECConstants.java
+++ b/core/java/src/net/i2p/crypto/ECConstants.java
@@ -20,7 +20,7 @@ import net.i2p.util.NativeBigInteger;
  *
  * @since 0.9.9
  */
-class ECConstants {
+final class ECConstants {
 
     private static final boolean DEBUG = false;
 
diff --git a/core/java/src/net/i2p/crypto/ECUtil.java b/core/java/src/net/i2p/crypto/ECUtil.java
index 8d22284804bd835ef3793f252c6a6d0d353f8bf9..03fd77e0b8cb3000001b1147d1a1f5d103c9ed49 100644
--- a/core/java/src/net/i2p/crypto/ECUtil.java
+++ b/core/java/src/net/i2p/crypto/ECUtil.java
@@ -19,7 +19,7 @@ import net.i2p.util.NativeBigInteger;
  *
  *  @since 0.9.16
  */
-class ECUtil {
+final class ECUtil {
 
     private static final BigInteger TWO = new BigInteger("2");
     private static final BigInteger THREE = new BigInteger("3");
diff --git a/core/java/src/net/i2p/crypto/ElGamalAESEngine.java b/core/java/src/net/i2p/crypto/ElGamalAESEngine.java
index 0fe652bb219305fc889a8b40358fe29d5d32f270..6f0c87ab351b813faa01dc8d74bfa89720c74bb5 100644
--- a/core/java/src/net/i2p/crypto/ElGamalAESEngine.java
+++ b/core/java/src/net/i2p/crypto/ElGamalAESEngine.java
@@ -32,7 +32,7 @@ import net.i2p.util.SimpleByteCache;
  *
  * No, this does not extend AESEngine or CryptixAESEngine.
  */
-public class ElGamalAESEngine {
+public final class ElGamalAESEngine {
     private final Log _log;
     private final static int MIN_ENCRYPTED_SIZE = 80; // smallest possible resulting size
     private final I2PAppContext _context;
diff --git a/core/java/src/net/i2p/crypto/ElGamalEngine.java b/core/java/src/net/i2p/crypto/ElGamalEngine.java
index a80e0a99eae998bacdf7f6fa2690ecb1bbd1f6cb..9e25e8ad42ce3f43a47f879d592826e239c97e26 100644
--- a/core/java/src/net/i2p/crypto/ElGamalEngine.java
+++ b/core/java/src/net/i2p/crypto/ElGamalEngine.java
@@ -52,7 +52,7 @@ import net.i2p.util.SimpleByteCache;
  * @author thecrypto, jrandom
  */
 
-public class ElGamalEngine {
+public final class ElGamalEngine {
     private final Log _log;
     private final I2PAppContext _context;
     private final YKGenerator _ykgen;
diff --git a/core/java/src/net/i2p/crypto/ElGamalParameterSpec.java b/core/java/src/net/i2p/crypto/ElGamalParameterSpec.java
deleted file mode 100644
index cee640804633809432703ae796ad21b93332e694..0000000000000000000000000000000000000000
--- a/core/java/src/net/i2p/crypto/ElGamalParameterSpec.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package net.i2p.crypto;
-
-/*
- * Copyright (c) 2000 - 2013 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org)
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
- * and associated documentation files (the "Software"), to deal in the Software without restriction,
- * including without limitation the rights to use, copy, modify, merge, publish, distribute,
- * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
- * is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all copies or
- * substantial portions of the Software.
- *
- *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
- * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
- * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
- * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- *
- */
-
-import java.math.BigInteger;
-import java.security.spec.AlgorithmParameterSpec;
-
-/**
- *  Copied from org.bouncycastle.jce.spec
- *  This can't actually be passed to the BC provider, we would have to
- *  use reflection to create a "real" org.bouncycasle.jce.spec.ElGamalParameterSpec.
- *
- *  @since 0.9.18
- */
-public class ElGamalParameterSpec implements AlgorithmParameterSpec {
-    private final BigInteger p;
-    private final BigInteger g;
-
-    /**
-     * Constructs a parameter set for Diffie-Hellman, using a prime modulus
-     * <code>p</code> and a base generator <code>g</code>.
-     * 
-     * @param p the prime modulus
-     * @param g the base generator
-     */
-    public ElGamalParameterSpec(BigInteger p, BigInteger g) {
-        this.p = p;
-        this.g = g;
-    }
-
-    /**
-     * Returns the prime modulus <code>p</code>.
-     *
-     * @return the prime modulus <code>p</code>
-     */
-    public BigInteger getP() {
-        return p;
-    }
-
-    /**
-     * Returns the base generator <code>g</code>.
-     *
-     * @return the base generator <code>g</code>
-     */
-    public BigInteger getG() {
-        return g;
-    }
-}
diff --git a/core/java/src/net/i2p/crypto/EncType.java b/core/java/src/net/i2p/crypto/EncType.java
index fc07d5d5a1a5d06725acea3f719e659ef47d3ec6..ca59e0267b1667eb8f2f49671f857aac8d380dd8 100644
--- a/core/java/src/net/i2p/crypto/EncType.java
+++ b/core/java/src/net/i2p/crypto/EncType.java
@@ -27,7 +27,7 @@ public enum EncType {
      *  This is the default.
      *  Pubkey 256 bytes, privkey 256 bytes.
      */
-    ELGAMAL_2048(0, 256, 256, EncAlgo.ELGAMAL, "ElGamal/None/NoPadding", CryptoConstants.ELGAMAL_2048_SPEC, "0"),
+    ELGAMAL_2048(0, 256, 256, EncAlgo.ELGAMAL, "ElGamal/None/NoPadding", CryptoConstants.I2P_ELGAMAL_2048_SPEC, "0"),
 
     /**  Pubkey 64 bytes; privkey 32 bytes; */
     EC_P256(1, 64, 32, EncAlgo.EC, "EC/None/NoPadding", ECConstants.P256_SPEC, "0.9.20"),
diff --git a/core/java/src/net/i2p/crypto/HMAC256Generator.java b/core/java/src/net/i2p/crypto/HMAC256Generator.java
index 688e3148dc479cb5d0387a2edfef2f786dbe04eb..bb5b62d155fb85d37b8eadcf43d66cf8fccf0474 100644
--- a/core/java/src/net/i2p/crypto/HMAC256Generator.java
+++ b/core/java/src/net/i2p/crypto/HMAC256Generator.java
@@ -21,7 +21,7 @@ import org.bouncycastle.oldcrypto.macs.I2PHMac;
  *
  * Deprecated, used only by Syndie.
  */
-public class HMAC256Generator extends HMACGenerator {
+public final class HMAC256Generator extends HMACGenerator {
 
     /**
      *  @param context unused
diff --git a/core/java/src/net/i2p/crypto/KeyGenerator.java b/core/java/src/net/i2p/crypto/KeyGenerator.java
index efe9bacac46dc6784b9c295b825bdbb9337db9ed..4297fcae20faa36e79a283378b5fff1d72431510 100644
--- a/core/java/src/net/i2p/crypto/KeyGenerator.java
+++ b/core/java/src/net/i2p/crypto/KeyGenerator.java
@@ -34,6 +34,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.crypto.eddsa.EdDSAPrivateKey;
 import net.i2p.crypto.eddsa.EdDSAPublicKey;
 import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
+import net.i2p.crypto.provider.I2PProvider;
 import net.i2p.data.Hash;
 import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
@@ -55,9 +56,13 @@ import net.i2p.util.RandomSource;
 /** Define a way of generating asymmetrical key pairs as well as symmetrical keys
  * @author jrandom
  */
-public class KeyGenerator {
+public final class KeyGenerator {
     private final I2PAppContext _context;
 
+    static {
+        I2PProvider.addProvider();
+    }
+
     public KeyGenerator(I2PAppContext context) {
         _context = context;
     }
@@ -208,10 +213,10 @@ public class KeyGenerator {
         SimpleDataStructure[] keys = new SimpleDataStructure[2];
         BigInteger x = null;
 
-        // make sure the random key is less than the DSA q
+        // make sure the random key is less than the DSA q and greater than zero
         do {
             x = new NativeBigInteger(160, _context.random());
-        } while (x.compareTo(CryptoConstants.dsaq) >= 0);
+        } while (x.compareTo(CryptoConstants.dsaq) >= 0 || x.equals(BigInteger.ZERO));
 
         BigInteger y = CryptoConstants.dsag.modPow(x, CryptoConstants.dsap);
         keys[0] = new SigningPublicKey();
diff --git a/core/java/src/net/i2p/crypto/KeyStoreUtil.java b/core/java/src/net/i2p/crypto/KeyStoreUtil.java
index 316b123091a770e06f8896e60552aa25034bc023..1de4687de21ab7581236a99283848293cb9d3d7d 100644
--- a/core/java/src/net/i2p/crypto/KeyStoreUtil.java
+++ b/core/java/src/net/i2p/crypto/KeyStoreUtil.java
@@ -13,10 +13,13 @@ import java.security.cert.Certificate;
 import java.security.cert.CertificateExpiredException;
 import java.security.cert.CertificateNotYetValidException;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.Enumeration;
+import java.util.List;
 import java.util.Locale;
 
 import net.i2p.I2PAppContext;
+import net.i2p.crypto.provider.I2PProvider;
 import net.i2p.data.Base32;
 import net.i2p.util.Log;
 import net.i2p.util.SecureDirectory;
@@ -29,7 +32,7 @@ import net.i2p.util.SystemVersion;
  *
  *  @since 0.9.9
  */
-public class KeyStoreUtil {
+public final class KeyStoreUtil {
         
     public static boolean _blacklistLogged;
 
@@ -38,6 +41,10 @@ public class KeyStoreUtil {
     private static final int DEFAULT_KEY_SIZE = 2048;
     private static final int DEFAULT_KEY_VALID_DAYS = 3652;  // 10 years
 
+    static {
+        I2PProvider.addProvider();
+    }
+
     /**
      *  No reports of some of these in a Java keystore but just to be safe...
      *  CNNIC ones are in Ubuntu keystore.
@@ -464,20 +471,29 @@ public class KeyStoreUtil {
             }
         }
         String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath();
-        String[] args = new String[] {
-                   keytool,
-                   "-genkey",            // -genkeypair preferred in newer keytools, but this works with more
-                   "-storetype", KeyStore.getDefaultType(),
-                   "-keystore", ks.getAbsolutePath(),
-                   "-storepass", ksPW,
-                   "-alias", alias,
-                   "-dname", "CN=" + cname + ",OU=" + ou + ",O=I2P Anonymous Network,L=XX,ST=XX,C=XX",
-                   "-validity", Integer.toString(validDays),  // 10 years
-                   "-keyalg", keyAlg,
-                   "-sigalg", getSigAlg(keySize, keyAlg),
-                   "-keysize", Integer.toString(keySize),
-                   "-keypass", keyPW
-        };
+        List<String> a = new ArrayList<String>(32);
+        a.add(keytool);
+        a.add("-genkey");    // -genkeypair preferred in newer keytools, but this works with more
+        //a.add("-v");         // verbose, gives you a stack trace on exception
+        a.add("-storetype"); a.add(KeyStore.getDefaultType());
+        a.add("-keystore");  a.add(ks.getAbsolutePath());
+        a.add("-storepass"); a.add(ksPW);
+        a.add("-alias");     a.add(alias);
+        a.add("-dname");     a.add("CN=" + cname + ",OU=" + ou + ",O=I2P Anonymous Network,L=XX,ST=XX,C=XX");
+        a.add("-validity");  a.add(Integer.toString(validDays));  // 10 years
+        a.add("-keyalg");    a.add(keyAlg);
+        a.add("-sigalg");    a.add(getSigAlg(keySize, keyAlg));
+        a.add("-keysize");   a.add(Integer.toString(keySize));
+        a.add("-keypass");   a.add(keyPW);
+        if (keyAlg.equals("Ed") || keyAlg.equals("EdDSA") || keyAlg.equals("ElGamal")) {
+            File f = I2PAppContext.getGlobalContext().getBaseDir();
+            f = new File(f, "lib");
+            f = new File(f, "i2p.jar");
+            // providerpath is not in the man page; see keytool -genkey -help
+            a.add("-providerpath");  a.add(f.getAbsolutePath());
+            a.add("-providerclass"); a.add("net.i2p.crypto.provider.I2PProvider");
+        }
+        String[] args = a.toArray(new String[a.size()]);
         // TODO pipe key password to process; requires ShellCommand enhancements
         boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 240);
         if (success) {
@@ -514,6 +530,8 @@ public class KeyStoreUtil {
     private static String getSigAlg(int size, String keyalg) {
         if (keyalg.equals("EC"))
             keyalg = "ECDSA";
+        else if (keyalg.equals("Ed"))
+            keyalg = "EdDSA";
         String hash;
         if (keyalg.equals("ECDSA")) {
             if (size <= 256)
@@ -522,6 +540,8 @@ public class KeyStoreUtil {
                 hash = "SHA384";
             else
                 hash = "SHA512";
+        } else if (keyalg.equals("EdDSA")) {
+            hash = "SHA512";
         } else {
             if (size <= 1024)
                 hash = "SHA1";
@@ -559,6 +579,103 @@ public class KeyStoreUtil {
         }
     }
 
+    /** 
+     *  Export the private key and certificate chain (if any) out of a keystore.
+     *  Does NOT close the stream. Throws on all errors.
+     *
+     *  @param ks path to the keystore
+     *  @param ksPW the keystore password, may be null
+     *  @param alias the name of the key
+     *  @param keyPW the key password, must be at least 6 characters
+     *  @since 0.9.25
+     */
+    public static void exportPrivateKey(File ks, String ksPW, String alias, String keyPW,
+                                        OutputStream out)
+                              throws GeneralSecurityException, IOException {
+        InputStream fis = null;
+        try {
+            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+            fis = new FileInputStream(ks);
+            char[] pwchars = ksPW != null ? ksPW.toCharArray() : null;
+            keyStore.load(fis, pwchars);
+            char[] keypwchars = keyPW.toCharArray();
+            PrivateKey pk = (PrivateKey) keyStore.getKey(alias, keypwchars);
+            if (pk == null)
+                throw new GeneralSecurityException("private key not found: " + alias);
+            Certificate[] certs = keyStore.getCertificateChain(alias);
+            CertUtil.exportPrivateKey(pk, certs, out);
+        } finally {
+            if (fis != null) try { fis.close(); } catch (IOException ioe) {}
+        }
+    }
+
+    /** 
+     *  Import the private key and certificate chain to a keystore.
+     *  Keystore will be created if it does not exist.
+     *  Private key MUST be first in the stream.
+     *  Closes the stream. Throws on all errors.
+     *
+     *  @param ks path to the keystore
+     *  @param ksPW the keystore password, may be null
+     *  @param alias the name of the key. If null, will be taken from the Subject CN
+     *               of the first certificate in the chain.
+     *  @param keyPW the key password, must be at least 6 characters
+     *  @return the alias as specified or extracted
+     *  @since 0.9.25
+     */
+    public static String importPrivateKey(File ks, String ksPW, String alias, String keyPW,
+                                          InputStream in)
+                              throws GeneralSecurityException, IOException {
+        OutputStream fos = null;
+        try {
+            KeyStore keyStore = createKeyStore(ks, ksPW);
+            PrivateKey pk = CertUtil.loadPrivateKey(in);
+            List<X509Certificate> certs = CertUtil.loadCerts(in);
+            if (alias == null) {
+                alias = CertUtil.getSubjectValue(certs.get(0), "CN");
+                if (alias == null)
+                    throw new GeneralSecurityException("no alias specified and no Subject CN in cert");
+                if (alias.endsWith(".family.i2p.net") && alias.length() > ".family.i2p.net".length())
+                    alias = alias.substring(0, ".family.i2p.net".length());
+            }
+            keyStore.setKeyEntry(alias, pk, keyPW.toCharArray(), certs.toArray(new Certificate[certs.size()]));
+            char[] pwchars = ksPW != null ? ksPW.toCharArray() : null;
+            fos = new SecureFileOutputStream(ks);
+            keyStore.store(fos, pwchars);
+            return alias;
+        } finally {
+            if (fos != null) try { fos.close(); } catch (IOException ioe) {}
+            try { in.close(); } catch (IOException ioe) {}
+        }
+    }
+
+    /** 
+     *  Import the private key and certificate chain to a keystore.
+     *  Keystore will be created if it does not exist.
+     *  Private key MUST be first in the stream.
+     *  Closes the stream. Throws on all errors.
+     *
+     *  @param ks path to the keystore
+     *  @param ksPW the keystore password, may be null
+     *  @param alias the name of the key, non-null.
+     *  @param keyPW the key password, must be at least 6 characters
+     *  @since 0.9.25
+     */
+    public static void storePrivateKey(File ks, String ksPW, String alias, String keyPW,
+                                       PrivateKey pk, List<X509Certificate> certs)
+                              throws GeneralSecurityException, IOException {
+        OutputStream fos = null;
+        try {
+            KeyStore keyStore = createKeyStore(ks, ksPW);
+            keyStore.setKeyEntry(alias, pk, keyPW.toCharArray(), certs.toArray(new Certificate[certs.size()]));
+            char[] pwchars = ksPW != null ? ksPW.toCharArray() : null;
+            fos = new SecureFileOutputStream(ks);
+            keyStore.store(fos, pwchars);
+        } finally {
+            if (fos != null) try { fos.close(); } catch (IOException ioe) {}
+        }
+    }
+
     /** 
      *  Get a cert out of a keystore
      *
@@ -641,11 +758,31 @@ public class KeyStoreUtil {
      *   Usage: KeyStoreUtil (loads from system keystore)
      *          KeyStoreUtil foo.ks (loads from system keystore, and from foo.ks keystore if exists, else creates empty)
      *          KeyStoreUtil certDir (loads from system keystore and all certs in certDir if exists)
+     *          KeyStoreUtil import file.ks file.key alias keypw (imxports private key from file to keystore)
+     *          KeyStoreUtil export file.ks alias keypw (exports private key from keystore)
+     *          KeyStoreUtil keygen file.ks alias keypw (create keypair in keystore)
+     *          KeyStoreUtil keygen2 file.ks alias keypw (create keypair using I2PProvider)
      */
 /****
     public static void main(String[] args) {
-        File ksf = (args.length > 0) ? new File(args[0]) : null;
         try {
+            if (args.length > 0 && "import".equals(args[0])) {
+                testImport(args);
+                return;
+            }
+            if (args.length > 0 && "export".equals(args[0])) {
+                testExport(args);
+                return;
+            }
+            if (args.length > 0 && "keygen".equals(args[0])) {
+                testKeygen(args);
+                return;
+            }
+            if (args.length > 0 && "keygen2".equals(args[0])) {
+                testKeygen2(args);
+                return;
+            }
+            File ksf = (args.length > 0) ? new File(args[0]) : null;
             if (ksf != null && !ksf.exists()) {
                 createKeyStore(ksf, DEFAULT_KEYSTORE_PASSWORD);
                 System.out.println("Created empty keystore " + ksf);
@@ -674,5 +811,63 @@ public class KeyStoreUtil {
             e.printStackTrace();
         }
     }
+
+    private static void testImport(String[] args) throws Exception {
+        File ksf = new File(args[1]);
+        InputStream in = new FileInputStream(args[2]);
+        String alias = args[3];
+        String pw = args[4];
+        importPrivateKey(ksf, DEFAULT_KEYSTORE_PASSWORD, alias, pw, in);
+    }
+
+
+    private static void testExport(String[] args) throws Exception {
+        File ksf = new File(args[1]);
+        String alias = args[2];
+        String pw = args[3];
+        exportPrivateKey(ksf, DEFAULT_KEYSTORE_PASSWORD, alias, pw, System.out);
+    }
+
+    private static void testKeygen(String[] args) throws Exception {
+        File ksf = new File(args[1]);
+        String alias = args[2];
+        String pw = args[3];
+        boolean ok = createKeys(ksf, DEFAULT_KEYSTORE_PASSWORD, alias, "test cname", "test ou",
+                                //DEFAULT_KEY_VALID_DAYS, "EdDSA", 256, pw);
+                                DEFAULT_KEY_VALID_DAYS, "ElGamal", 2048, pw);
+        System.out.println("genkey ok? " + ok);
+    }
+
+    private static void testKeygen2(String[] args) throws Exception {
+        // keygen test using the I2PProvider
+        //SigType type = SigType.EdDSA_SHA512_Ed25519;
+        SigType type = SigType.ElGamal_SHA256_MODP2048;
+        java.security.KeyPairGenerator kpg = java.security.KeyPairGenerator.getInstance(type.getBaseAlgorithm().getName());
+        kpg.initialize(type.getParams());
+        java.security.KeyPair kp = kpg.generateKeyPair();
+        java.security.PublicKey jpub = kp.getPublic();
+        java.security.PrivateKey jpriv = kp.getPrivate();
+
+        System.out.println("Encoded private key:");
+        System.out.println(net.i2p.util.HexDump.dump(jpriv.getEncoded()));
+        System.out.println("Encoded public key:");
+        System.out.println(net.i2p.util.HexDump.dump(jpub.getEncoded()));
+
+        java.security.Signature jsig = java.security.Signature.getInstance(type.getAlgorithmName());
+        jsig.initSign(jpriv);
+        byte[] data = new byte[111];
+        net.i2p.util.RandomSource.getInstance().nextBytes(data);
+        jsig.update(data);
+        byte[] bsig = jsig.sign();
+        System.out.println("Encoded signature:");
+        System.out.println(net.i2p.util.HexDump.dump(bsig));
+        jsig.initVerify(jpub);
+        jsig.update(data);
+        boolean ok = jsig.verify(bsig);
+        System.out.println("verify passed? " + ok);
+
+        net.i2p.data.Signature sig = SigUtil.fromJavaSig(bsig, type);
+        System.out.println("Signature test: " + sig);
+    }
 ****/
 }
diff --git a/core/java/src/net/i2p/crypto/RSAConstants.java b/core/java/src/net/i2p/crypto/RSAConstants.java
index 3f77d09f19a0d6152fb234a96c9a3ad8346bae5d..c6e0234dc1062217a698ce5ef69bf9e0bf57b339 100644
--- a/core/java/src/net/i2p/crypto/RSAConstants.java
+++ b/core/java/src/net/i2p/crypto/RSAConstants.java
@@ -10,7 +10,7 @@ import net.i2p.util.NativeBigInteger;
  *
  * @since 0.9.9
  */
-class RSAConstants {
+final class RSAConstants {
 
     /**
      *  Generate a spec
diff --git a/core/java/src/net/i2p/crypto/SHA256Generator.java b/core/java/src/net/i2p/crypto/SHA256Generator.java
index 66d630087fb3ea6bc4607b9067ee9b808548b3dd..e3facad9e455ce313b4f2df9ffd3a9f18d97f134 100644
--- a/core/java/src/net/i2p/crypto/SHA256Generator.java
+++ b/core/java/src/net/i2p/crypto/SHA256Generator.java
@@ -1,7 +1,5 @@
 package net.i2p.crypto;
 
-import gnu.crypto.hash.Sha256Standalone;
-
 import java.security.DigestException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -14,25 +12,13 @@ import net.i2p.data.Hash;
  * Defines a wrapper for SHA-256 operation.
  * 
  * As of release 0.8.7, uses java.security.MessageDigest by default.
- * If that is unavailable, it uses
+ * As of release 0.9.25, uses only MessageDigest.
  * GNU-Crypto {@link gnu.crypto.hash.Sha256Standalone}
+ * is deprecated.
  */
 public final class SHA256Generator {
     private final LinkedBlockingQueue<MessageDigest> _digests;
 
-    private static final boolean _useGnu;
-
-    static {
-        boolean useGnu = false;
-        try {
-            MessageDigest.getInstance("SHA-256");
-        } catch (NoSuchAlgorithmException e) {
-            useGnu = true;
-            System.out.println("INFO: Using GNU SHA-256");
-        }
-        _useGnu = useGnu;
-    }
-
     /**
      *  @param context unused
      */
@@ -96,45 +82,14 @@ public final class SHA256Generator {
     }
     
     /**
-     *  Return a new MessageDigest from the system libs unless unavailable
-     *  in this JVM, in that case return a wrapped GNU Sha256Standalone
+     *  Return a new MessageDigest from the system libs.
      *  @since 0.8.7, public since 0.8.8 for FortunaStandalone
      */
     public static MessageDigest getDigestInstance() {
-        if (!_useGnu) {
-            try {
-                return MessageDigest.getInstance("SHA-256");
-            } catch (NoSuchAlgorithmException e) {}
-        }
-        return new GnuMessageDigest();
-    }
-
-    /**
-     *  Wrapper to make Sha256Standalone a MessageDigest
-     *  @since 0.8.7
-     */
-    private static class GnuMessageDigest extends MessageDigest {
-        private final Sha256Standalone _gnu;
-
-        protected GnuMessageDigest() {
-            super("SHA-256");
-            _gnu = new Sha256Standalone();
-        }
-
-        protected byte[] engineDigest() {
-            return _gnu.digest();
-        }
-
-        protected void engineReset() {
-            _gnu.reset();
-        }
-
-        protected void engineUpdate(byte input) {
-            _gnu.update(input);
-        }
-
-        protected void engineUpdate(byte[] input, int offset, int len) {
-            _gnu.update(input, offset, len);
+        try {
+            return MessageDigest.getInstance("SHA-256");
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e);
         }
     }
 
diff --git a/core/java/src/net/i2p/crypto/SelfSignedGenerator.java b/core/java/src/net/i2p/crypto/SelfSignedGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd494792c654b2906a1d8d857fb205999a9c0351
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/SelfSignedGenerator.java
@@ -0,0 +1,605 @@
+package net.i2p.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.cert.X509CRL;
+import java.security.spec.X509EncodedKeySpec;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import javax.crypto.interfaces.DHPublicKey;
+import javax.crypto.spec.DHParameterSpec;
+import javax.crypto.spec.DHPublicKeySpec;
+import javax.security.auth.x500.X500Principal;
+
+import static net.i2p.crypto.SigUtil.intToASN1;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Signature;
+import net.i2p.data.SigningPrivateKey;
+import net.i2p.data.SigningPublicKey;
+import net.i2p.data.SimpleDataStructure;
+import net.i2p.util.HexDump;
+import net.i2p.util.RandomSource;
+import net.i2p.util.SecureFileOutputStream;
+import net.i2p.util.SystemVersion;
+
+/**
+ *  Generate keys and a selfsigned certificate, suitable for
+ *  storing in a Keystore with KeyStoreUtil.storePrivateKey().
+ *  All done programatically, no keytool, no BC libs, no sun classes.
+ *  Ref: RFC 2459
+ *
+ *  This is coded to create a cert that matches what comes out of keytool
+ *  exactly, even if I don't understand all of it.
+ *
+ *  @since 0.9.25
+ */
+public final class SelfSignedGenerator {
+
+    private static final boolean DEBUG = false;
+
+    private static final String OID_CN = "2.5.4.3";
+    private static final String OID_C = "2.5.4.6";
+    private static final String OID_L = "2.5.4.7";
+    private static final String OID_ST = "2.5.4.8";
+    private static final String OID_O = "2.5.4.10";
+    private static final String OID_OU = "2.5.4.11";
+    // Subject Key Identifier
+    private static final String OID_SKI = "2.5.29.14";
+    // CRL number
+    private static final String OID_CRLNUM = "2.5.29.20";
+
+    private static final Map<String, String> OIDS;
+    static {
+        OIDS = new HashMap<String, String>(16);
+        OIDS.put(OID_CN, "CN");
+        OIDS.put(OID_C, "C");
+        OIDS.put(OID_L, "L");
+        OIDS.put(OID_ST, "ST");
+        OIDS.put(OID_O, "O");
+        OIDS.put(OID_OU, "OU");
+        OIDS.put(OID_SKI, "SKI");
+    }
+
+    /**
+     *  rv[0] is a Java PublicKey
+     *  rv[1] is a Java PrivateKey
+     *  rv[2] is a Java X509Certificate
+     *  rv[3] is a Java X509CRL
+     */
+    public static Object[] generate(String cname, String ou, String o, String l, String st, String c,
+                             int validDays, SigType type) throws GeneralSecurityException {
+        SimpleDataStructure[] keys = KeyGenerator.getInstance().generateSigningKeys(type);
+        SigningPublicKey pub = (SigningPublicKey) keys[0];
+        SigningPrivateKey priv = (SigningPrivateKey) keys[1];
+        PublicKey jpub = SigUtil.toJavaKey(pub);
+        PrivateKey jpriv = SigUtil.toJavaKey(priv);
+
+        String oid;
+        switch (type) {
+            case DSA_SHA1:
+            case ECDSA_SHA256_P256:
+            case ECDSA_SHA384_P384:
+            case ECDSA_SHA512_P521:
+            case RSA_SHA256_2048:
+            case RSA_SHA384_3072:
+            case RSA_SHA512_4096:
+            case EdDSA_SHA512_Ed25519:
+            case EdDSA_SHA512_Ed25519ph:
+                oid = type.getOID();
+                break;
+            default:
+                throw new GeneralSecurityException("Unsupported: " + type);
+        }
+        byte[] sigoid = getEncodedOIDSeq(oid);
+
+        byte[] tbs = genTBS(cname, ou, o, l, st, c, validDays, sigoid, jpub);
+        int tbslen = tbs.length;
+
+        Signature sig = DSAEngine.getInstance().sign(tbs, priv);
+        if (sig == null)
+            throw new GeneralSecurityException("sig failed");
+        byte[] sigbytes= SigUtil.toJavaSig(sig);
+
+        int seqlen = tbslen + sigoid.length + spaceFor(sigbytes.length + 1);
+        int totlen = spaceFor(seqlen);
+        byte[] cb = new byte[totlen];
+        int idx = 0;
+
+        // construct the whole encoded cert
+        cb[idx++] = 0x30;
+        idx = intToASN1(cb, idx, seqlen);
+
+        // TBS cert
+        System.arraycopy(tbs, 0, cb, idx, tbs.length);
+        idx += tbs.length;
+
+        // sig algo
+        System.arraycopy(sigoid, 0, cb, idx, sigoid.length);
+        idx += sigoid.length;
+
+        // sig (bit string)
+        cb[idx++] = 0x03;
+        idx = intToASN1(cb, idx, sigbytes.length + 1);
+        cb[idx++] = 0;
+        System.arraycopy(sigbytes, 0, cb, idx, sigbytes.length);
+
+        if (DEBUG) {
+            System.out.println("Sig OID");
+            System.out.println(HexDump.dump(sigoid));
+            System.out.println("Signature");
+            System.out.println(HexDump.dump(sigbytes));
+            System.out.println("Whole cert");
+            System.out.println(HexDump.dump(cb));
+        }
+        ByteArrayInputStream bais = new ByteArrayInputStream(cb);
+
+        X509Certificate cert;
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            cert = (X509Certificate)cf.generateCertificate(bais);
+            cert.checkValidity();
+        } catch (IllegalArgumentException iae) {
+            throw new GeneralSecurityException("cert error", iae);
+        }
+        X509CRL crl = generateCRL(cert, validDays, 1, sigoid, jpriv);
+
+        // some simple tests
+        PublicKey cpub = cert.getPublicKey();
+        cert.verify(cpub);
+        if (!cpub.equals(jpub))
+            throw new GeneralSecurityException("pubkey mismatch");
+        // todo crl tests
+
+        Object[] rv = { jpub, jpriv, cert, crl };
+        return rv;
+    }
+
+    /**
+     *  Generate a CRL for the given cert, signed with the given private key
+     */
+    private static X509CRL generateCRL(X509Certificate cert, int validDays, int crlNum,
+                                       byte[] sigoid, PrivateKey jpriv) throws GeneralSecurityException {
+
+        SigningPrivateKey priv = SigUtil.fromJavaKey(jpriv);
+
+        byte[] tbs = genTBSCRL(cert, validDays, crlNum, sigoid);
+        int tbslen = tbs.length;
+
+        Signature sig = DSAEngine.getInstance().sign(tbs, priv);
+        if (sig == null)
+            throw new GeneralSecurityException("sig failed");
+        byte[] sigbytes= SigUtil.toJavaSig(sig);
+
+        int seqlen = tbslen + sigoid.length + spaceFor(sigbytes.length + 1);
+        int totlen = spaceFor(seqlen);
+        byte[] cb = new byte[totlen];
+        int idx = 0;
+
+        // construct the whole encoded cert
+        cb[idx++] = 0x30;
+        idx = intToASN1(cb, idx, seqlen);
+
+        // TBS cert
+        System.arraycopy(tbs, 0, cb, idx, tbs.length);
+        idx += tbs.length;
+
+        // sig algo
+        System.arraycopy(sigoid, 0, cb, idx, sigoid.length);
+        idx += sigoid.length;
+
+        // sig (bit string)
+        cb[idx++] = 0x03;
+        idx = intToASN1(cb, idx, sigbytes.length + 1);
+        cb[idx++] = 0;
+        System.arraycopy(sigbytes, 0, cb, idx, sigbytes.length);
+
+     /****
+        if (DEBUG) {
+            System.out.println("CRL Sig OID");
+            System.out.println(HexDump.dump(sigoid));
+            System.out.println("CRL Signature");
+            System.out.println(HexDump.dump(sigbytes));
+            System.out.println("Whole CRL");
+            System.out.println(HexDump.dump(cb));
+        }
+      ****/
+
+        ByteArrayInputStream bais = new ByteArrayInputStream(cb);
+
+        X509CRL rv;
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            // wow, unlike for x509Certificates, there's no validation here at all
+            // ASN.1 errors don't cause any exceptions
+            rv = (X509CRL)cf.generateCRL(bais);
+        } catch (IllegalArgumentException iae) {
+            throw new GeneralSecurityException("cert error", iae);
+        }
+
+        return rv;
+    }
+
+    private static byte[] genTBS(String cname, String ou, String o, String l, String st, String c,
+                          int validDays, byte[] sigoid, PublicKey jpub) throws GeneralSecurityException {
+        // a0 ???, int = 2
+        byte[] version = { (byte) 0xa0, 3, 2, 1, 2 };
+
+        // postive serial number (int)
+        byte[] serial = new byte[6];
+        serial[0] = 2;
+        serial[1] = 4;
+        RandomSource.getInstance().nextBytes(serial, 2, 4);
+        serial[2] &= 0x7f;
+
+        // going to use this for both issuer and subject
+        String dname = "CN=" + cname + ",OU=" + ou + ",O=" + o + ",L=" + l + ",ST=" + st + ",C=" + c;
+        byte[] issuer = (new X500Principal(dname, OIDS)).getEncoded();
+        byte[] validity = getValidity(validDays);
+        byte[] subject = issuer;
+
+        byte[] pubbytes = jpub.getEncoded();
+        byte[] extbytes = getExtensions(pubbytes);
+
+        int len = version.length + serial.length + sigoid.length + issuer.length +
+                  validity.length + subject.length + pubbytes.length + extbytes.length;
+
+        int totlen = spaceFor(len);
+        byte[] rv = new byte[totlen];
+        int idx = 0;
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, len);
+        System.arraycopy(version, 0, rv, idx, version.length);
+        idx += version.length;
+        System.arraycopy(serial, 0, rv, idx, serial.length);
+        idx += serial.length;
+        System.arraycopy(sigoid, 0, rv, idx, sigoid.length);
+        idx += sigoid.length;
+        System.arraycopy(issuer, 0, rv, idx, issuer.length);
+        idx += issuer.length;
+        System.arraycopy(validity, 0, rv, idx, validity.length);
+        idx += validity.length;
+        System.arraycopy(subject, 0, rv, idx, subject.length);
+        idx += subject.length;
+        System.arraycopy(pubbytes, 0, rv, idx, pubbytes.length);
+        idx += pubbytes.length;
+        System.arraycopy(extbytes, 0, rv, idx, extbytes.length);
+
+        if (DEBUG) {
+            System.out.println(HexDump.dump(version));
+            System.out.println("serial");
+            System.out.println(HexDump.dump(serial));
+            System.out.println("oid");
+            System.out.println(HexDump.dump(sigoid));
+            System.out.println("issuer");
+            System.out.println(HexDump.dump(issuer));
+            System.out.println("valid");
+            System.out.println(HexDump.dump(validity));
+            System.out.println("subject");
+            System.out.println(HexDump.dump(subject));
+            System.out.println("pub");
+            System.out.println(HexDump.dump(pubbytes));
+            System.out.println("extensions");
+            System.out.println(HexDump.dump(extbytes));
+            System.out.println("TBS cert");
+            System.out.println(HexDump.dump(rv));
+        }
+        return rv;
+    }
+
+    /**
+     *
+     *  @param crlNum 0-255 because lazy
+     *  @return ASN.1 encoded object
+     */
+    private static byte[] genTBSCRL(X509Certificate cert, int validDays,
+                                    int crlNum, byte[] sigalg) throws GeneralSecurityException {
+        // a0 ???, int = 2
+        byte[] version = { 2, 1, 1 };
+        byte[] issuer = cert.getIssuerX500Principal().getEncoded();
+
+        byte[] serial = cert.getSerialNumber().toByteArray();
+        if (serial.length > 255)
+            throw new IllegalArgumentException();
+        long now = System.currentTimeMillis();
+        long then = now + (validDays * 24L * 60 * 60 * 1000);
+        // used for CRL time and revocation time
+        byte[] nowbytes = getDate(now);
+        // used for next CRL time
+        byte[] thenbytes = getDate(then);
+
+        byte[] extbytes = getCRLExtensions(crlNum);
+
+        int revlen = 2 + serial.length + nowbytes.length;
+        int revseqlen = spaceFor(revlen);
+        int revsseqlen = spaceFor(revseqlen);
+
+
+        int len = version.length + sigalg.length + issuer.length + nowbytes.length +
+                  thenbytes.length + revsseqlen + extbytes.length;
+
+        int totlen = spaceFor(len);
+        byte[] rv = new byte[totlen];
+        int idx = 0;
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, len);
+        System.arraycopy(version, 0, rv, idx, version.length);
+        idx += version.length;
+        System.arraycopy(sigalg, 0, rv, idx, sigalg.length);
+        idx += sigalg.length;
+        System.arraycopy(issuer, 0, rv, idx, issuer.length);
+        idx += issuer.length;
+        System.arraycopy(nowbytes, 0, rv, idx, nowbytes.length);
+        idx += nowbytes.length;
+        System.arraycopy(thenbytes, 0, rv, idx, thenbytes.length);
+        idx += thenbytes.length;
+        // the certs
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, revseqlen);
+        // the cert
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, revlen);
+        rv[idx++] = 0x02;
+        rv[idx++] = (byte) serial.length;
+        System.arraycopy(serial, 0, rv, idx, serial.length);
+        idx += serial.length;
+        System.arraycopy(nowbytes, 0, rv, idx, nowbytes.length);
+        idx += nowbytes.length;
+        // extensions
+        System.arraycopy(extbytes, 0, rv, idx, extbytes.length);
+
+        if (DEBUG) {
+            System.out.println("version");
+            System.out.println(HexDump.dump(version));
+            System.out.println("sigalg");
+            System.out.println(HexDump.dump(sigalg));
+            System.out.println("issuer");
+            System.out.println(HexDump.dump(issuer));
+            System.out.println("now");
+            System.out.println(HexDump.dump(nowbytes));
+            System.out.println("then");
+            System.out.println(HexDump.dump(thenbytes));
+            System.out.println("serial");
+            System.out.println(HexDump.dump(serial));
+            System.out.println("extensions");
+            System.out.println(HexDump.dump(extbytes));
+            System.out.println("TBS CRL");
+            System.out.println(HexDump.dump(rv));
+        }
+        return rv;
+    }
+
+    /**
+     *  @param val the length of the value, 65535 max
+     *  @return the length of the TLV
+     */
+    private static int spaceFor(int val) {
+        int rv;
+        if (val > 255)
+            rv = 3;
+        else if (val > 127)
+            rv = 2;
+        else
+            rv = 1;
+        return 1 + rv + val;
+    }
+
+    /**
+     *  Sequence of two UTCDates
+     *  @return 32 bytes ASN.1 encoded object
+     */
+    private static byte[] getValidity(int validDays) {
+        byte[] rv = new byte[32];
+        rv[0] = 0x30;
+        rv[1] = 30;
+        long now = System.currentTimeMillis();
+        long then = now + (validDays * 24L * 60 * 60 * 1000);
+        byte[] nowbytes = getDate(now);
+        byte[] thenbytes = getDate(then);
+        System.arraycopy(nowbytes, 0, rv, 2, 15);
+        System.arraycopy(thenbytes, 0, rv, 17, 15);
+        return rv;
+    }
+
+    /**
+     *  A single UTCDate
+     *  @return 15 bytes ASN.1 encoded object
+     */
+    private static byte[] getDate(long now) {
+        // UTCDate format (HH 0-23)
+        SimpleDateFormat fmt = new SimpleDateFormat("yyMMddHHmmss");
+        fmt.setTimeZone(TimeZone.getTimeZone("GMT"));
+        byte[] nowbytes = DataHelper.getASCII(fmt.format(new Date(now)));
+        if (nowbytes.length != 12)
+            throw new IllegalArgumentException();
+        byte[] rv = new byte[15];
+        rv[0] = 0x17;
+        rv[1] = 13;
+        System.arraycopy(nowbytes, 0, rv, 2, 12);
+        rv[14] = (byte) 'Z';
+        return rv;
+    }
+
+    /**
+     *
+     *  @param pubbytes bit string
+     *  @return 35 bytes ASN.1 encoded object
+     */
+    private static byte[] getExtensions(byte[] pubbytes) {
+        // RFC 2549 sec. 4.2.1.2
+        // subject public key identifier is the sha1 hash of the bit string of the public key
+        // without the tag, length, and igore fields
+        int pidx = 1;
+        int skip = pubbytes[pidx++];
+        if ((skip & 0x80)!= 0)
+            pidx += skip & 0x80;
+        pidx++; // ignore
+        MessageDigest md = SHA1.getInstance();
+        md.update(pubbytes, pidx, pubbytes.length - pidx);
+        byte[] sha = md.digest();
+        byte[] oid = getEncodedOID(OID_SKI);
+
+        int wraplen = spaceFor(sha.length);
+        int extlen = oid.length + spaceFor(wraplen);
+        int extslen = spaceFor(extlen);
+        int seqlen = spaceFor(extslen);
+        int totlen = spaceFor(seqlen);
+        byte[] rv = new byte[totlen];
+        int idx = 0;
+        rv[idx++] = (byte) 0xa3;
+        idx = intToASN1(rv, idx, seqlen);
+        rv[idx++] = (byte) 0x30;
+        idx = intToASN1(rv, idx, extslen);
+        rv[idx++] = (byte) 0x30;
+        idx = intToASN1(rv, idx, extlen);
+        System.arraycopy(oid, 0, rv, idx, oid.length);
+        idx += oid.length;
+        // don't know why we wrap the octet string in an octet string
+        rv[idx++] = (byte) 0x04;
+        idx = intToASN1(rv, idx, wraplen);
+        rv[idx++] = (byte) 0x04;
+        idx = intToASN1(rv, idx, sha.length);
+        System.arraycopy(sha, 0, rv, idx, sha.length);
+        return rv;
+    }
+
+    /**
+     *
+     *  @param crlNum 0-255 because lazy
+     *  @return 16 bytes ASN.1 encoded object
+     */
+    private static byte[] getCRLExtensions(int crlNum) {
+        if (crlNum < 0 || crlNum > 255)
+            throw new IllegalArgumentException();
+        byte[] oid = getEncodedOID(OID_CRLNUM);
+        int extlen = oid.length + 5;
+        int extslen = spaceFor(extlen);
+        int seqlen = spaceFor(extslen);
+        int totlen = spaceFor(seqlen);
+        byte[] rv = new byte[totlen];
+        int idx = 0;
+        rv[idx++] = (byte) 0xa0;
+        idx = intToASN1(rv, idx, seqlen);
+        rv[idx++] = (byte) 0x30;
+        idx = intToASN1(rv, idx, extslen);
+        rv[idx++] = (byte) 0x30;
+        idx = intToASN1(rv, idx, extlen);
+        System.arraycopy(oid, 0, rv, idx, oid.length);
+        idx += oid.length;
+        // don't know why we wrap the int in an octet string
+        rv[idx++] = (byte) 0x04;
+        rv[idx++] = (byte) 3;
+        rv[idx++] = (byte) 0x02;
+        rv[idx++] = (byte) 1;
+        rv[idx++] = (byte) crlNum;
+        return rv;
+    }
+
+    /**
+     *  0x30 len 0x06 len encodedbytes... 0x05 0
+     *  @return ASN.1 encoded object
+     *  @throws IllegalArgumentException
+     */
+    private static byte[] getEncodedOIDSeq(String oid) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(16);
+        baos.write(0x30);
+        // len to be filled in later
+        baos.write(0);
+        byte[] b = getEncodedOID(oid);
+        baos.write(b, 0, b.length);
+        // NULL
+        baos.write(0x05);
+        baos.write(0);
+        byte[] rv = baos.toByteArray();
+        rv[1] = (byte) (rv.length - 2);
+        return rv;
+    }
+
+    /**
+     *  0x06 len encodedbytes...
+     *  @return ASN.1 encoded object
+     *  @throws IllegalArgumentException
+     */
+    private static byte[] getEncodedOID(String oid) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(16);
+        baos.write(0x06);
+        // len to be filled in later
+        baos.write(0);
+        String[] f = DataHelper.split(oid, "[.]");
+        if (f.length < 2)
+            throw new IllegalArgumentException("length: " + f.length);
+        baos.write((40 * Integer.parseInt(f[0])) + Integer.parseInt(f[1]));
+        for (int i = 2; i < f.length; i++) {
+            int v = Integer.parseInt(f[i]);
+            if (v >= 128 * 128 * 128 || v < 0)
+                throw new IllegalArgumentException();
+            if (v >= 128 * 128)
+                baos.write((v >> 14) | 0x80);
+            if (v >= 128)
+                baos.write((v >> 7) | 0x80);
+            baos.write(v & 0x7f);
+        }
+        byte[] rv = baos.toByteArray();
+        if (rv.length > 129)
+            throw new IllegalArgumentException();
+        rv[1] = (byte) (rv.length - 2);
+        return rv;
+    }
+
+/****
+    public static void main(String[] args) {
+        try {
+            test("test0", SigType.DSA_SHA1);
+            test("test1", SigType.ECDSA_SHA256_P256);
+            test("test2", SigType.ECDSA_SHA384_P384);
+            test("test3", SigType.ECDSA_SHA512_P521);
+            test("test4", SigType.RSA_SHA256_2048);
+            test("test5", SigType.RSA_SHA384_3072);
+            test("test6", SigType.RSA_SHA512_4096);
+            test("test7", SigType.EdDSA_SHA512_Ed25519);
+            test("test8", SigType.EdDSA_SHA512_Ed25519ph);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static final void test(String name, SigType type) throws Exception {
+            Object[] rv = generate("cname", "ou", "l", "o", "st", "c", 3652, type);
+            PublicKey jpub = (PublicKey) rv[0];
+            PrivateKey jpriv = (PrivateKey) rv[1];
+            X509Certificate cert = (X509Certificate) rv[2];
+            X509CRL crl = (X509CRL) rv[3];
+            File ks = new File(name + ".ks");
+            List<X509Certificate> certs = new ArrayList<X509Certificate>(1);
+            certs.add(cert);
+            KeyStoreUtil.storePrivateKey(ks, "changeit", "foo", "foobar", jpriv, certs);
+            System.out.println("Private key saved to " + ks + " with alias foo, password foobar, keystore password changeit");
+            File cf = new File(name + ".crt");
+            CertUtil.saveCert(cert, cf);
+            System.out.println("Certificate saved to " + cf);
+            File pf = new File(name + ".priv");
+            FileOutputStream pfs = new SecureFileOutputStream(pf);
+            KeyStoreUtil.exportPrivateKey(ks, "changeit", "foo", "foobar", pfs);
+            pfs.close();
+            System.out.println("Private key saved to " + pf);
+            File cr = new File(name + ".crl");
+            CertUtil.saveCRL(crl, cr);
+            System.out.println("CRL saved to " + cr);
+    }
+****/
+}
diff --git a/core/java/src/net/i2p/crypto/SigAlgo.java b/core/java/src/net/i2p/crypto/SigAlgo.java
index 8e0523c1bbd1222ffdac757b10d116d47b639e14..1195df148bab713c595fb35653be24ab7a36a46f 100644
--- a/core/java/src/net/i2p/crypto/SigAlgo.java
+++ b/core/java/src/net/i2p/crypto/SigAlgo.java
@@ -10,7 +10,15 @@ public enum SigAlgo {
     DSA("DSA"),
     EC("EC"),
     EdDSA("EdDSA"),
-    RSA("RSA")
+    /**
+     *  For local use only, not for use in the network.
+     */
+    RSA("RSA"),
+    /**
+     *  For local use only, not for use in the network.
+     *  @since 0.9.25
+     */
+    ElGamal("ElGamal")
     ;
 
     private final String name;
diff --git a/core/java/src/net/i2p/crypto/SigType.java b/core/java/src/net/i2p/crypto/SigType.java
index 05dd1906e7760c3e198853abf28175eac85c83c6..5da9e22b0475adf6b20817d229328ed0f11b270c 100644
--- a/core/java/src/net/i2p/crypto/SigType.java
+++ b/core/java/src/net/i2p/crypto/SigType.java
@@ -32,20 +32,20 @@ public enum SigType {
      *  Pubkey 128 bytes; privkey 20 bytes; hash 20 bytes; sig 40 bytes
      *  @since 0.9.8
      */
-    DSA_SHA1(0, 128, 20, 20, 40, SigAlgo.DSA, "SHA-1", "SHA1withDSA", CryptoConstants.DSA_SHA1_SPEC, "0"),
+    DSA_SHA1(0, 128, 20, 20, 40, SigAlgo.DSA, "SHA-1", "SHA1withDSA", CryptoConstants.DSA_SHA1_SPEC, "1.2.840.10040.4.3", "0"),
     /**  Pubkey 64 bytes; privkey 32 bytes; hash 32 bytes; sig 64 bytes */
-    ECDSA_SHA256_P256(1, 64, 32, 32, 64, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.P256_SPEC, "0.9.12"),
+    ECDSA_SHA256_P256(1, 64, 32, 32, 64, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.P256_SPEC, "1.2.840.10045.4.3.2", "0.9.12"),
     /**  Pubkey 96 bytes; privkey 48 bytes; hash 48 bytes; sig 96 bytes */
-    ECDSA_SHA384_P384(2, 96, 48, 48, 96, SigAlgo.EC, "SHA-384", "SHA384withECDSA", ECConstants.P384_SPEC, "0.9.12"),
+    ECDSA_SHA384_P384(2, 96, 48, 48, 96, SigAlgo.EC, "SHA-384", "SHA384withECDSA", ECConstants.P384_SPEC, "1.2.840.10045.4.3.3", "0.9.12"),
     /**  Pubkey 132 bytes; privkey 66 bytes; hash 64 bytes; sig 132 bytes */
-    ECDSA_SHA512_P521(3, 132, 66, 64, 132, SigAlgo.EC, "SHA-512", "SHA512withECDSA", ECConstants.P521_SPEC, "0.9.12"),
+    ECDSA_SHA512_P521(3, 132, 66, 64, 132, SigAlgo.EC, "SHA-512", "SHA512withECDSA", ECConstants.P521_SPEC, "1.2.840.10045.4.3.4", "0.9.12"),
 
     /**  Pubkey 256 bytes; privkey 512 bytes; hash 32 bytes; sig 256 bytes */
-    RSA_SHA256_2048(4, 256, 512, 32, 256, SigAlgo.RSA, "SHA-256", "SHA256withRSA", RSAConstants.F4_2048_SPEC, "0.9.12"),
+    RSA_SHA256_2048(4, 256, 512, 32, 256, SigAlgo.RSA, "SHA-256", "SHA256withRSA", RSAConstants.F4_2048_SPEC, "1.2.840.113549.1.1.11", "0.9.12"),
     /**  Pubkey 384 bytes; privkey 768 bytes; hash 48 bytes; sig 384 bytes */
-    RSA_SHA384_3072(5, 384, 768, 48, 384, SigAlgo.RSA, "SHA-384", "SHA384withRSA", RSAConstants.F4_3072_SPEC, "0.9.12"),
+    RSA_SHA384_3072(5, 384, 768, 48, 384, SigAlgo.RSA, "SHA-384", "SHA384withRSA", RSAConstants.F4_3072_SPEC, "1.2.840.113549.1.1.12", "0.9.12"),
     /**  Pubkey 512 bytes; privkey 1024 bytes; hash 64 bytes; sig 512 bytes */
-    RSA_SHA512_4096(6, 512, 1024, 64, 512, SigAlgo.RSA, "SHA-512", "SHA512withRSA", RSAConstants.F4_4096_SPEC, "0.9.12"),
+    RSA_SHA512_4096(6, 512, 1024, 64, 512, SigAlgo.RSA, "SHA-512", "SHA512withRSA", RSAConstants.F4_4096_SPEC, "1.2.840.113549.1.1.13", "0.9.12"),
 
     /**
      *  Pubkey 32 bytes; privkey 32 bytes; hash 64 bytes; sig 64 bytes
@@ -55,8 +55,17 @@ public enum SigType {
      *  @since 0.9.15
      */
     EdDSA_SHA512_Ed25519(7, 32, 32, 64, 64, SigAlgo.EdDSA, "SHA-512", "SHA512withEdDSA",
-                         EdDSANamedCurveTable.getByName("ed25519-sha-512"), "0.9.17");
+                         EdDSANamedCurveTable.getByName("ed25519-sha-512"), "1.3.101.101", "0.9.17"),
 
+    /**
+     *  Prehash version (double hashing, for offline use such as su3, not for use on the network)
+     *  Pubkey 32 bytes; privkey 32 bytes; hash 64 bytes; sig 64 bytes
+     *  @since 0.9.25
+     */
+    EdDSA_SHA512_Ed25519ph(8, 32, 32, 64, 64, SigAlgo.EdDSA, "SHA-512", "NonewithEdDSA",
+                           EdDSANamedCurveTable.getByName("ed25519-sha-512"), "1.3.101.101", "0.9.25"),
+
+    ;
 
     // TESTING....................
 
@@ -99,12 +108,12 @@ public enum SigType {
 
     private final int code, pubkeyLen, privkeyLen, hashLen, sigLen;
     private final SigAlgo base;
-    private final String digestName, algoName, since;
+    private final String digestName, algoName, oid, since;
     private final AlgorithmParameterSpec params;
     private final boolean isAvail;
 
     SigType(int cod, int pubLen, int privLen, int hLen, int sLen, SigAlgo baseAlgo,
-            String mdName, String aName, AlgorithmParameterSpec pSpec, String supportedSince) {
+            String mdName, String aName, AlgorithmParameterSpec pSpec, String oid, String supportedSince) {
         code = cod;
         pubkeyLen = pubLen;
         privkeyLen = privLen;
@@ -114,6 +123,7 @@ public enum SigType {
         digestName = mdName;
         algoName = aName;
         params = pSpec;
+        this.oid = oid;
         since = supportedSince;
         isAvail = x_isAvailable();
     }
@@ -183,6 +193,15 @@ public enum SigType {
         return since;
     }
 
+    /**
+     *  The OID for the signature.
+     *
+     *  @since 0.9.25
+     */
+    public String getOID() {
+        return oid;
+    }
+
     /**
      *  @since 0.9.12
      *  @return true if supported in this JVM
@@ -274,6 +293,8 @@ public enum SigType {
             // handle mixed-case enum
             if (uc.equals("EDDSA_SHA512_ED25519"))
                 return EdDSA_SHA512_Ed25519;
+            if (uc.equals("EDDSA_SHA512_ED25519PH"))
+                return EdDSA_SHA512_Ed25519ph;
             return valueOf(uc);
         } catch (IllegalArgumentException iae) {
             try {
diff --git a/core/java/src/net/i2p/crypto/SigUtil.java b/core/java/src/net/i2p/crypto/SigUtil.java
index d04abb438d125646c53760906d2bcd4beeeae13b..06d84770506aaee3c83c45097db5dc33ae3365d1 100644
--- a/core/java/src/net/i2p/crypto/SigUtil.java
+++ b/core/java/src/net/i2p/crypto/SigUtil.java
@@ -50,7 +50,7 @@ import net.i2p.util.NativeBigInteger;
  *
  * @since 0.9.9, public since 0.9.12
  */
-public class SigUtil {
+public final class SigUtil {
 
     private static final Map<SigningPublicKey, ECPublicKey> _ECPubkeyCache = new LHMCache<SigningPublicKey, ECPublicKey>(64);
     private static final Map<SigningPrivateKey, ECPrivateKey> _ECPrivkeyCache = new LHMCache<SigningPrivateKey, ECPrivateKey>(16);
@@ -141,7 +141,7 @@ public class SigUtil {
                 throw new IllegalArgumentException("Unknown RSA type");
             return fromJavaKey(k, type);
         }
-        throw new IllegalArgumentException("Unknown type");
+        throw new IllegalArgumentException("Unknown type: " + pk.getClass());
     }
 
     /**
@@ -161,7 +161,7 @@ public class SigUtil {
             case RSA:
                 return fromJavaKey((RSAPublicKey) pk, type);
             default:
-                throw new IllegalArgumentException();
+                throw new IllegalArgumentException("Unknown type: " + type);
         }
     }
 
@@ -209,7 +209,7 @@ public class SigUtil {
                 throw new IllegalArgumentException("Unknown RSA type");
             return fromJavaKey(k, type);
         }
-        throw new IllegalArgumentException("Unknown type");
+        throw new IllegalArgumentException("Unknown type: " + pk.getClass());
     }
 
     /**
@@ -229,7 +229,7 @@ public class SigUtil {
             case RSA:
                 return fromJavaKey((RSAPrivateKey) pk, type);
             default:
-                throw new IllegalArgumentException();
+                throw new IllegalArgumentException("Unknown type: " + type);
         }
     }
 
@@ -549,9 +549,13 @@ public class SigUtil {
 
     /**
      *  Split a byte array into two BigIntegers
+     *  @param b length must be even
      *  @return array of two BigIntegers
+     *  @since 0.9.9
      */
-    private static BigInteger[] split(byte[] b) {
+    private static NativeBigInteger[] split(byte[] b) {
+        if ((b.length & 0x01) != 0)
+            throw new IllegalArgumentException("length must be even");
         int sublen = b.length / 2;
         byte[] bx = new byte[sublen];
         byte[] by = new byte[sublen];
@@ -565,9 +569,12 @@ public class SigUtil {
     /**
      *  Combine two BigIntegers of nominal length = len / 2
      *  @return array of exactly len bytes
+     *  @since 0.9.9
      */
     private static byte[] combine(BigInteger x, BigInteger y, int len)
                               throws InvalidKeyException {
+        if ((len & 0x01) != 0)
+            throw new InvalidKeyException("length must be even");
         int sublen = len / 2;
         byte[] b = new byte[len];
         byte[] bx = rectify(x, sublen);
@@ -609,7 +616,8 @@ public class SigUtil {
 
     /**
      *  http://download.oracle.com/javase/1.5.0/docs/guide/security/CryptoSpec.html
-     *  Signature Format	ASN.1 sequence of two INTEGER values: r and s, in that order:
+     *<pre>
+     *  Signature Format: ASN.1 sequence of two INTEGER values: r and s, in that order:
      *                                SEQUENCE ::= { r INTEGER, s INTEGER }
      *
      *  http://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One
@@ -619,6 +627,7 @@ public class SigUtil {
      *  02 -- tag indicating INTEGER
      *  xx - length in octets
      *  xxxxxx - value
+     *</pre>
      *
      *  Convert to BigInteger and back so we have the minimum length representation, as required.
      *  r and s are always non-negative.
@@ -626,57 +635,107 @@ public class SigUtil {
      *  Only supports sigs up to about 252 bytes. See code to fix BER encoding for this before you
      *  add a SigType with bigger signatures.
      *
+     *  @param sig length must be even
      *  @throws IllegalArgumentException if too big
      *  @since 0.8.7, moved to SigUtil in 0.9.9
      */
     private static byte[] sigBytesToASN1(byte[] sig) {
-        //System.out.println("pre TO asn1\n" + net.i2p.util.HexDump.dump(sig));
-        int len = sig.length;
-        int sublen = len / 2;
-        byte[] tmp = new byte[sublen];
+        BigInteger[] rs = split(sig);
+        return sigBytesToASN1(rs[0], rs[1]);
+    }
 
-        System.arraycopy(sig, 0, tmp, 0, sublen);
-        BigInteger r = new BigInteger(1, tmp);
+    /**
+     *  http://download.oracle.com/javase/1.5.0/docs/guide/security/CryptoSpec.html
+     *<pre>
+     *  Signature Format: ASN.1 sequence of two INTEGER values: r and s, in that order:
+     *                                SEQUENCE ::= { r INTEGER, s INTEGER }
+     *
+     *  http://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One
+     *  30 -- tag indicating SEQUENCE
+     *  xx - length in octets
+     *
+     *  02 -- tag indicating INTEGER
+     *  xx - length in octets
+     *  xxxxxx - value
+     *</pre>
+     *
+     *  r and s are always non-negative.
+     *
+     *  Only supports sigs up to about 65530 bytes. See code to fix BER encoding for this before you
+     *  add a SigType with bigger signatures.
+     *
+     *  @throws IllegalArgumentException if too big
+     *  @since 0.9.25, split out from sigBytesToASN1(byte[])
+     */
+    public static byte[] sigBytesToASN1(BigInteger r, BigInteger s) {
+        int extra = 4;
         byte[] rb = r.toByteArray();
-        if (rb.length > 127)
-            throw new IllegalArgumentException("FIXME R length > 127");
-        System.arraycopy(sig, sublen, tmp, 0, sublen);
-        BigInteger s = new BigInteger(1, tmp);
+        if (rb.length > 127) {
+            extra++;
+            if (rb.length > 255)
+                extra++;
+        }
         byte[] sb = s.toByteArray();
-        if (sb.length > 127)
-            throw new IllegalArgumentException("FIXME S length > 127");
-        int seqlen = rb.length + sb.length + 4;
-        if (seqlen > 255)
-            throw new IllegalArgumentException("FIXME seq length > 255");
+        if (sb.length > 127) {
+            extra++;
+            if (sb.length > 255)
+                extra++;
+        }
+        int seqlen = rb.length + sb.length + extra;
         int totlen = seqlen + 2;
-        if (seqlen > 127)
+        if (seqlen > 127) {
             totlen++;
+            if (seqlen > 255)
+                totlen++;
+        }
         byte[] rv = new byte[totlen];
         int idx = 0;
 
         rv[idx++] = 0x30;
-        if (seqlen > 127)
-            rv[idx++] =(byte) 0x81;
-        rv[idx++] = (byte) seqlen;
+        idx = intToASN1(rv, idx, seqlen);
 
         rv[idx++] = 0x02;
-        rv[idx++] = (byte) rb.length;
+        idx = intToASN1(rv, idx, rb.length);
         System.arraycopy(rb, 0, rv, idx, rb.length);
         idx += rb.length;
 
         rv[idx++] = 0x02;
-        rv[idx++] = (byte) sb.length;
+        idx = intToASN1(rv, idx, sb.length);
         System.arraycopy(sb, 0, rv, idx, sb.length);
 
         //System.out.println("post TO asn1\n" + net.i2p.util.HexDump.dump(rv));
         return rv;
     }
 
+    /**
+     *  Output an length or integer value in ASN.1
+     *  Does NOT output the tag e.g. 0x02 / 0x30
+     *
+     *  @param val 0-65535
+     *  @return the new index
+     *  @since 0.9.25
+     */
+    public static int intToASN1(byte[] d, int idx, int val) {
+        if (val < 0 || val > 65535)
+            throw new IllegalArgumentException("fixme length " + val);
+        if (val > 127) {
+            if (val > 255) {
+                d[idx++] = (byte) 0x82;
+                d[idx++] = (byte) (val >> 8);
+            } else {
+                d[idx++] = (byte) 0x81;
+            }
+        }
+        d[idx++] = (byte) val;
+        return idx;
+    }
+
     /**
      *  See above.
-     *  Only supports sigs up to about 252 bytes. See code to fix BER encoding for bigger than that.
+     *  Only supports sigs up to about 65530 bytes. See code to fix BER encoding for bigger than that.
      *
-     *  @return len bytes
+     *  @param len must be even, twice the nominal length of each BigInteger
+     *  @return len bytes, call split() on the result to get two BigIntegers
      *  @since 0.8.7, moved to SigUtil in 0.9.9
      */
     private static byte[] aSN1ToSigBytes(byte[] asn, int len)
@@ -693,8 +752,17 @@ public class SigUtil {
         byte[] rv = new byte[len];
         int sublen = len / 2;
         int rlen = asn[++idx];
-        if ((rlen & 0x80) != 0)
-            throw new SignatureException("FIXME R length > 127");
+        if ((rlen & 0x80) != 0) {
+            if ((rlen & 0xff) == 0x81) { 
+                rlen = asn[++idx] & 0xff;
+            } else if ((rlen & 0xff) == 0x82) {
+                rlen = asn[++idx] & 0xff;
+                rlen <<= 8;
+                rlen |= asn[++idx] & 0xff;
+            } else {
+                throw new SignatureException("FIXME R length > 65535");
+            }
+        }
         if ((asn[++idx] & 0x80) != 0)
             throw new SignatureException("R is negative");
         if (rlen > sublen + 1)
@@ -704,24 +772,47 @@ public class SigUtil {
         else
             System.arraycopy(asn, idx, rv, sublen - rlen, rlen);
         idx += rlen;
-        int slenloc = idx + 1;
+
         if (asn[idx] != 0x02)
             throw new SignatureException("asn[s] = " + (asn[idx] & 0xff));
-        int slen = asn[slenloc];
-        if ((slen & 0x80) != 0)
-            throw new SignatureException("FIXME S length > 127");
-        if ((asn[slenloc + 1] & 0x80) != 0)
+        int slen = asn[++idx];
+        if ((slen & 0x80) != 0) {
+            if ((slen & 0xff) == 0x81) { 
+                slen = asn[++idx] & 0xff;
+            } else if ((slen & 0xff) == 0x82) {
+                slen = asn[++idx] & 0xff;
+                slen <<= 8;
+                slen |= asn[++idx] & 0xff;
+            } else {
+                throw new SignatureException("FIXME S length > 65535");
+            }
+        }
+        if ((asn[++idx] & 0x80) != 0)
             throw new SignatureException("S is negative");
         if (slen > sublen + 1)
             throw new SignatureException("S too big " + slen);
         if (slen == sublen + 1)
-            System.arraycopy(asn, slenloc + 2, rv, sublen, sublen);
+            System.arraycopy(asn, idx + 1, rv, sublen, sublen);
         else
-            System.arraycopy(asn, slenloc + 1, rv, len - slen, slen);
+            System.arraycopy(asn, idx, rv, len - slen, slen);
         //System.out.println("post from asn1\n" + net.i2p.util.HexDump.dump(rv));
         return rv;
     }
 
+    /**
+     *  See above.
+     *  Only supports sigs up to about 65530 bytes. See code to fix BER encoding for bigger than that.
+     *
+     *  @param len nominal length of each BigInteger
+     *  @return two BigIntegers
+     *  @since 0.9.25
+     */
+    public static NativeBigInteger[] aSN1ToBigInteger(byte[] asn, int len)
+                              throws SignatureException {
+        byte[] sig = aSN1ToSigBytes(asn, len * 2);
+        return split(sig);
+    }
+
     public static void clearCaches() {
         synchronized(_ECPubkeyCache) {
             _ECPubkeyCache.clear();
diff --git a/core/java/src/net/i2p/crypto/YKGenerator.java b/core/java/src/net/i2p/crypto/YKGenerator.java
index 30245c22992d0b93ded777188f894733c83916e2..7a69eee367e828c645489294a87f3567dff2c26f 100644
--- a/core/java/src/net/i2p/crypto/YKGenerator.java
+++ b/core/java/src/net/i2p/crypto/YKGenerator.java
@@ -35,7 +35,7 @@ import net.i2p.util.SystemVersion;
  *
  * @author jrandom
  */
-class YKGenerator {
+final class YKGenerator {
     //private final static Log _log = new Log(YKGenerator.class);
     private final int MIN_NUM_BUILDERS;
     private final int MAX_NUM_BUILDERS;
diff --git a/core/java/src/net/i2p/crypto/eddsa/EdDSAEngine.java b/core/java/src/net/i2p/crypto/eddsa/EdDSAEngine.java
index 6cf9b4d859d371dbc50af63e074799354c7faab9..d39863c38dfb8b66a547648fbd93602a7a4eff8f 100644
--- a/core/java/src/net/i2p/crypto/eddsa/EdDSAEngine.java
+++ b/core/java/src/net/i2p/crypto/eddsa/EdDSAEngine.java
@@ -2,6 +2,7 @@ package net.i2p.crypto.eddsa;
 
 import java.io.ByteArrayOutputStream;
 import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -9,6 +10,7 @@ import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.Signature;
 import java.security.SignatureException;
+import java.security.spec.AlgorithmParameterSpec;
 import java.util.Arrays;
 
 import net.i2p.crypto.eddsa.math.Curve;
@@ -16,21 +18,68 @@ import net.i2p.crypto.eddsa.math.GroupElement;
 import net.i2p.crypto.eddsa.math.ScalarOps;
 
 /**
+ * Signing and verification for EdDSA.
+ *<p>
+ * The EdDSA sign and verify algorithms do not interact well with
+ * the Java Signature API, as one or more update() methods must be
+ * called before sign() or verify(). Using the standard API,
+ * this implementation must copy and buffer all data passed in
+ * via update().
+ *</p><p>
+ * This implementation offers two ways to avoid this copying,
+ * but only if all data to be signed or verified is available
+ * in a single byte array.
+ *</p><p>
+ *Option 1:
+ *</p><ol>
+ *<li>Call initSign() or initVerify() as usual.
+ *</li><li>Call setParameter(ONE_SHOT_MOE)
+ *</li><li>Call update(byte[]) or update(byte[], int, int) exactly once
+ *</li><li>Call sign() or verify() as usual.
+ *</li><li>If doing additional one-shot signs or verifies with this object, you must
+ *         call setParameter(ONE_SHOT_MODE) each time
+ *</li></ol>
+ *
+ *<p>
+ *Option 2:
+ *</p><ol>
+ *<li>Call initSign() or initVerify() as usual.
+ *</li><li>Call one of the signOneShot() or verifyOneShot() methods.
+ *</li><li>If doing additional one-shot signs or verifies with this object,
+ *         just call signOneShot() or verifyOneShot() again.
+ *</li></ol>
+ *
  * @since 0.9.15
  * @author str4d
  *
  */
-public class EdDSAEngine extends Signature {
+public final class EdDSAEngine extends Signature {
     private MessageDigest digest;
-    private final ByteArrayOutputStream baos;
+    private ByteArrayOutputStream baos;
     private EdDSAKey key;
+    private boolean oneShotMode;
+    private byte[] oneShotBytes;
+    private int oneShotOffset;
+    private int oneShotLength;
+
+    /**
+     *  To efficiently sign or verify data in one shot, pass this to setParameters()
+     *  after initSign() or initVerify() but BEFORE THE FIRST AND ONLY
+     *  update(data) or update(data, off, len). The data reference will be saved
+     *  and then used in sign() or verify() without copying the data.
+     *  Violate these rules and you will get a SignatureException.
+     *
+     *  @since 0.9.25
+     */
+    public static final AlgorithmParameterSpec ONE_SHOT_MODE = new OneShotSpec();
+
+    private static class OneShotSpec implements AlgorithmParameterSpec {}
 
     /**
      * No specific hash requested, allows any EdDSA key.
      */
     public EdDSAEngine() {
         super("EdDSA");
-        baos = new ByteArrayOutputStream(256);
     }
 
     /**
@@ -42,12 +91,21 @@ public class EdDSAEngine extends Signature {
         this.digest = digest;
     }
 
-    @Override
-    protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
+    /**
+     * @since 0.9.25
+     */
+    private void reset() {
         if (digest != null)
             digest.reset();
-        baos.reset();
+        if (baos != null)
+            baos.reset();
+        oneShotMode = false;
+        oneShotBytes = null;
+    }
 
+    @Override
+    protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
+        reset();
         if (privateKey instanceof EdDSAPrivateKey) {
             EdDSAPrivateKey privKey = (EdDSAPrivateKey) privateKey;
             key = privKey;
@@ -61,21 +119,22 @@ public class EdDSAEngine extends Signature {
                 }
             } else if (!key.getParams().getHashAlgorithm().equals(digest.getAlgorithm()))
                 throw new InvalidKeyException("Key hash algorithm does not match chosen digest");
+            digestInitSign(privKey);
+        } else {
+            throw new InvalidKeyException("cannot identify EdDSA private key: " + privateKey.getClass());
+        }
+    }
 
-            // Preparing for hash
-            // r = H(h_b,...,h_2b-1,M)
-            int b = privKey.getParams().getCurve().getField().getb();
-            digest.update(privKey.getH(), b/8, b/4 - b/8);
-        } else
-            throw new InvalidKeyException("cannot identify EdDSA private key.");
+    private void digestInitSign(EdDSAPrivateKey privKey) {
+        // Preparing for hash
+        // r = H(h_b,...,h_2b-1,M)
+        int b = privKey.getParams().getCurve().getField().getb();
+        digest.update(privKey.getH(), b/8, b/4 - b/8);
     }
 
     @Override
     protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
-        if (digest != null)
-            digest.reset();
-        baos.reset();
-
+        reset();
         if (publicKey instanceof EdDSAPublicKey) {
             key = (EdDSAPublicKey) publicKey;
 
@@ -88,34 +147,79 @@ public class EdDSAEngine extends Signature {
                 }
             } else if (!key.getParams().getHashAlgorithm().equals(digest.getAlgorithm()))
                 throw new InvalidKeyException("Key hash algorithm does not match chosen digest");
-        } else
-            throw new InvalidKeyException("cannot identify EdDSA public key.");
+        } else {
+            throw new InvalidKeyException("cannot identify EdDSA public key: " + publicKey.getClass());
+        }
     }
 
+    /**
+     * @throws SignatureException if in one-shot mode
+     */
     @Override
     protected void engineUpdate(byte b) throws SignatureException {
-        // We need to store the message because it is used in several hashes
-        // XXX Can this be done more efficiently?
+        if (oneShotMode)
+            throw new SignatureException("unsupported in one-shot mode");
+        if (baos == null)
+            baos = new ByteArrayOutputStream(256);
         baos.write(b);
     }
 
+    /**
+     * @throws SignatureException if one-shot rules are violated
+     */
     @Override
     protected void engineUpdate(byte[] b, int off, int len)
             throws SignatureException {
-        // We need to store the message because it is used in several hashes
-        // XXX Can this be done more efficiently?
-        baos.write(b, off, len);
+        if (oneShotMode) {
+            if (oneShotBytes != null)
+                throw new SignatureException("update() already called");
+            oneShotBytes = b;
+            oneShotOffset = off;
+            oneShotLength = len;
+        } else {
+            if (baos == null)
+                baos = new ByteArrayOutputStream(256);
+            baos.write(b, off, len);
+        }
     }
 
     @Override
     protected byte[] engineSign() throws SignatureException {
+        try {
+            return x_engineSign();
+        } finally {
+            reset();
+            // must leave the object ready to sign again with
+            // the same key, as required by the API
+            EdDSAPrivateKey privKey = (EdDSAPrivateKey) key;
+            digestInitSign(privKey);
+        }
+    }
+
+    private byte[] x_engineSign() throws SignatureException {
         Curve curve = key.getParams().getCurve();
         ScalarOps sc = key.getParams().getScalarOps();
         byte[] a = ((EdDSAPrivateKey) key).geta();
 
-        byte[] message = baos.toByteArray();
+        byte[] message;
+        int offset, length;
+        if (oneShotMode) {
+            if (oneShotBytes == null)
+                throw new SignatureException("update() not called first");
+            message = oneShotBytes;
+            offset = oneShotOffset;
+            length = oneShotLength;
+        } else {
+            if (baos == null)
+                message = new byte[0];
+            else
+                message = baos.toByteArray();
+            offset = 0;
+            length = message.length;
+        }
         // r = H(h_b,...,h_2b-1,M)
-        byte[] r = digest.digest(message);
+        digest.update(message, offset, length);
+        byte[] r = digest.digest();
 
         // r mod l
         // Reduces r from 64 bytes to 32 bytes
@@ -128,7 +232,8 @@ public class EdDSAEngine extends Signature {
         // S = (r + H(Rbar,Abar,M)*a) mod l
         digest.update(Rbyte);
         digest.update(((EdDSAPrivateKey) key).getAbyte());
-        byte[] h = digest.digest(message);
+        digest.update(message, offset, length);
+        byte[] h = digest.digest();
         h = sc.reduce(h);
         byte[] S = sc.multiplyAndAdd(h, a, r);
 
@@ -141,6 +246,14 @@ public class EdDSAEngine extends Signature {
 
     @Override
     protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
+        try {
+            return x_engineVerify(sigBytes);
+        } finally {
+            reset();
+        }
+    }
+
+    private boolean x_engineVerify(byte[] sigBytes) throws SignatureException {
         Curve curve = key.getParams().getCurve();
         int b = curve.getField().getb();
         if (sigBytes.length != b/4)
@@ -150,8 +263,24 @@ public class EdDSAEngine extends Signature {
         digest.update(sigBytes, 0, b/8);
         digest.update(((EdDSAPublicKey) key).getAbyte());
         // h = H(Rbar,Abar,M)
-        byte[] message = baos.toByteArray();
-        byte[] h = digest.digest(message);
+        byte[] message;
+        int offset, length;
+        if (oneShotMode) {
+            if (oneShotBytes == null)
+                throw new SignatureException("update() not called first");
+            message = oneShotBytes;
+            offset = oneShotOffset;
+            length = oneShotLength;
+        } else {
+            if (baos == null)
+                message = new byte[0];
+            else
+                message = baos.toByteArray();
+            offset = 0;
+            length = message.length;
+        }
+        digest.update(message, offset, length);
+        byte[] h = digest.digest();
 
         // h mod l
         h = key.getParams().getScalarOps().reduce(h);
@@ -171,6 +300,140 @@ public class EdDSAEngine extends Signature {
         return true;
     }
 
+    /**
+     *  To efficiently sign all the data in one shot, if it is available,
+     *  use this method, which will avoid copying the data.
+     *
+     * Same as:
+     *<pre>
+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data)
+     *  sig = sign()
+     *</pre>
+     *
+     * @throws SignatureException if update() already called
+     * @see #ONE_SHOT_MODE
+     * @since 0.9.25
+     */
+    public byte[] signOneShot(byte[] data) throws SignatureException {
+        return signOneShot(data, 0, data.length);
+    }
+
+    /**
+     *  To efficiently sign all the data in one shot, if it is available,
+     *  use this method, which will avoid copying the data.
+     *
+     * Same as:
+     *<pre>
+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data, off, len)
+     *  sig = sign()
+     *</pre>
+     *
+     * @throws SignatureException if update() already called
+     * @see #ONE_SHOT_MODE
+     * @since 0.9.25
+     */
+    public byte[] signOneShot(byte[] data, int off, int len) throws SignatureException {
+        oneShotMode = true;
+        update(data, off, len);
+        return sign();
+    }
+
+    /**
+     *  To efficiently verify all the data in one shot, if it is available,
+     *  use this method, which will avoid copying the data.
+     *
+     * Same as:
+     *<pre>
+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data)
+     *  ok = verify(signature)
+     *</pre>
+     *
+     * @throws SignatureException if update() already called
+     * @see #ONE_SHOT_MODE
+     * @since 0.9.25
+     */
+    public boolean verifyOneShot(byte[] data, byte[] signature) throws SignatureException {
+        return verifyOneShot(data, 0, data.length, signature, 0, signature.length);
+    }
+
+    /**
+     *  To efficiently verify all the data in one shot, if it is available,
+     *  use this method, which will avoid copying the data.
+     *
+     * Same as:
+     *<pre>
+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data, off, len)
+     *  ok = verify(signature)
+     *</pre>
+     *
+     * @throws SignatureException if update() already called
+     * @see #ONE_SHOT_MODE
+     * @since 0.9.25
+     */
+    public boolean verifyOneShot(byte[] data, int off, int len, byte[] signature) throws SignatureException {
+        return verifyOneShot(data, off, len, signature, 0, signature.length);
+    }
+
+    /**
+     *  To efficiently verify all the data in one shot, if it is available,
+     *  use this method, which will avoid copying the data.
+     *
+     * Same as:
+     *<pre>
+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data)
+     *  ok = verify(signature, sigoff, siglen)
+     *</pre>
+     *
+     * @throws SignatureException if update() already called
+     * @see #ONE_SHOT_MODE
+     * @since 0.9.25
+     */
+    public boolean verifyOneShot(byte[] data, byte[] signature, int sigoff, int siglen) throws SignatureException {
+        return verifyOneShot(data, 0, data.length, signature, sigoff, siglen);
+    }
+
+    /**
+     *  To efficiently verify all the data in one shot, if it is available,
+     *  use this method, which will avoid copying the data.
+     *
+     * Same as:
+     *<pre>
+     *  setParameter(ONE_SHOT_MODE)
+     *  update(data, off, len)
+     *  ok = verify(signature, sigoff, siglen)
+     *</pre>
+     *
+     * @throws SignatureException if update() already called
+     * @see #ONE_SHOT_MODE
+     * @since 0.9.25
+     */
+    public boolean verifyOneShot(byte[] data, int off, int len, byte[] signature, int sigoff, int siglen) throws SignatureException {
+        oneShotMode = true;
+        update(data, off, len);
+        return verify(signature, sigoff, siglen);
+    }
+
+    /**
+     * @throws InvalidAlgorithmParameterException if spec is ONE_SHOT_MODE and update() already called
+     * @see #ONE_SHOT_MODE
+     * @since 0.9.25
+     */
+    @Override
+    protected void engineSetParameter(AlgorithmParameterSpec spec) throws InvalidAlgorithmParameterException {
+        if (spec.equals(ONE_SHOT_MODE)) {
+            if (oneShotBytes != null || baos != null && baos.size() > 0)
+                throw new InvalidAlgorithmParameterException("update() already called");
+            oneShotMode = true;
+        } else {
+            super.engineSetParameter(spec);
+        }
+    }
+
     /**
      * @deprecated replaced with <a href="#engineSetParameter(java.security.spec.AlgorithmParameterSpec)">
      */
diff --git a/core/java/src/net/i2p/crypto/eddsa/EdDSAPrivateKey.java b/core/java/src/net/i2p/crypto/eddsa/EdDSAPrivateKey.java
index 4136b321a72222ce14374efcca9029680227c4cc..3092ee12ad1a6615a81646a29d9a93fd5fe9e1c0 100644
--- a/core/java/src/net/i2p/crypto/eddsa/EdDSAPrivateKey.java
+++ b/core/java/src/net/i2p/crypto/eddsa/EdDSAPrivateKey.java
@@ -1,13 +1,24 @@
 package net.i2p.crypto.eddsa;
 
 import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Arrays;
 
 import net.i2p.crypto.eddsa.math.GroupElement;
+import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
 import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
 import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
 
 /**
  * An EdDSA private key.
+ *<p>
+ * Warning: Private key encoding is not fully specified in the
+ * current IETF draft. This implementation uses PKCS#8 encoding,
+ * and is subject to change. See getEncoded().
+ *</p><p>
+ * Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
+ *</p>
  *
  * @since 0.9.15
  * @author str4d
@@ -31,6 +42,14 @@ public class EdDSAPrivateKey implements EdDSAKey, PrivateKey {
         this.edDsaSpec = spec.getParams();
     }
 
+    /**
+     *  @since 0.9.25
+     */
+    public EdDSAPrivateKey(PKCS8EncodedKeySpec spec) throws InvalidKeySpecException {
+        this(new EdDSAPrivateKeySpec(decode(spec.getEncoded()),
+                                     EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)));
+    }
+
     public String getAlgorithm() {
         return "EdDSA";
     }
@@ -39,9 +58,116 @@ public class EdDSAPrivateKey implements EdDSAKey, PrivateKey {
         return "PKCS#8";
     }
 
+    /**
+     *  This follows the docs from
+     *  java.security.spec.PKCS8EncodedKeySpec
+     *  quote:
+     *<pre>
+     *  The PrivateKeyInfo syntax is defined in the PKCS#8 standard as follows:
+     *  PrivateKeyInfo ::= SEQUENCE {
+     *    version Version,
+     *    privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
+     *    privateKey PrivateKey,
+     *    attributes [0] IMPLICIT Attributes OPTIONAL }
+     *  Version ::= INTEGER
+     *  PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
+     *  PrivateKey ::= OCTET STRING
+     *  Attributes ::= SET OF Attribute
+     *</pre>
+     *
+     *<pre>
+     *  AlgorithmIdentifier ::= SEQUENCE
+     *  {
+     *    algorithm           OBJECT IDENTIFIER,
+     *    parameters          ANY OPTIONAL
+     *  }
+     *</pre>
+     *
+     *  Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
+     *
+     *  Note that the private key encoding is not fully specified in the Josefsson draft version 04,
+     *  and the example could be wrong, as it's lacking Version and AlgorithmIdentifier.
+     *  This will hopefully be clarified in the next draft.
+     *  But sun.security.pkcs.PKCS8Key expects them so we must include them for keytool to work.
+     *
+     *  @return 49 bytes for Ed25519, null for other curves
+     *  @since implemented in 0.9.25
+     */
     public byte[] getEncoded() {
-        // TODO Auto-generated method stub
-        return null;
+        if (!edDsaSpec.equals(EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)))
+            return null;
+        int totlen = 17 + seed.length;
+        byte[] rv = new byte[totlen];
+        int idx = 0;
+        // sequence
+        rv[idx++] = 0x30;
+        rv[idx++] = (byte) (15 + seed.length);
+
+        // version
+        // not in the Josefsson example
+        rv[idx++] = 0x02;
+        rv[idx++] = 1;
+        rv[idx++] = 0;
+
+        // Algorithm Identifier
+        // sequence
+        // not in the Josefsson example
+        rv[idx++] = 0x30;
+        rv[idx++] = 8;
+        // OID 1.3.101.100
+        // https://msdn.microsoft.com/en-us/library/windows/desktop/bb540809%28v=vs.85%29.aspx
+        // not in the Josefsson example
+        rv[idx++] = 0x06;
+        rv[idx++] = 3;
+        rv[idx++] = (1 * 40) + 3;
+        rv[idx++] = 101;
+        rv[idx++] = 100;
+        // params
+        rv[idx++] = 0x0a;
+        rv[idx++] = 1;
+        rv[idx++] = 1; // Ed25519
+        // the key
+        rv[idx++] = 0x04;  // octet string
+        rv[idx++] = (byte) seed.length;
+        System.arraycopy(seed, 0, rv, idx, seed.length);
+        return rv;
+    }
+
+    /**
+     *  This is really dumb for now.
+     *  See getEncoded().
+     *
+     *  @return 32 bytes for Ed25519, throws for other curves
+     *  @since 0.9.25
+     */
+    private static byte[] decode(byte[] d) throws InvalidKeySpecException {
+        try {
+            int idx = 0;
+            if (d[idx++] != 0x30 ||
+                d[idx++] != 47 ||
+                d[idx++] != 0x02 ||
+                d[idx++] != 1 ||
+                d[idx++] != 0 ||
+                d[idx++] != 0x30 ||
+                d[idx++] != 8 ||
+                d[idx++] != 0x06 ||
+                d[idx++] != 3 ||
+                d[idx++] != (1 * 40) + 3 ||
+                d[idx++] != 101 ||
+                d[idx++] != 100 ||
+                d[idx++] != 0x0a ||
+                d[idx++] != 1 ||
+                d[idx++] != 1 ||
+                d[idx++] != 0x04 ||
+                d[idx++] != 32) {
+                throw new InvalidKeySpecException("unsupported key spec");
+            }
+            byte[] rv = new byte[32];
+            System.arraycopy(d, idx, rv, 0, 32);
+            return rv;
+        } catch (IndexOutOfBoundsException ioobe) {
+            throw new InvalidKeySpecException(ioobe);
+        }
     }
 
     public EdDSAParameterSpec getParams() {
@@ -67,4 +193,26 @@ public class EdDSAPrivateKey implements EdDSAKey, PrivateKey {
     public byte[] getAbyte() {
         return Abyte;
     }
+
+    /**
+     *  @since 0.9.25
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(seed);
+    }
+
+    /**
+     *  @since 0.9.25
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (o == this)
+            return true;
+        if (!(o instanceof EdDSAPrivateKey))
+            return false;
+        EdDSAPrivateKey pk = (EdDSAPrivateKey) o;
+        return Arrays.equals(seed, pk.getSeed()) &&
+               edDsaSpec.equals(pk.getParams());
+    }
 }
diff --git a/core/java/src/net/i2p/crypto/eddsa/EdDSAPublicKey.java b/core/java/src/net/i2p/crypto/eddsa/EdDSAPublicKey.java
index 8e436bd1d8b305cd761fc6d4334748427e8efb3f..747d92b7be3356874bf5e11bb9ef9651ebceafc2 100644
--- a/core/java/src/net/i2p/crypto/eddsa/EdDSAPublicKey.java
+++ b/core/java/src/net/i2p/crypto/eddsa/EdDSAPublicKey.java
@@ -1,13 +1,23 @@
 package net.i2p.crypto.eddsa;
 
 import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
 
 import net.i2p.crypto.eddsa.math.GroupElement;
+import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
 import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
 import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
 
 /**
  * An EdDSA public key.
+ *<p>
+ * Warning: Public key encoding is is based on the
+ * current IETF draft, and is subject to change. See getEncoded().
+ *</p><p>
+ * Ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
+ *</p>
  *
  * @since 0.9.15
  * @author str4d
@@ -27,6 +37,14 @@ public class EdDSAPublicKey implements EdDSAKey, PublicKey {
         this.edDsaSpec = spec.getParams();
     }
 
+    /**
+     *  @since 0.9.25
+     */
+    public EdDSAPublicKey(X509EncodedKeySpec spec) throws InvalidKeySpecException {
+        this(new EdDSAPublicKeySpec(decode(spec.getEncoded()),
+                                    EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)));
+    }
+
     public String getAlgorithm() {
         return "EdDSA";
     }
@@ -35,9 +53,95 @@ public class EdDSAPublicKey implements EdDSAKey, PublicKey {
         return "X.509";
     }
 
+    /**
+     *  This follows the spec at
+     *  ref: https://tools.ietf.org/html/draft-josefsson-pkix-eddsa-04
+     *  which matches the docs from
+     *  java.security.spec.X509EncodedKeySpec
+     *  quote:
+     *<pre>
+     * The SubjectPublicKeyInfo syntax is defined in the X.509 standard as follows:
+     *  SubjectPublicKeyInfo ::= SEQUENCE {
+     *    algorithm AlgorithmIdentifier,
+     *    subjectPublicKey BIT STRING }
+     *</pre>
+     *
+     *<pre>
+     *  AlgorithmIdentifier ::= SEQUENCE
+     *  {
+     *    algorithm           OBJECT IDENTIFIER,
+     *    parameters          ANY OPTIONAL
+     *  }
+     *</pre>
+     *
+     *  @return 47 bytes for Ed25519, null for other curves
+     *  @since implemented in 0.9.25
+     */
     public byte[] getEncoded() {
-        // TODO Auto-generated method stub
-        return null;
+        if (!edDsaSpec.equals(EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)))
+            return null;
+        int totlen = 15 + Abyte.length;
+        byte[] rv = new byte[totlen];
+        int idx = 0;
+        // sequence
+        rv[idx++] = 0x30;
+        rv[idx++] = (byte) (13 + Abyte.length);
+        // Algorithm Identifier
+        // sequence
+        rv[idx++] = 0x30;
+        rv[idx++] = 8;
+        // OID 1.3.101.100
+        // https://msdn.microsoft.com/en-us/library/windows/desktop/bb540809%28v=vs.85%29.aspx
+        rv[idx++] = 0x06;
+        rv[idx++] = 3;
+        rv[idx++] = (1 * 40) + 3;
+        rv[idx++] = 101;
+        rv[idx++] = 100;
+        // params
+        rv[idx++] = 0x0a;
+        rv[idx++] = 1;
+        rv[idx++] = 1; // Ed25519
+        // the key
+        rv[idx++] = 0x03; // bit string
+        rv[idx++] = (byte) (1 + Abyte.length);
+        rv[idx++] = 0; // number of trailing unused bits
+        System.arraycopy(Abyte, 0, rv, idx, Abyte.length);
+        return rv;
+    }
+
+    /**
+     *  This is really dumb for now.
+     *  See getEncoded().
+     *
+     *  @return 32 bytes for Ed25519, throws for other curves
+     *  @since 0.9.25
+     */
+    private static byte[] decode(byte[] d) throws InvalidKeySpecException {
+        try {
+            int idx = 0;
+            if (d[idx++] != 0x30 ||
+                d[idx++] != 45 ||
+                d[idx++] != 0x30 ||
+                d[idx++] != 8 ||
+                d[idx++] != 0x06 ||
+                d[idx++] != 3 ||
+                d[idx++] != (1 * 40) + 3 ||
+                d[idx++] != 101 ||
+                d[idx++] != 100 ||
+                d[idx++] != 0x0a ||
+                d[idx++] != 1 ||
+                d[idx++] != 1 ||
+                d[idx++] != 0x03 ||
+                d[idx++] != 33 ||
+                d[idx++] != 0) {
+                throw new InvalidKeySpecException("unsupported key spec");
+            }
+            byte[] rv = new byte[32];
+            System.arraycopy(d, idx, rv, 0, 32);
+            return rv;
+        } catch (IndexOutOfBoundsException ioobe) {
+            throw new InvalidKeySpecException(ioobe);
+        }
     }
 
     public EdDSAParameterSpec getParams() {
@@ -55,4 +159,26 @@ public class EdDSAPublicKey implements EdDSAKey, PublicKey {
     public byte[] getAbyte() {
         return Abyte;
     }
+
+    /**
+     *  @since 0.9.25
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(Abyte);
+    }
+
+    /**
+     *  @since 0.9.25
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (o == this)
+            return true;
+        if (!(o instanceof EdDSAPublicKey))
+            return false;
+        EdDSAPublicKey pk = (EdDSAPublicKey) o;
+        return Arrays.equals(Abyte, pk.getAbyte()) &&
+               edDsaSpec.equals(pk.getParams());
+    }
 }
diff --git a/core/java/src/net/i2p/crypto/eddsa/KeyFactory.java b/core/java/src/net/i2p/crypto/eddsa/KeyFactory.java
index 950130ac987fd290c835117a281cb58a8656e04d..72fd03152ec098058722cfc2f88c9d73abb00eef 100644
--- a/core/java/src/net/i2p/crypto/eddsa/KeyFactory.java
+++ b/core/java/src/net/i2p/crypto/eddsa/KeyFactory.java
@@ -7,6 +7,8 @@ import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
 
 import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
 import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
@@ -16,22 +18,34 @@ import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
  * @author str4d
  *
  */
-public class KeyFactory extends KeyFactorySpi {
+public final class KeyFactory extends KeyFactorySpi {
 
+    /**
+     *  As of 0.9.25, supports PKCS8EncodedKeySpec
+     */
     protected PrivateKey engineGeneratePrivate(KeySpec keySpec)
             throws InvalidKeySpecException {
         if (keySpec instanceof EdDSAPrivateKeySpec) {
             return new EdDSAPrivateKey((EdDSAPrivateKeySpec) keySpec);
         }
-        throw new InvalidKeySpecException("key spec not recognised");
+        if (keySpec instanceof PKCS8EncodedKeySpec) {
+            return new EdDSAPrivateKey((PKCS8EncodedKeySpec) keySpec);
+        }
+        throw new InvalidKeySpecException("key spec not recognised: " + keySpec.getClass());
     }
 
+    /**
+     *  As of 0.9.25, supports X509EncodedKeySpec
+     */
     protected PublicKey engineGeneratePublic(KeySpec keySpec)
             throws InvalidKeySpecException {
         if (keySpec instanceof EdDSAPublicKeySpec) {
             return new EdDSAPublicKey((EdDSAPublicKeySpec) keySpec);
         }
-        throw new InvalidKeySpecException("key spec not recognised");
+        if (keySpec instanceof X509EncodedKeySpec) {
+            return new EdDSAPublicKey((X509EncodedKeySpec) keySpec);
+        }
+        throw new InvalidKeySpecException("key spec not recognised: " + keySpec.getClass());
     }
 
     @SuppressWarnings("unchecked")
diff --git a/core/java/src/net/i2p/crypto/eddsa/KeyPairGenerator.java b/core/java/src/net/i2p/crypto/eddsa/KeyPairGenerator.java
index 1de1f5b2448e505e4fcf7a8220518334955f8d97..6fd13d865ce643d10c13287468d9cc8ca0d8a3a1 100644
--- a/core/java/src/net/i2p/crypto/eddsa/KeyPairGenerator.java
+++ b/core/java/src/net/i2p/crypto/eddsa/KeyPairGenerator.java
@@ -21,7 +21,7 @@ import net.i2p.util.RandomSource;
  *
  *  @since 0.9.15
  */
-public class KeyPairGenerator extends KeyPairGeneratorSpi {
+public final class KeyPairGenerator extends KeyPairGeneratorSpi {
     private static final int DEFAULT_STRENGTH = 256;
     private EdDSAParameterSpec edParams;
     private SecureRandom random;
diff --git a/core/java/src/net/i2p/crypto/eddsa/math/Curve.java b/core/java/src/net/i2p/crypto/eddsa/math/Curve.java
index 2c6ade4ea17c3e67efce84a356fbd67435b77f44..fba8f29aa8e0c1fa81fba86ac5a17093d5cca30d 100644
--- a/core/java/src/net/i2p/crypto/eddsa/math/Curve.java
+++ b/core/java/src/net/i2p/crypto/eddsa/math/Curve.java
@@ -69,4 +69,29 @@ public class Curve implements Serializable {
             ge.precompute(true);
         return ge;
     }
+
+    /**
+     *  @since 0.9.25
+     */
+    @Override
+    public int hashCode() {
+        return f.hashCode() ^
+               d.hashCode() ^
+               I.hashCode();
+    }
+
+    /**
+     *  @since 0.9.25
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (o == this)
+            return true;
+        if (!(o instanceof Curve))
+            return false;
+        Curve c = (Curve) o;
+        return f.equals(c.getField()) &&
+               d.equals(c.getD()) &&
+               I.equals(c.getI());
+    }
 }
diff --git a/core/java/src/net/i2p/crypto/eddsa/math/FieldElement.java b/core/java/src/net/i2p/crypto/eddsa/math/FieldElement.java
index 4bea7c39e955f5e1f5c1c103be5420831775bbb3..b55d8705a5c8c3b720d2b71e4f47189914fb7656 100644
--- a/core/java/src/net/i2p/crypto/eddsa/math/FieldElement.java
+++ b/core/java/src/net/i2p/crypto/eddsa/math/FieldElement.java
@@ -3,6 +3,8 @@ package net.i2p.crypto.eddsa.math;
 import java.io.Serializable;
 
 /**
+ *
+ * Note: concrete subclasses must implement hashCode() and equals()
  *
  * @since 0.9.15
  *
@@ -60,4 +62,6 @@ public abstract class FieldElement implements Serializable {
     public abstract FieldElement invert();
 
     public abstract FieldElement pow22523();
+
+    // Note: concrete subclasses must implement hashCode() and equals()
 }
diff --git a/core/java/src/net/i2p/crypto/eddsa/math/GroupElement.java b/core/java/src/net/i2p/crypto/eddsa/math/GroupElement.java
index ec81b5d408449da9ef91de337f422fb1f234ef3f..3a6c57b8249ffaa324b58a90cf612e024ace075b 100644
--- a/core/java/src/net/i2p/crypto/eddsa/math/GroupElement.java
+++ b/core/java/src/net/i2p/crypto/eddsa/math/GroupElement.java
@@ -716,6 +716,8 @@ public class GroupElement implements Serializable {
 
     @Override
     public boolean equals(Object obj) {
+        if (obj == this)
+            return true;
         if (!(obj instanceof GroupElement))
             return false;
         GroupElement ge = (GroupElement) obj;
diff --git a/core/java/src/net/i2p/crypto/eddsa/math/bigint/BigIntegerLittleEndianEncoding.java b/core/java/src/net/i2p/crypto/eddsa/math/bigint/BigIntegerLittleEndianEncoding.java
index 06d3fd6a7c46cb61264f83e738b04f2cbe18b35d..e3088daca235dc9e7f684772d075fc0e0d85bbb1 100644
--- a/core/java/src/net/i2p/crypto/eddsa/math/bigint/BigIntegerLittleEndianEncoding.java
+++ b/core/java/src/net/i2p/crypto/eddsa/math/bigint/BigIntegerLittleEndianEncoding.java
@@ -15,7 +15,7 @@ public class BigIntegerLittleEndianEncoding extends Encoding implements Serializ
     private BigInteger mask;
 
     @Override
-    public void setField(Field f) {
+    public synchronized void setField(Field f) {
         super.setField(f);
         mask = BigInteger.ONE.shiftLeft(f.getb()-1).subtract(BigInteger.ONE);
     }
diff --git a/core/java/src/net/i2p/crypto/eddsa/spec/EdDSAParameterSpec.java b/core/java/src/net/i2p/crypto/eddsa/spec/EdDSAParameterSpec.java
index 4ace0fb62c699355c1d2712c3e0cabb39648c9b6..75b0140c6ac461a154f58113e5eeda8b29a75746 100644
--- a/core/java/src/net/i2p/crypto/eddsa/spec/EdDSAParameterSpec.java
+++ b/core/java/src/net/i2p/crypto/eddsa/spec/EdDSAParameterSpec.java
@@ -59,4 +59,29 @@ public class EdDSAParameterSpec implements AlgorithmParameterSpec, Serializable
     public GroupElement getB() {
         return B;
     }
+
+    /**
+     *  @since 0.9.25
+     */
+    @Override
+    public int hashCode() {
+        return hashAlgo.hashCode() ^
+               curve.hashCode() ^
+               B.hashCode();
+    }
+
+    /**
+     *  @since 0.9.25
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (o == this)
+            return true;
+        if (!(o instanceof EdDSAParameterSpec))
+            return false;
+        EdDSAParameterSpec s = (EdDSAParameterSpec) o;
+        return hashAlgo.equals(s.getHashAlgorithm()) &&
+               curve.equals(s.getCurve()) &&
+               B.equals(s.getB());
+    }
 }
diff --git a/core/java/src/net/i2p/crypto/elgamal/ElGamalKey.java b/core/java/src/net/i2p/crypto/elgamal/ElGamalKey.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d2520709ffe63a4248ce5af000a25628060b2e5
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/ElGamalKey.java
@@ -0,0 +1,11 @@
+package net.i2p.crypto.elgamal;
+
+import javax.crypto.interfaces.DHKey;
+
+import net.i2p.crypto.elgamal.spec.ElGamalParameterSpec;
+
+public interface ElGamalKey
+    extends DHKey
+{
+    public ElGamalParameterSpec getParameters();
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/ElGamalPrivateKey.java b/core/java/src/net/i2p/crypto/elgamal/ElGamalPrivateKey.java
new file mode 100644
index 0000000000000000000000000000000000000000..b7093a7dae464736fa605bcd6ce924a6a2ded763
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/ElGamalPrivateKey.java
@@ -0,0 +1,11 @@
+package net.i2p.crypto.elgamal;
+
+import java.math.BigInteger;
+
+import javax.crypto.interfaces.DHPrivateKey;
+
+public interface ElGamalPrivateKey
+    extends ElGamalKey, DHPrivateKey
+{
+    public BigInteger getX();
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/ElGamalPublicKey.java b/core/java/src/net/i2p/crypto/elgamal/ElGamalPublicKey.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc1b981459e9c743410f0843e082eaca2c9f97f6
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/ElGamalPublicKey.java
@@ -0,0 +1,11 @@
+package net.i2p.crypto.elgamal;
+
+import java.math.BigInteger;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+public interface ElGamalPublicKey
+    extends ElGamalKey, DHPublicKey
+{
+    public BigInteger getY();
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/ElGamalSigEngine.java b/core/java/src/net/i2p/crypto/elgamal/ElGamalSigEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..b44dc5fbe08f1c4a4000b03c505710d65f786217
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/ElGamalSigEngine.java
@@ -0,0 +1,164 @@
+package net.i2p.crypto.elgamal;
+
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.Arrays;
+
+import net.i2p.crypto.SHA256Generator;
+import net.i2p.crypto.SigUtil;
+import net.i2p.util.NativeBigInteger;
+import net.i2p.util.RandomSource;
+
+/**
+ * ElG signatures with SHA-256
+ *
+ * ref: https://en.wikipedia.org/wiki/ElGamal_signature_scheme
+ *
+ * @since 0.9.25
+ */
+public final class ElGamalSigEngine extends Signature {
+    private final MessageDigest digest;
+    private ElGamalKey key;
+
+    /**
+     * No specific hash requested, allows any ElGamal key.
+     */
+    public ElGamalSigEngine() {
+        this(SHA256Generator.getDigestInstance());
+    }
+
+    /**
+     * Specific hash requested, only matching keys will be allowed.
+     * @param digest the hash algorithm that keys must have to sign or verify.
+     */
+    public ElGamalSigEngine(MessageDigest digest) {
+        super("ElGamal");
+        this.digest = digest;
+    }
+
+    @Override
+    protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
+        digest.reset();
+        if (privateKey instanceof ElGamalPrivateKey) {
+            ElGamalPrivateKey privKey = (ElGamalPrivateKey) privateKey;
+            key = privKey;
+        } else {
+            throw new InvalidKeyException("cannot identify ElGamal private key: " + privateKey.getClass());
+        }
+    }
+
+    @Override
+    protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
+        digest.reset();
+        if (publicKey instanceof ElGamalPublicKey) {
+            key = (ElGamalPublicKey) publicKey;
+        } else {
+            throw new InvalidKeyException("cannot identify ElGamal public key: " + publicKey.getClass());
+        }
+    }
+
+    @Override
+    protected void engineUpdate(byte b) throws SignatureException {
+        digest.update(b);
+    }
+
+    @Override
+    protected void engineUpdate(byte[] b, int off, int len)
+            throws SignatureException {
+        digest.update(b, off, len);
+    }
+
+    /**
+     *  @return ASN.1 R,S
+     */
+    @Override
+    protected byte[] engineSign() throws SignatureException {
+        BigInteger elgp = key.getParams().getP();
+        BigInteger pm1 = elgp.subtract(BigInteger.ONE);
+        BigInteger elgg = key.getParams().getG();
+        BigInteger x = ((ElGamalPrivateKey) key).getX();
+        if (!(x instanceof NativeBigInteger))
+            x = new NativeBigInteger(x);
+        byte[] data = digest.digest();
+
+        BigInteger k;
+        boolean ok;
+        do {
+            k = new BigInteger(2048, RandomSource.getInstance());
+            ok = k.compareTo(pm1) == -1;
+            ok = ok && k.compareTo(BigInteger.ONE) == 1;
+            ok = ok && k.gcd(pm1).equals(BigInteger.ONE);
+        } while (!ok);
+
+        BigInteger r = elgg.modPow(k, elgp);
+        BigInteger kinv = k.modInverse(pm1);
+        BigInteger h = new NativeBigInteger(1, data);
+        BigInteger s = (kinv.multiply(h.subtract(x.multiply(r)))).mod(pm1);
+        // todo if s == 0 go around again
+
+        byte[] rv;
+        try {
+            rv = SigUtil.sigBytesToASN1(r, s);
+        } catch (IllegalArgumentException iae) {
+            throw new SignatureException("ASN1", iae);
+        }
+        return rv;
+    }
+
+    /**
+     *  @param sigBytes ASN.1 R,S
+     */
+    @Override
+    protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
+        BigInteger elgp = key.getParams().getP();
+        BigInteger pm1 = elgp.subtract(BigInteger.ONE);
+        BigInteger elgg = key.getParams().getG();
+        BigInteger y = ((ElGamalPublicKey) key).getY();
+        if (!(y instanceof NativeBigInteger))
+            y = new NativeBigInteger(y);
+        byte[] data = digest.digest();
+
+        try {
+            BigInteger[] rs = SigUtil.aSN1ToBigInteger(sigBytes, 256);
+            BigInteger r = rs[0];
+            BigInteger s = rs[1];
+            if (r.signum() != 1 || s.signum() != 1 ||
+                r.compareTo(elgp) != -1 || s.compareTo(pm1) != -1)
+                return false;
+            NativeBigInteger h = new NativeBigInteger(1, data);
+            BigInteger modvalr = r.modPow(s, elgp);
+            BigInteger modvaly = y.modPow(r, elgp);
+            BigInteger modmulval = modvalr.multiply(modvaly).mod(elgp);
+            BigInteger v = elgg.modPow(h, elgp);
+
+            boolean ok = v.compareTo(modmulval) == 0;
+            return ok;
+        } catch (RuntimeException e) {
+            throw new SignatureException("verify", e);
+        }
+    }
+
+    /**
+     * @deprecated replaced with <a href="#engineSetParameter(java.security.spec.AlgorithmParameterSpec)">
+     */
+    @Override
+    protected void engineSetParameter(String param, Object value) {
+        throw new UnsupportedOperationException("engineSetParameter unsupported");
+    }
+
+    /**
+     * @deprecated
+     */
+    @Override
+    protected Object engineGetParameter(String param) {
+        throw new UnsupportedOperationException("engineSetParameter unsupported");
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/KeyFactory.java b/core/java/src/net/i2p/crypto/elgamal/KeyFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c7809fb4dd3d2ba63fd8eea95a18007649067cb
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/KeyFactory.java
@@ -0,0 +1,80 @@
+package net.i2p.crypto.elgamal;
+
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyFactorySpi;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+
+import javax.crypto.spec.DHParameterSpec;
+
+import static net.i2p.crypto.CryptoConstants.I2P_ELGAMAL_2048_SPEC;
+import net.i2p.crypto.elgamal.impl.ElGamalPrivateKeyImpl;
+import net.i2p.crypto.elgamal.impl.ElGamalPrivateKeyImpl;
+import net.i2p.crypto.elgamal.impl.ElGamalPublicKeyImpl;
+import net.i2p.crypto.elgamal.spec.ElGamalParameterSpec;
+import net.i2p.crypto.elgamal.spec.ElGamalPrivateKeySpec;
+import net.i2p.crypto.elgamal.spec.ElGamalPublicKeySpec;
+
+/**
+ * Modified from eddsa
+ *
+ * @since 0.9.25
+ */
+public final class KeyFactory extends KeyFactorySpi {
+
+    /**
+     *  Supports PKCS8EncodedKeySpec
+     */
+    protected PrivateKey engineGeneratePrivate(KeySpec keySpec)
+            throws InvalidKeySpecException {
+        if (keySpec instanceof ElGamalPrivateKeySpec) {
+            return new ElGamalPrivateKeyImpl((ElGamalPrivateKeySpec) keySpec);
+        }
+        if (keySpec instanceof PKCS8EncodedKeySpec) {
+            return new ElGamalPrivateKeyImpl((PKCS8EncodedKeySpec) keySpec);
+        }
+        throw new InvalidKeySpecException("key spec not recognised");
+    }
+
+    /**
+     *  Supports X509EncodedKeySpec
+     */
+    protected PublicKey engineGeneratePublic(KeySpec keySpec)
+            throws InvalidKeySpecException {
+        if (keySpec instanceof ElGamalPublicKeySpec) {
+            return new ElGamalPublicKeyImpl((ElGamalPublicKeySpec) keySpec);
+        }
+        if (keySpec instanceof X509EncodedKeySpec) {
+            return new ElGamalPublicKeyImpl((X509EncodedKeySpec) keySpec);
+        }
+        throw new InvalidKeySpecException("key spec not recognised");
+    }
+
+    @SuppressWarnings("unchecked")
+    protected <T extends KeySpec> T engineGetKeySpec(Key key, Class<T> keySpec)
+            throws InvalidKeySpecException {
+        if (keySpec.isAssignableFrom(ElGamalPublicKeySpec.class) && key instanceof ElGamalPublicKey) {
+            ElGamalPublicKey k = (ElGamalPublicKey) key;
+            ElGamalParameterSpec egp = k.getParameters();
+            if (egp != null) {
+                return (T) new ElGamalPrivateKeySpec(k.getY(), egp);
+            }
+        } else if (keySpec.isAssignableFrom(ElGamalPrivateKeySpec.class) && key instanceof ElGamalPrivateKey) {
+            ElGamalPrivateKey k = (ElGamalPrivateKey) key;
+            ElGamalParameterSpec egp = k.getParameters();
+            if (egp != null) {
+                return (T) new ElGamalPrivateKeySpec(k.getX(), egp);
+            }
+        }
+        throw new InvalidKeySpecException("not implemented yet " + key + " " + keySpec);
+    }
+
+    protected Key engineTranslateKey(Key key) throws InvalidKeyException {
+        throw new InvalidKeyException("No other ElGamal key providers known");
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/KeyPairGenerator.java b/core/java/src/net/i2p/crypto/elgamal/KeyPairGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..0819c8a6be9f4415d67727d7f7a5f23105255334
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/KeyPairGenerator.java
@@ -0,0 +1,84 @@
+package net.i2p.crypto.elgamal;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidParameterException;
+import java.security.KeyPair;
+import java.security.KeyPairGeneratorSpi;
+import java.security.SecureRandom;
+import java.security.spec.AlgorithmParameterSpec;
+
+import static net.i2p.crypto.CryptoConstants.I2P_ELGAMAL_2048_SPEC;
+import net.i2p.crypto.KeyGenerator;
+import net.i2p.crypto.elgamal.impl.ElGamalPrivateKeyImpl;
+import net.i2p.crypto.elgamal.impl.ElGamalPublicKeyImpl;
+import net.i2p.crypto.elgamal.spec.ElGamalGenParameterSpec;
+import net.i2p.crypto.elgamal.spec.ElGamalParameterSpec;
+import net.i2p.crypto.elgamal.spec.ElGamalPrivateKeySpec;
+import net.i2p.crypto.elgamal.spec.ElGamalPublicKeySpec;
+import net.i2p.data.PrivateKey;
+import net.i2p.data.PublicKey;
+import net.i2p.data.SimpleDataStructure;
+import net.i2p.util.NativeBigInteger;
+import net.i2p.util.RandomSource;
+
+/**
+ * Modified from eddsa
+ * Only supported strength is 2048
+ *
+ * @since 0.9.25
+ */
+public final class KeyPairGenerator extends KeyPairGeneratorSpi {
+    // always long, don't use short key
+    private static final int DEFAULT_STRENGTH = 2048;
+    private ElGamalParameterSpec elgParams;
+    //private SecureRandom random;
+    private boolean initialized;
+
+    /**
+     *  @param strength must be 2048
+     *  @param random ignored
+     */
+    public void initialize(int strength, SecureRandom random) {
+        if (strength != DEFAULT_STRENGTH)
+            throw new InvalidParameterException("unknown key type.");
+        elgParams = I2P_ELGAMAL_2048_SPEC;
+        try {
+            initialize(elgParams, random);
+        } catch (InvalidAlgorithmParameterException e) {
+            throw new InvalidParameterException("key type not configurable.");
+        }
+    }
+
+    /**
+     *  @param random ignored
+     */
+    @Override
+    public void initialize(AlgorithmParameterSpec params, SecureRandom random) throws InvalidAlgorithmParameterException {
+        if (params instanceof ElGamalParameterSpec) {
+            elgParams = (ElGamalParameterSpec) params;
+            if (!elgParams.equals(I2P_ELGAMAL_2048_SPEC))
+                throw new InvalidAlgorithmParameterException("unsupported ElGamalParameterSpec");
+        } else if (params instanceof ElGamalGenParameterSpec) {
+            ElGamalGenParameterSpec elgGPS = (ElGamalGenParameterSpec) params;
+            if (elgGPS.getPrimeSize() != DEFAULT_STRENGTH)
+                throw new InvalidAlgorithmParameterException("unsupported prime size");
+            elgParams = I2P_ELGAMAL_2048_SPEC;
+        } else {
+            throw new InvalidAlgorithmParameterException("parameter object not a ElGamalParameterSpec");
+        }
+        //this.random = random;
+        initialized = true;
+    }
+
+    public KeyPair generateKeyPair() {
+        if (!initialized)
+            initialize(DEFAULT_STRENGTH, RandomSource.getInstance());
+        KeyGenerator kg = KeyGenerator.getInstance();
+        SimpleDataStructure[] keys = kg.generatePKIKeys();
+        PublicKey pubKey = (PublicKey) keys[0];
+        PrivateKey privKey = (PrivateKey) keys[1];
+        ElGamalPublicKey epubKey = new ElGamalPublicKeyImpl(new NativeBigInteger(1, pubKey.getData()), elgParams);
+        ElGamalPrivateKey eprivKey = new ElGamalPrivateKeyImpl(new NativeBigInteger(1, privKey.getData()), elgParams);
+        return new KeyPair(epubKey, eprivKey);
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/impl/ElGamalPrivateKeyImpl.java b/core/java/src/net/i2p/crypto/elgamal/impl/ElGamalPrivateKeyImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..0076c58e00791a737d0cf728ee6768a5ea9671f8
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/impl/ElGamalPrivateKeyImpl.java
@@ -0,0 +1,188 @@
+package net.i2p.crypto.elgamal.impl;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.math.BigInteger;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import javax.crypto.interfaces.DHPrivateKey;
+import javax.crypto.spec.DHParameterSpec;
+import javax.crypto.spec.DHPrivateKeySpec;
+
+import static net.i2p.crypto.SigUtil.intToASN1;
+import net.i2p.crypto.elgamal.ElGamalPrivateKey;
+import static net.i2p.crypto.elgamal.impl.ElGamalPublicKeyImpl.spaceFor;
+import net.i2p.crypto.elgamal.spec.ElGamalParameterSpec;
+import net.i2p.crypto.elgamal.spec.ElGamalPrivateKeySpec;
+
+public class ElGamalPrivateKeyImpl
+    implements ElGamalPrivateKey, DHPrivateKey
+{
+    private static final long serialVersionUID = 4819350091141529678L;
+        
+    private BigInteger x;
+    private ElGamalParameterSpec elSpec;
+
+    protected ElGamalPrivateKeyImpl()
+    {
+    }
+
+    public ElGamalPrivateKeyImpl(
+        ElGamalPrivateKey    key)
+    {
+        this.x = key.getX();
+        this.elSpec = key.getParameters();
+    }
+
+    public ElGamalPrivateKeyImpl(
+        DHPrivateKey    key)
+    {
+        this.x = key.getX();
+        this.elSpec = new ElGamalParameterSpec(key.getParams().getP(), key.getParams().getG());
+    }
+    
+    public ElGamalPrivateKeyImpl(
+        ElGamalPrivateKeySpec    spec)
+    {
+        this.x = spec.getX();
+        this.elSpec = new ElGamalParameterSpec(spec.getParams().getP(), spec.getParams().getG());
+    }
+
+    public ElGamalPrivateKeyImpl(
+        DHPrivateKeySpec    spec)
+    {
+        this.x = spec.getX();
+        this.elSpec = new ElGamalParameterSpec(spec.getP(), spec.getG());
+    }
+    
+    public ElGamalPrivateKeyImpl(
+        BigInteger x,
+        ElGamalParameterSpec elSpec)
+    {
+        this.x = x;
+        this.elSpec = elSpec;
+    }
+
+    public ElGamalPrivateKeyImpl(
+        PKCS8EncodedKeySpec spec)
+    {
+        throw new UnsupportedOperationException("todo");
+        //this.x = spec.getX();
+        //this.elSpec = new ElGamalParameterSpec(spec.getP(), spec.getG());
+    }
+    
+    public String getAlgorithm()
+    {
+        return "ElGamal";
+    }
+
+    /**
+     * return the encoding format we produce in getEncoded().
+     *
+     * @return the string "PKCS#8"
+     */
+    public String getFormat()
+    {
+        return "PKCS#8";
+    }
+
+    /**
+     * Return a PKCS8 representation of the key. The sequence returned
+     * represents a full PrivateKeyInfo object.
+     *
+     * @return a PKCS8 representation of the key.
+     */
+    public byte[] getEncoded()
+    {
+        byte[] pb = elSpec.getP().toByteArray();
+        byte[] gb = elSpec.getG().toByteArray();
+        byte[] xb = x.toByteArray();
+        int seq3len = spaceFor(pb.length) + spaceFor(gb.length);
+        int seq2len = 8 + spaceFor(seq3len);
+        int seq1len = 3 + spaceFor(seq2len) + spaceFor(xb.length);
+        int totlen = spaceFor(seq1len);
+        byte[] rv = new byte[totlen];
+        int idx = 0;
+        // sequence 1
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, seq1len);
+
+        // version
+        rv[idx++] = 0x02;
+        rv[idx++] = 1;
+        rv[idx++] = 0;
+
+        // Algorithm Identifier
+        // sequence 2
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, seq2len);
+        // OID: 1.3.14.7.2.1.1
+        rv[idx++] = 0x06;
+        rv[idx++] = 6;
+        rv[idx++] = (1 * 40) + 3;
+        rv[idx++] = 14;
+        rv[idx++] = 7;
+        rv[idx++] = 2;
+        rv[idx++] = 1;
+        rv[idx++] = 1;
+
+        // params
+        // sequence 3
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, seq3len);
+        // P
+        // integer
+        rv[idx++] = 0x02;
+        idx = intToASN1(rv, idx, pb.length);
+        System.arraycopy(pb, 0, rv, idx, pb.length);
+        idx += pb.length;
+        // G
+        // integer
+        rv[idx++] = 0x02;
+        idx = intToASN1(rv, idx, gb.length);
+        System.arraycopy(gb, 0, rv, idx, gb.length);
+        idx += gb.length;
+
+        // the key
+        // octet string
+        rv[idx++] = 0x04;
+        idx = intToASN1(rv, idx, xb.length);
+        // BC puts an integer in the bit string, we're not going to do that
+        System.arraycopy(xb, 0, rv, idx, xb.length);
+        return rv;
+    }
+
+    public ElGamalParameterSpec getParameters()
+    {
+        return elSpec;
+    }
+
+    public DHParameterSpec getParams()
+    {
+        return new DHParameterSpec(elSpec.getP(), elSpec.getG());
+    }
+    
+    public BigInteger getX()
+    {
+        return x;
+    }
+
+    private void readObject(
+        ObjectInputStream   in)
+        throws IOException, ClassNotFoundException
+    {
+        x = (BigInteger)in.readObject();
+
+        this.elSpec = new ElGamalParameterSpec((BigInteger)in.readObject(), (BigInteger)in.readObject());
+    }
+
+    private void writeObject(
+        ObjectOutputStream  out)
+        throws IOException
+    {
+        out.writeObject(this.getX());
+        out.writeObject(elSpec.getP());
+        out.writeObject(elSpec.getG());
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/impl/ElGamalPublicKeyImpl.java b/core/java/src/net/i2p/crypto/elgamal/impl/ElGamalPublicKeyImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..8df61ec50ebd40cd7a921b5076db6f027579b8f9
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/impl/ElGamalPublicKeyImpl.java
@@ -0,0 +1,182 @@
+package net.i2p.crypto.elgamal.impl;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.math.BigInteger;
+import java.security.spec.X509EncodedKeySpec;
+
+import javax.crypto.interfaces.DHPublicKey;
+import javax.crypto.spec.DHParameterSpec;
+import javax.crypto.spec.DHPublicKeySpec;
+
+import static net.i2p.crypto.SigUtil.intToASN1;
+import net.i2p.crypto.elgamal.ElGamalPublicKey;
+import net.i2p.crypto.elgamal.spec.ElGamalParameterSpec;
+import net.i2p.crypto.elgamal.spec.ElGamalPublicKeySpec;
+
+public class ElGamalPublicKeyImpl
+    implements ElGamalPublicKey, DHPublicKey
+{
+    private static final long serialVersionUID = 8712728417091216948L;
+        
+    private BigInteger              y;
+    private ElGamalParameterSpec    elSpec;
+
+    public ElGamalPublicKeyImpl(
+        ElGamalPublicKeySpec    spec)
+    {
+        this.y = spec.getY();
+        this.elSpec = new ElGamalParameterSpec(spec.getParams().getP(), spec.getParams().getG());
+    }
+
+    public ElGamalPublicKeyImpl(
+        DHPublicKeySpec    spec)
+    {
+        this.y = spec.getY();
+        this.elSpec = new ElGamalParameterSpec(spec.getP(), spec.getG());
+    }
+    
+    public ElGamalPublicKeyImpl(
+        ElGamalPublicKey    key)
+    {
+        this.y = key.getY();
+        this.elSpec = key.getParameters();
+    }
+
+    public ElGamalPublicKeyImpl(
+        DHPublicKey    key)
+    {
+        this.y = key.getY();
+        this.elSpec = new ElGamalParameterSpec(key.getParams().getP(), key.getParams().getG());
+    }
+    
+    public ElGamalPublicKeyImpl(
+        BigInteger              y,
+        ElGamalParameterSpec    elSpec)
+    {
+        this.y = y;
+        this.elSpec = elSpec;
+    }
+    
+    public ElGamalPublicKeyImpl(
+        X509EncodedKeySpec spec)
+    {
+        throw new UnsupportedOperationException("todo");
+        //this.y = y;
+        //this.elSpec = elSpec;
+    }
+
+    public String getAlgorithm()
+    {
+        return "ElGamal";
+    }
+
+    public String getFormat()
+    {
+        return "X.509";
+    }
+
+    public byte[] getEncoded()
+    {
+        byte[] pb = elSpec.getP().toByteArray();
+        byte[] gb = elSpec.getG().toByteArray();
+        byte[] yb = y.toByteArray();
+        int seq3len = spaceFor(pb.length) + spaceFor(gb.length);
+        int seq2len = 8 + spaceFor(seq3len);
+        int seq1len = spaceFor(seq2len) + spaceFor(yb.length + 1);
+        int totlen = spaceFor(seq1len);
+        byte[] rv = new byte[totlen];
+        int idx = 0;
+        // sequence 1
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, seq1len);
+
+        // Algorithm Identifier
+        // sequence 2
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, seq2len);
+        // OID: 1.3.14.7.2.1.1
+        rv[idx++] = 0x06;
+        rv[idx++] = 6;
+        rv[idx++] = (1 * 40) + 3;
+        rv[idx++] = 14;
+        rv[idx++] = 7;
+        rv[idx++] = 2;
+        rv[idx++] = 1;
+        rv[idx++] = 1;
+
+        // params
+        // sequence 3
+        rv[idx++] = 0x30;
+        idx = intToASN1(rv, idx, seq3len);
+        // P
+        // integer
+        rv[idx++] = 0x02;
+        idx = intToASN1(rv, idx, pb.length);
+        System.arraycopy(pb, 0, rv, idx, pb.length);
+        idx += pb.length;
+        // G
+        // integer
+        rv[idx++] = 0x02;
+        idx = intToASN1(rv, idx, gb.length);
+        System.arraycopy(gb, 0, rv, idx, gb.length);
+        idx += gb.length;
+
+        // the key
+        // bit string
+        rv[idx++] = 0x03;
+        idx = intToASN1(rv, idx, yb.length + 1);
+        rv[idx++] = 0; // number of trailing unused bits
+        // BC puts an integer in the bit string, we're not going to do that
+        System.arraycopy(yb, 0, rv, idx, yb.length);
+        return rv;
+    }
+
+    /**
+     *  @param val the length of the value, 65535 max
+     *  @return the length of the TLV
+     */
+    static int spaceFor(int val) {
+        int rv;
+        if (val > 255)
+            rv = 3;
+        else if (val > 127)
+            rv = 2;
+        else
+            rv = 1;
+        return 1 + rv + val;
+    }
+
+    public ElGamalParameterSpec getParameters()
+    {
+        return elSpec;
+    }
+    
+    public DHParameterSpec getParams()
+    {
+        return new DHParameterSpec(elSpec.getP(), elSpec.getG());
+    }
+
+    public BigInteger getY()
+    {
+        return y;
+    }
+
+    private void readObject(
+        ObjectInputStream   in)
+        throws IOException, ClassNotFoundException
+    {
+        this.y = (BigInteger)in.readObject();
+        this.elSpec = new ElGamalParameterSpec((BigInteger)in.readObject(), (BigInteger)in.readObject());
+    }
+
+    private void writeObject(
+        ObjectOutputStream  out)
+        throws IOException
+    {
+        out.writeObject(this.getY());
+        out.writeObject(elSpec.getP());
+        out.writeObject(elSpec.getG());
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/impl/package.html b/core/java/src/net/i2p/crypto/elgamal/impl/package.html
new file mode 100644
index 0000000000000000000000000000000000000000..b977d38db02980c5afcd9f84ac8d199eef5ef678
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/impl/package.html
@@ -0,0 +1,9 @@
+<html><body>
+<p>
+   Implementation of ElGamal keys, used for I2PProvider.
+   Modified from Bouncy Castle 1.53.
+   See net.i2p.crypto.elgamal for license info.
+</p><p>
+   Since 0.9.25.
+</p>
+</body></html>
diff --git a/core/java/src/net/i2p/crypto/elgamal/package.html b/core/java/src/net/i2p/crypto/elgamal/package.html
new file mode 100644
index 0000000000000000000000000000000000000000..f3fc75522d5d528e6301bece132eeb9d4e623866
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/package.html
@@ -0,0 +1,29 @@
+<html><body>
+<p>
+   Interfaces for ElGamal keys, used for I2PProvider.
+   Copied from Bouncy Castle 1.53.
+</p><p>
+   Since 0.9.25.
+</p><p><pre>
+
+
+Copyright (c) 2000 - 2013 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+and associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
+AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+</pre></p>
+</body></html>
diff --git a/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalGenParameterSpec.java b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalGenParameterSpec.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a404d09142e6123045cf0dc94810590581268cf
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalGenParameterSpec.java
@@ -0,0 +1,28 @@
+package net.i2p.crypto.elgamal.spec;
+
+import java.security.spec.AlgorithmParameterSpec;
+
+public class ElGamalGenParameterSpec
+    implements AlgorithmParameterSpec
+{
+    private final int primeSize;
+
+    /*
+     * @param primeSize the size (in bits) of the prime modulus.
+     */
+    public ElGamalGenParameterSpec(
+        int     primeSize)
+    {
+        this.primeSize = primeSize;
+    }
+
+    /**
+     * Returns the size in bits of the prime modulus.
+     *
+     * @return the size in bits of the prime modulus
+     */
+    public int getPrimeSize()
+    {
+        return primeSize;
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalKeySpec.java b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalKeySpec.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d20d32a4cf33c96e5ba1c5cd947cd8c35676c7f
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalKeySpec.java
@@ -0,0 +1,20 @@
+package net.i2p.crypto.elgamal.spec;
+
+import java.security.spec.KeySpec;
+
+public class ElGamalKeySpec
+    implements KeySpec
+{
+    private final ElGamalParameterSpec  spec;
+
+    public ElGamalKeySpec(
+        ElGamalParameterSpec  spec)
+    {
+        this.spec = spec;
+    }
+
+    public ElGamalParameterSpec getParams()
+    {
+        return spec;
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalParameterSpec.java b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalParameterSpec.java
new file mode 100644
index 0000000000000000000000000000000000000000..50284884a67260f82002409989a28734b4dca7e2
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalParameterSpec.java
@@ -0,0 +1,74 @@
+package net.i2p.crypto.elgamal.spec;
+
+import java.math.BigInteger;
+import java.security.spec.AlgorithmParameterSpec;
+
+/**
+ *  Copied from org.bouncycastle.jce.spec
+ *  This can't actually be passed to the BC provider, we would have to
+ *  use reflection to create a "real" org.bouncycasle.jce.spec.ElGamalParameterSpec.
+ *
+ *  @since 0.9.18, moved from net.i2p.crypto in 0.9.25
+ */
+public class ElGamalParameterSpec implements AlgorithmParameterSpec {
+    private final BigInteger p;
+    private final BigInteger g;
+
+    /**
+     * Constructs a parameter set for Diffie-Hellman, using a prime modulus
+     * <code>p</code> and a base generator <code>g</code>.
+     * 
+     * @param p the prime modulus
+     * @param g the base generator
+     */
+    public ElGamalParameterSpec(BigInteger p, BigInteger g) {
+        this.p = p;
+        this.g = g;
+    }
+
+    /**
+     * Returns the prime modulus <code>p</code>.
+     *
+     * @return the prime modulus <code>p</code>
+     */
+    public BigInteger getP() {
+        return p;
+    }
+
+    /**
+     * Returns the base generator <code>g</code>.
+     *
+     * @return the base generator <code>g</code>
+     */
+    public BigInteger getG() {
+        return g;
+    }
+
+    /**
+     * @since 0.9.25
+     */
+    @Override
+    public int hashCode() {
+        return p.hashCode() ^ g.hashCode();
+    }
+
+    /**
+     * @since 0.9.25
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null)
+            return false;
+        BigInteger op, og;
+        if (obj instanceof ElGamalParameterSpec) {
+            ElGamalParameterSpec egps = (ElGamalParameterSpec) obj;
+            op = egps.getP();
+            og = egps.getG();
+        //} else if (obj.getClass().getName().equals("org.bouncycastle.jce.spec.ElGamalParameterSpec")) {
+            //reflection... no...
+        } else {
+            return false;
+        }
+        return p.equals(op) && g.equals(og);
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalPrivateKeySpec.java b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalPrivateKeySpec.java
new file mode 100644
index 0000000000000000000000000000000000000000..014c57941fd47603f0fc0970473d7ce4906a4cea
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalPrivateKeySpec.java
@@ -0,0 +1,33 @@
+package net.i2p.crypto.elgamal.spec;
+
+import java.math.BigInteger;
+
+/**
+ * This class specifies an ElGamal private key with its associated parameters.
+ *
+ * @see ElGamalPublicKeySpec
+ */
+public class ElGamalPrivateKeySpec
+    extends ElGamalKeySpec
+{
+    private final BigInteger x;
+
+    public ElGamalPrivateKeySpec(
+        BigInteger              x,
+        ElGamalParameterSpec    spec)
+    {
+        super(spec);
+
+        this.x = x;
+    }
+
+    /**
+     * Returns the private value <code>x</code>.
+     *
+     * @return the private value <code>x</code>
+     */
+    public BigInteger getX()
+    {
+        return x;
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalPublicKeySpec.java b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalPublicKeySpec.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b68686e1bad92b3b887c7c4bb350c003b613a57
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/spec/ElGamalPublicKeySpec.java
@@ -0,0 +1,33 @@
+package net.i2p.crypto.elgamal.spec;
+
+import java.math.BigInteger;
+
+/**
+ * This class specifies an ElGamal public key with its associated parameters.
+ *
+ * @see ElGamalPrivateKeySpec
+ */
+public class ElGamalPublicKeySpec
+    extends ElGamalKeySpec
+{
+    private final BigInteger y;
+
+    public ElGamalPublicKeySpec(
+        BigInteger              y,
+        ElGamalParameterSpec    spec)
+    {
+        super(spec);
+
+        this.y = y;
+    }
+
+    /**
+     * Returns the public value <code>y</code>.
+     *
+     * @return the public value <code>y</code>
+     */
+    public BigInteger getY()
+    {
+        return y;
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/elgamal/spec/package.html b/core/java/src/net/i2p/crypto/elgamal/spec/package.html
new file mode 100644
index 0000000000000000000000000000000000000000..4c767ee4c8e382a3ce3fc244323bc86e391b793d
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/elgamal/spec/package.html
@@ -0,0 +1,9 @@
+<html><body>
+<p>
+   Specs ElGamal keys, used for I2PProvider.
+   Copied from Bouncy Castle 1.53.
+   See net.i2p.crypto.elgamal for license info.
+</p><p>
+   Since 0.9.25.
+</p>
+</body></html>
diff --git a/core/java/src/net/i2p/crypto/provider/I2PProvider.java b/core/java/src/net/i2p/crypto/provider/I2PProvider.java
index d24b80d369c752d7939dc3d6e42c7c3f2cefd422..b6b26d396d779b8a04d9414b798934b64b8ef539 100644
--- a/core/java/src/net/i2p/crypto/provider/I2PProvider.java
+++ b/core/java/src/net/i2p/crypto/provider/I2PProvider.java
@@ -3,6 +3,7 @@ package net.i2p.crypto.provider;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.security.Provider;
+import java.security.Security;
 
 /**
  *  @since 0.9.15
@@ -11,6 +12,7 @@ public final class I2PProvider extends Provider {
     public static final String PROVIDER_NAME = "I2P";
     private static final String INFO = "I2P Security Provider v0.1, implementing" +
             "several algorithms used by I2P.";
+    private static boolean _installed;
 
     /**
      * Construct a new provider.  This should only be required when
@@ -31,15 +33,89 @@ public final class I2PProvider extends Provider {
 
     private void setup() {
         // TODO: Implement SPIs for existing code
+        // However -
+        // quote
+        // http://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/HowToImplAProvider.html
+        //
+        // If your provider is supplying encryption algorithms through the
+        // Cipher, KeyAgreement, KeyGenerator, Mac, or SecretKeyFactory classes,
+        // you will need to sign your JAR file so that the JCA can authenticate the code at runtime.
+        // If you are NOT providing an implementation of this type you can skip this step.
+        //
         //put("Cipher.AES", "net.i2p.crypto.provider.CipherSpi$aesCBC");
         //put("Cipher.ElGamal", "net.i2p.crypto.provider.CipherSpi$elGamal");
         //put("Mac.HmacMD5-I2P", "net.i2p.crypto.provider.MacSpi");
+
         put("MessageDigest.SHA-1", "net.i2p.crypto.SHA1");
         //put("Signature.SHA1withDSA", "net.i2p.crypto.provider.SignatureSpi");
 
         // EdDSA
+        // Key OID: 1.3.101.100; Sig OID: 1.3.101.101
         put("KeyFactory.EdDSA", "net.i2p.crypto.eddsa.KeyFactory");
         put("KeyPairGenerator.EdDSA", "net.i2p.crypto.eddsa.KeyPairGenerator");
         put("Signature.SHA512withEdDSA", "net.i2p.crypto.eddsa.EdDSAEngine");
+        // Didn't find much documentation on these at all,
+        // see http://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/HowToImplAProvider.html
+        // section "Mapping from OID to name"
+        // without these, Certificate.verify() fails
+        put("Alg.Alias.KeyFactory.1.3.101.100", "EdDSA");
+        put("Alg.Alias.KeyFactory.OID.1.3.101.100", "EdDSA");
+        // Without these, keytool fails with:
+        // keytool error: java.security.NoSuchAlgorithmException: unrecognized algorithm name: SHA512withEdDSA
+        put("Alg.Alias.KeyPairGenerator.1.3.101.100", "EdDSA");
+        put("Alg.Alias.KeyPairGenerator.OID.1.3.101.100", "EdDSA");
+        // with this setting, keytool keygen doesn't work
+        // java.security.cert.CertificateException: Signature algorithm mismatch
+        // it must match the key setting (1.3.101.100) to work
+        // but this works fine with programmatic cert generation
+        put("Alg.Alias.Signature.1.3.101.101", "SHA512withEdDSA");
+        put("Alg.Alias.Signature.OID.1.3.101.101", "SHA512withEdDSA");
+        // TODO Ed25519ph
+        // OID: 1.3.101.101
+
+        // ElGamal
+        // OID: 1.3.14.7.2.1.1
+        put("KeyFactory.DH", "net.i2p.crypto.elgamal.KeyFactory");
+        put("KeyFactory.DiffieHellman", "net.i2p.crypto.elgamal.KeyFactory");
+        put("KeyFactory.ElGamal", "net.i2p.crypto.elgamal.KeyFactory");
+        put("KeyPairGenerator.DH", "net.i2p.crypto.elgamal.KeyPairGenerator");
+        put("KeyPairGenerator.DiffieHellman", "net.i2p.crypto.elgamal.KeyPairGenerator");
+        put("KeyPairGenerator.ElGamal", "net.i2p.crypto.elgamal.KeyPairGenerator");
+        put("Signature.SHA256withElGamal", "net.i2p.crypto.elgamal.ElGamalSigEngine");
+        put("Alg.Alias.KeyFactory.1.3.14.7.2.1.1", "ElGamal");
+        put("Alg.Alias.KeyFactory.OID.1.3.14.7.2.1.1", "ElGamal");
+        put("Alg.Alias.KeyPairGenerator.1.3.14.7.2.1.1", "ElGamal");
+        put("Alg.Alias.KeyPairGenerator.OID.1.3.14.7.2.1.1", "ElGamal");
+        put("Alg.Alias.Signature.1.3.14.7.2.1.1", "SHA256withElGamal");
+        put("Alg.Alias.Signature.OID.1.3.14.7.2.1.1", "SHA256withElGamal");
+    }
+
+    /**
+     *  Install the I2PProvider.
+     *  Harmless to call multiple times.
+     *  @since 0.9.25
+     */
+    public static void addProvider() {
+        synchronized(I2PProvider.class) {
+            if (!_installed) {
+                try {
+                    Provider us = new I2PProvider();
+                    // put ours ahead of BC, if installed, because our ElGamal
+                    // implementation may not be fully compatible with BC
+                    Provider[] provs = Security.getProviders();
+                    for (int i = 0; i < provs.length; i++) {
+                        if (provs[i].getName().equals("BC")) {
+                            Security.insertProviderAt(us, i);
+                            _installed = true;
+                            return;
+                        }
+                    }
+                    Security.addProvider(us);
+                    _installed = true;
+                } catch (SecurityException se) {
+                    System.out.println("WARN: Could not install I2P provider: " + se);
+                }
+            }
+        }
     }
 }
diff --git a/core/java/src/net/i2p/data/Base64.java b/core/java/src/net/i2p/data/Base64.java
index 3babe10ee11416ecb972ae13923645b729700aa1..210bf9efc2b7e6d6178699763bafa22712cb38a1 100644
--- a/core/java/src/net/i2p/data/Base64.java
+++ b/core/java/src/net/i2p/data/Base64.java
@@ -104,6 +104,17 @@ public class Base64 {
         return safeDecode(s, false);
     }
 
+    /**
+     *  Decodes data from Base64 notation using the I2P alphabet.
+     *
+     *  @param useStandardAlphabet Warning, must be false for I2P compatibility
+     *  @return the decoded data, null on error
+     *  @since 0.9.25
+     */
+    public static byte[] decode(String s, boolean useStandardAlphabet) {
+        return safeDecode(s, useStandardAlphabet);
+    }
+
     /** Maximum line length (76) of Base64 output. */
     private final static int MAX_LINE_LENGTH = 76;
 
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index a9689add73cfe4e78f0097175433b0152bc16b6c..dcd1ce71cf8ac648d5e12fa4af1f3cfbf2001da1 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -1324,8 +1324,9 @@ public class DataHelper {
      *
      * @param hash null OK
      * @return null on EOF
-     * @deprecated use MessageDigest version
+     * @deprecated use MessageDigest version to be removed in 0.9.27
      */
+    @Deprecated
     public static String readLine(InputStream in, Sha256Standalone hash) throws IOException {
         StringBuilder buf = new StringBuilder(128);
         boolean ok = readLine(in, buf, hash);
@@ -1380,7 +1381,7 @@ public class DataHelper {
      *
      * @return true if the line was read, false if eof was reached on an empty line
      *              (returns true for non-empty last line without a newline)
-     * @deprecated use StringBuilder / MessageDigest version
+     * @deprecated use StringBuilder / MessageDigest version, to be removed in 0.9.27
      */
     @Deprecated
     public static boolean readLine(InputStream in, StringBuffer buf, Sha256Standalone hash) throws IOException {
@@ -1420,8 +1421,9 @@ public class DataHelper {
      * @param hash null OK
      * @return true if the line was read, false if eof was reached on an empty line
      *              (returns true for non-empty last line without a newline)
-     * @deprecated use MessageDigest version
+     * @deprecated use MessageDigest version, to be removed in 0.9.27
      */
+    @Deprecated
     public static boolean readLine(InputStream in, StringBuilder buf, Sha256Standalone hash) throws IOException {
         int c = -1;
         int i = 0;
@@ -1463,8 +1465,9 @@ public class DataHelper {
     
     /**
      *  update the hash along the way
-     *  @deprecated use MessageDigest version
+     *  @deprecated use MessageDigest version, to be removed in 0.9.27
      */
+    @Deprecated
     public static void write(OutputStream out, byte data[], Sha256Standalone hash) throws IOException {
         hash.update(data);
         out.write(data);
diff --git a/core/java/src/net/i2p/util/RandomSource.java b/core/java/src/net/i2p/util/RandomSource.java
index 9c522ddd0b2d5a1431922cf08c1b2baf240042ec..b92295e633852983f489b8240a2fdabb15e1f75d 100644
--- a/core/java/src/net/i2p/util/RandomSource.java
+++ b/core/java/src/net/i2p/util/RandomSource.java
@@ -201,7 +201,8 @@ public class RandomSource extends SecureRandom implements EntropyHarvester {
         } catch (InterruptedException ie) {}
 
         // why urandom?  because /dev/random blocks
-        ok = seedFromFile(new File("/dev/urandom"), buf) || ok;
+        if (!SystemVersion.isWindows())
+            ok = seedFromFile(new File("/dev/urandom"), buf) || ok;
         // we merge (XOR) in the data from /dev/urandom with our own seedfile
         File localFile = new File(_context.getConfigDir(), SEEDFILE);
         ok = seedFromFile(localFile, buf) || ok;
diff --git a/installer/resources/checklist.md b/installer/resources/checklist.md
index 3a36f232af4e848846eb886eba62cf1d645419dd..e3b4e3b536e6f6b9158d9c1c679462de937222cb 100644
--- a/installer/resources/checklist.md
+++ b/installer/resources/checklist.md
@@ -51,6 +51,7 @@
     release.signer.su3=xxx@mail.i2p
     build.built-by=xxx
     javac.compilerargs=-bootclasspath /usr/lib/jvm/java-6-openjdk-amd64/jre/lib/rt.jar:/usr/lib/jvm/java-6-openjdk-amd64/jre/lib/jce.jar
+    javac.compilerargs7=-bootclasspath /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/jce.jar
     ```
 
 5. Copy latest trust list _MTN/monotonerc from website or some other workspace
@@ -169,7 +170,7 @@
   - In the i2p.newsxml branch, edit magnet links, release dates and release
     number in data/releases.json, and check in
 
-2. Add update torrents to tracker2.postman.i2p and start seeding (su2 and su3)
+2. Add i2pupdate-0.9.xx.su3 torrent to tracker2.postman.i2p and start seeding
 
 3. Notify the following people:
   - All in-network update hosts
diff --git a/installer/resources/eepsite/contexts/base-context.xml b/installer/resources/eepsite/contexts/base-context.xml
index ee995738b886e0eb51973b651f9f464c2444a5d0..99f788d3fe84c40d52b740c266a6c80272c1c6aa 100644
--- a/installer/resources/eepsite/contexts/base-context.xml
+++ b/installer/resources/eepsite/contexts/base-context.xml
@@ -37,5 +37,46 @@ to serve static html files and images.
     <Arg>org.eclipse.jetty.servlet.DefaultServlet</Arg>
     <Arg>/</Arg>
   </Call>
+  <Call name="addServlet">
+    <Arg>org.eclipse.jetty.servlet.DefaultServlet</Arg>
+    <Arg>/</Arg>
+  </Call>
+  <Call name="addFilter">
+    <!-- Add a filter to gzip on-the fly, since if we don't do it, I2P will.
+      -  This lowers the resource usage in the Java process on the client side,
+      -  by pushing the decompression out of Java and into the browser.
+      -  For all the reasons noted in the GzipFilter javadocs, this is normally
+      -  a bad idea for static content, but this is I2P.
+      -  See I2PTunnelHTTPServer for the I2P compressor.
+      -->
+    <Arg>
+      <New class="org.eclipse.jetty.servlet.FilterHolder" >
+        <Arg>
+          <New class="org.eclipse.jetty.servlets.GzipFilter" />
+        </Arg>
+        <Call name="setInitParameter">
+          <!-- minimum in Java is 1300 -->
+          <Arg>minGzipSize</Arg>
+          <Arg>512</Arg>
+        </Call>
+        <Call name="setInitParameter">
+          <!-- In Java we have a blacklist. This covers the most common cases. -->
+          <Arg>mimeTypes</Arg>
+          <Arg>application/pdf,application/x-javascript,application/xhtml+xml,application/xml,image/svg+xml,text/css,text/html,text/plain</Arg>
+        </Call>
+      </New>
+    </Arg>
+    <Arg>/*</Arg>
+    <Arg>
+      <!-- just guessing here -->
+      <Call class="java.util.EnumSet" name="of" >
+        <Arg>
+          <Call class="javax.servlet.DispatcherType" name="valueOf" >
+            <Arg>REQUEST</Arg>
+          </Call>
+        </Arg>
+      </Call>
+    </Arg>
+  </Call>
 </Configure>
 
diff --git a/installer/resources/eepsite/contexts/cgi-context.xml b/installer/resources/eepsite/contexts/cgi-context.xml
index 3ae8f390d12dc3758f70577a4185b2fd37475fd6..01df01123406a092fd08af2a55423b707cdeb182 100644
--- a/installer/resources/eepsite/contexts/cgi-context.xml
+++ b/installer/resources/eepsite/contexts/cgi-context.xml
@@ -33,4 +33,35 @@ Configure a custom context for the eepsite.
     <Arg>org.eclipse.jetty.servlets.CGI</Arg>
     <Arg>/</Arg>
   </Call>
+  <Call name="addFilter">
+    <!-- See base-context.xml for info.
+         Unlike for DefaultServlet, there's not even a theoretical
+         inefficiency for using this.
+      -->
+    <Arg>
+      <New class="org.eclipse.jetty.servlet.FilterHolder" >
+        <Arg>
+          <New class="org.eclipse.jetty.servlets.GzipFilter" />
+        </Arg>
+        <Call name="setInitParameter">
+          <Arg>minGzipSize</Arg>
+          <Arg>512</Arg>
+        </Call>
+        <Call name="setInitParameter">
+          <Arg>mimeTypes</Arg>
+          <Arg>application/pdf,application/x-javascript,application/xhtml+xml,application/xml,image/svg+xml,text/css,text/html,text/plain</Arg>
+        </Call>
+      </New>
+    </Arg>
+    <Arg>/*</Arg>
+    <Arg>
+      <Call class="java.util.EnumSet" name="of" >
+        <Arg>
+          <Call class="javax.servlet.DispatcherType" name="valueOf" >
+            <Arg>REQUEST</Arg>
+          </Call>
+        </Arg>
+      </Call>
+    </Arg>
+  </Call>
 </Configure>
diff --git a/installer/resources/hosts.txt b/installer/resources/hosts.txt
index 34987b08f82e4edbfc264d7e4053d565bf1ad4d3..ea67a2cdc0bfd54af8709f10e7e0b0c88649f232 100644
--- a/installer/resources/hosts.txt
+++ b/installer/resources/hosts.txt
@@ -364,3 +364,4 @@ i2pnews.i2p=XHS99uhrvijk3KxU438LjNf-SMXXiNXsbV8uwHFXdqsDsHPZRdc6LH-hEMGWDR5g2b65
 exchanged.i2p=rLFzsOqsfSmJZvg6xvl~ctUulYWwGaM8K~rGs-e4WXkKePTCMOpU8l6LIU-wwDOiUZ7Ve8Y-zWPBVYQjH8~~lgT-BJ81zjP5I6H051KOVaXDChdx5F99mZu0sEjnYoFX484QHsUkFc5GUypqhpv1iwWwdPL7bVNzr1fS6sIZvq7tYWEOymbnifxk2jC0BnjultNPCq1wiI2Y-G66iOHDvuLu5f7RvNGJYlpw0UYNv6g8jUu3gXYjDRMBD5OIxFUJaksfmml2CiaGjrPfXKEXBR4q1CogVruq3r~447VHb32f52aeYszcslNzQjYyFCdipnAi5JiNTFpzTZPMEglt2J3KZYB3SMCmxSLktFI7376c7mT7EbMIFFv1GrmcUy9oIyYasbb82Sec9y0nJ9ahZt0x3iGokAYekXKXq-rGPzgFeBwfuCHzQnLzm1akVyJHoGDdaG0QHJfqfW1sY3F2n1xaWrnKcqIz2ypemxVnTMFKQqm2pdG-dMsXNYiGmZtaBQAEAAcAAA==
 i2pwiki.i2p=Zr1YUKIKooxymZRqLvtfSYuZggqv5txkLE8Ks~FTh3A8JDmhOV8C82dUBKJhzst~Pcbqz7rXc~TPyrqasaQ~LioAji~WLSs8PpmCLVF83edhYpx75Fp23ELboEszHduhKWuibVchPLNa-6nP4F0Ttcr4krTlphe3KveNfZDAbm511zFFlNzPcY4PqOdCRBrp7INiWkVwQxwGWO7jiNYTYa5x4QXJmxIN1YOiNRYQST7THz1aV6219ThsfT9FE5JtiX-epli6PF5ZX9TcVSjHUKZnr8uJLXfh5T4RMVNe1n~KXutMUZwxpFE0scOIez9vhDFd7t0HPIsQUsv7MUBzrz4FM9qol7UUPueSGDRgTOOfXMfj4BDsouiWQC4GcSmH3SflR0Ith9QWKC4u3XYvB7Tw-70QWBEV53hUo6I8YKidV13WgeN9JI3KWTYkMyX-TYjmY9y2q6Xd-Maszv4Tb~NzxQs9CNdu0W-JRSUFOqzgt3l4cx0K1pmx4p0tM5dLBQAEAAEAAA==
 lenta.i2p=DnW8NqbKilYLcIx5g5CG4mWVHkzrCkl0MbV4a5rGJku4BSs7HjvzjZpCoXWFky9JCUlHzjFotMETxQBhaKl0Q46vu-plKQ4BLnYyo45p7j2lTiejWvV4SDuXU4IAdmug27i~Jl4N44zwe9KYy~gMfY1Vsgv4bH9ov7X7l2iS-bycfcd9nE7JfycwFc4e0XU-dx7xf~tHw7I5--25dp-SsRX3-UYz4ygb58aD8UsKfQaFZtMy4x~Z1ufNEftuekb1HH9g2Rhhq8Bl62ad8PWSDa9Ne-SkCQsqTYjrCsvMY2DMvWgmZxI1hJYqzjRdFV6JEprrr~CJgHGJXr~KdnZhX12Vm4bKisZK847wBm42CoBQBT5HRzDkeflkbsliirRuKSUxVYMoZ1vic~avPZZl~pvIKZsz-YtiKha4vjCNE1zD-tHIS~2qq4uEO546Ol9pNokPaNttV6r7D2-zurEDx~9grJ8LhBozTxtdTdfZv2OqN4bVhrE7xUrxe0flIFKEAAAA
+secure.thetinhat.i2p=0ncSrtVS20zwfcM7h2S6SSF56uVM2bftQwf40jsWKASNQnzyDVEzXpS04y-DJpm9EwKMGkgvx8ICBX-80W4E9xPJEdGFbb2u34fWmpTVMc3vwwB9ywmSXoxFbwiFx2sm7-HCcdALZwrjU3J41AfBvpEVkB5dXklTZIh~bU0JBTK2JIvQMD0XrSOztEruTc5kYymtkiCUpJaJJFXyIM3lKRcNlZ76UidE8AyQxHX7s9OR02pk7FhYV8Uh-Bs8loAZg6IPZzoYnnBYyi--b1-N8Ipv3aKmqSZPbQEzfQxU8-BE74xBLNEWAJtB8ptKMiKfHphO7qDKWqTzOU-7BtGXZAEOA3oblRAQcgqUbi~aICj0V0MAuYAdj7f-8BIi2k3Qfcl6k6XOFEpZqYFle71LeCjIZN~0mDDzxlr0Scx6LKMGnQAtYlGXFq99urp1MutPDZEu47mdxGWqc9CoNNNsE2UgS9ykvWygefNpZhkmceBXmDxWhuAPD1M2~eNF-fCMBQAIAAMAADZv~vU=
diff --git a/installer/resources/themes/console/images/thetinhat.png b/installer/resources/themes/console/images/thetinhat.png
new file mode 100644
index 0000000000000000000000000000000000000000..0c98f424b3f5f842b048ee6c9e6881382587f434
Binary files /dev/null and b/installer/resources/themes/console/images/thetinhat.png differ
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 410890547a2e68f2f140a0285bfbf3cc8f22107a..03183a9949342d78a3954575752f5f3140d9149e 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -100,7 +100,9 @@ public class Router implements RouterClock.ClockShiftListener {
     public final static long CLOCK_FUDGE_FACTOR = 1*60*1000; 
 
     /** used to differentiate routerInfo files on different networks */
-    public static final int NETWORK_ID = 2;
+    private static final int DEFAULT_NETWORK_ID = 2;
+    private static final String PROP_NETWORK_ID = "router.networkID";
+    private final int _networkID;
     
     /** coalesce stats this often - should be a little less than one minute, so the graphs get updated */
     public static final int COALESCE_TIME = 50*1000;
@@ -347,6 +349,14 @@ public class Router implements RouterClock.ClockShiftListener {
             _config.put("router.previousVersion", RouterVersion.VERSION);
             saveConfig();
         }
+        int id = DEFAULT_NETWORK_ID;
+        String sid = _config.get(PROP_NETWORK_ID);
+        if (sid != null) {
+            try {
+                id = Integer.parseInt(sid);
+            } catch (NumberFormatException nfe) {}
+        }
+        _networkID = id;
         changeState(State.INITIALIZED);
         // *********  Start no threads before here ********* //
     }
@@ -536,6 +546,14 @@ public class Router implements RouterClock.ClockShiftListener {
         if (_started <= 0) return 1000; // racing on startup
         return Math.max(1000, System.currentTimeMillis() - _started);
     }
+
+    /**
+     *  The network ID. Default 2.
+     *  May be changed with the config property router.networkID (restart required).
+     *  Change only if running a test network to prevent cross-network contamination.
+     *  @since 0.9.25
+     */
+    public int getNetworkID() { return _networkID; }
     
     /**
      *  Non-null, but take care when accessing context items before runRouter() is called
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 2698faa0cc8fabd8062b4ea31bbefc5c2435e077..e29d2bb43259c5188734adf2eaeb03ff3d438815 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -120,7 +120,8 @@ public class RouterContext extends I2PAppContext {
             // or about 2 seconds per buffer - so about 200x faster
             // to fill than to drain - so we don't need too many
             long maxMemory = SystemVersion.getMaxMemory();
-            long buffs = Math.min(16, Math.max(2, maxMemory / (14 * 1024 * 1024)));
+            long maxBuffs = (SystemVersion.isAndroid() || SystemVersion.isARM()) ? 4 : 8;
+            long buffs = Math.min(maxBuffs, Math.max(2, maxMemory / (21 * 1024 * 1024)));
             envProps.setProperty("prng.buffers", "" + buffs);
         }
         return envProps;
diff --git a/router/java/src/net/i2p/router/StatisticsManager.java b/router/java/src/net/i2p/router/StatisticsManager.java
index 21e0813cc36055166285e45f80a5811a2c46c571..9379df3504472df9a640d5837218394b46a34bde 100644
--- a/router/java/src/net/i2p/router/StatisticsManager.java
+++ b/router/java/src/net/i2p/router/StatisticsManager.java
@@ -32,6 +32,7 @@ import net.i2p.util.Log;
 public class StatisticsManager {
     private final Log _log;
     private final RouterContext _context;
+    private final String _networkID;
     
     public final static String PROP_PUBLISH_RANKINGS = "router.publishPeerRankings";
     private static final String PROP_CONTACT_NAME = "netdb.contact";
@@ -46,6 +47,7 @@ public class StatisticsManager {
         _fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK));
         _pct = new DecimalFormat("#0.00%", new DecimalFormatSymbols(Locale.UK));
         _log = context.logManager().getLog(StatisticsManager.class);
+        _networkID = Integer.toString(context.router().getNetworkID());
     }
         
     /**
@@ -72,7 +74,7 @@ public class StatisticsManager {
         // scheduled for removal, never used
         if (CoreVersion.VERSION.equals("0.9.23"))
             stats.setProperty("coreVersion", CoreVersion.VERSION);
-        stats.setProperty(RouterInfo.PROP_NETWORK_ID, Integer.toString(Router.NETWORK_ID));
+        stats.setProperty(RouterInfo.PROP_NETWORK_ID, _networkID);
         stats.setProperty(RouterInfo.PROP_CAPABILITIES, _context.router().getCapabilities());
 
         // No longer expose, to make build tracking more expensive
diff --git a/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java b/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java
index f69e62752d791f0b7dfc7b023579013a0a85ab78..39b81458a243744683d001dcabc7957ffdb684a0 100644
--- a/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java
+++ b/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java
@@ -47,12 +47,13 @@ public class FamilyKeyCrypto {
     private final SigningPrivateKey _privkey;
     private final SigningPublicKey _pubkey;
 
-    private static final String PROP_KEYSTORE_PASSWORD = "netdb.family.keystorePassword";
+    public static final String PROP_KEYSTORE_PASSWORD = "netdb.family.keystorePassword";
     public static final String PROP_FAMILY_NAME = "netdb.family.name";
-    private static final String PROP_KEY_PASSWORD = "netdb.family.keyPassword";
-    private static final String CERT_SUFFIX = ".crt";
-    private static final String KEYSTORE_PREFIX = "family-";
-    private static final String KEYSTORE_SUFFIX = ".ks";
+    public static final String PROP_KEY_PASSWORD = "netdb.family.keyPassword";
+    public static final String CERT_SUFFIX = ".crt";
+    public static final String KEYSTORE_PREFIX = "family-";
+    public static final String KEYSTORE_SUFFIX = ".ks";
+    public static final String CN_SUFFIX = ".family.i2p.net";
     private static final int DEFAULT_KEY_VALID_DAYS = 3652;  // 10 years
     // Note that we can't use RSA here, as the b64 sig would exceed the 255 char limit for a Mapping
     // Note that we can't use EdDSA here, as keystore doesn't know how, and encoding/decoding is unimplemented
@@ -289,7 +290,7 @@ public class FamilyKeyCrypto {
         // make a random 48 character password (30 * 8 / 5)
         String keyPassword = KeyStoreUtil.randomString();
         // and one for the cname
-        String cname = _fname + ".family.i2p.net";
+        String cname = _fname + CN_SUFFIX;
 
         boolean success = KeyStoreUtil.createKeys(ks, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD, _fname, cname, "family",
                                                   DEFAULT_KEY_VALID_DAYS, DEFAULT_KEY_ALGORITHM,
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
index 0df3b32902ded2216089a7aa2382dad0834439fa..5e66134f152b8e8783c3b1d2f483a79329d25eb0 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
@@ -479,7 +479,7 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
         // drop the peer in these cases
         // yikes don't do this - stack overflow //  getFloodfillPeers().size() == 0 ||
         // yikes2 don't do this either - deadlock! // getKnownRouters() < MIN_REMAINING_ROUTERS ||
-        if (info.getNetworkId() == Router.NETWORK_ID &&
+        if (info.getNetworkId() == _networkID &&
             (getKBucketSetSize() < MIN_REMAINING_ROUTERS ||
              _context.router().getUptime() < DONT_FAIL_PERIOD ||
              _context.commSystem().countActivePeers() <= MIN_ACTIVE_PEERS)) {
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
index ea0aa69084e49c44561d17dd09c84ecd8e9048ef..f1b13aefe8c5c7457f2619c3f370fa76e3207bff 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
@@ -70,6 +70,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
     private final ReseedChecker _reseedChecker;
     private volatile long _lastRIPublishTime;
     private NegativeLookupCache _negativeCache;
+    protected final int _networkID;
 
     /** 
      * Map of Hash to RepublishLeaseSetJob for leases we'realready managing.
@@ -156,6 +157,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
     public KademliaNetworkDatabaseFacade(RouterContext context) {
         _context = context;
         _log = _context.logManager().getLog(getClass());
+        _networkID = context.router().getNetworkID();
         _peerSelector = createPeerSelector();
         _publishingLeaseSets = new HashMap<Hash, RepublishLeaseSetJob>(8);
         _activeRequests = new HashMap<Hash, SearchJob>(8);
@@ -889,7 +891,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
                 _log.warn("Invalid routerInfo signature!  forged router structure!  router = " + routerInfo);
             return "Invalid routerInfo signature";
         }
-        if (routerInfo.getNetworkId() != Router.NETWORK_ID){
+        if (routerInfo.getNetworkId() != _networkID){
             _context.banlist().banlistRouter(key, "Not in our network");
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Bad network: " + routerInfo);
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
index 80aae33e2d294085f4f5a49a2e2dd01a854e0391..b1bbcf06540b35c48d56f417cc01e738af085548 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
@@ -54,6 +54,7 @@ public class PersistentDataStore extends TransientDataStore {
     private final ReadJob _readJob;
     private volatile boolean _initialized;
     private final boolean _flat;
+    private final int _networkID;
     
     private final static int READ_DELAY = 2*60*1000;
     private static final String PROP_FLAT = "router.networkDatabase.flat";
@@ -65,6 +66,7 @@ public class PersistentDataStore extends TransientDataStore {
      */
     public PersistentDataStore(RouterContext ctx, String dbDir, KademliaNetworkDatabaseFacade facade) throws IOException {
         super(ctx);
+        _networkID = ctx.router().getNetworkID();
         _flat = ctx.getBooleanProperty(PROP_FLAT);
         _dbDir = getDbDir(dbDir);
         _facade = facade;
@@ -505,7 +507,7 @@ public class PersistentDataStore extends TransientDataStore {
                     fis = new BufferedInputStream(fis);
                     RouterInfo ri = new RouterInfo();
                     ri.readBytes(fis, true);  // true = verify sig on read
-                    if (ri.getNetworkId() != Router.NETWORK_ID) {
+                    if (ri.getNetworkId() != _networkID) {
                         corrupt = true;
                         if (_log.shouldLog(Log.ERROR))
                             _log.error("The router "
diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
index 075eb04b982278a2719c2621d22a943fec1d609a..15699943d81444434c261e2764bc5f44177adcb1 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -42,6 +42,7 @@ class EstablishmentManager {
     private final Log _log;
     private final UDPTransport _transport;
     private final PacketBuilder _builder;
+    private final int _networkID;
 
     /** map of RemoteHostId to InboundEstablishState */
     private final ConcurrentHashMap<RemoteHostId, InboundEstablishState> _inboundStates;
@@ -140,6 +141,7 @@ class EstablishmentManager {
     public EstablishmentManager(RouterContext ctx, UDPTransport transport) {
         _context = ctx;
         _log = ctx.logManager().getLog(EstablishmentManager.class);
+        _networkID = ctx.router().getNetworkID();
         _transport = transport;
         _builder = new PacketBuilder(ctx, transport);
         _inboundStates = new ConcurrentHashMap<RemoteHostId, InboundEstablishState>();
@@ -249,7 +251,7 @@ class EstablishmentManager {
         }
         RouterIdentity toIdentity = toRouterInfo.getIdentity();
         Hash toHash = toIdentity.calculateHash();
-        if (toRouterInfo.getNetworkId() != Router.NETWORK_ID) {
+        if (toRouterInfo.getNetworkId() != _networkID) {
             _context.banlist().banlistRouter(toHash);
             _transport.markUnreachable(toHash);
             _transport.failed(msg, "Remote peer is on the wrong network, cannot establish");
@@ -762,7 +764,7 @@ class EstablishmentManager {
         if (_log.shouldLog(Log.INFO))
             _log.info("Completing to the peer after IB confirm: " + peer);
         DeliveryStatusMessage dsm = new DeliveryStatusMessage(_context);
-        dsm.setArrival(Router.NETWORK_ID); // overloaded, sure, but future versions can check this
+        dsm.setArrival(_networkID); // overloaded, sure, but future versions can check this
                                            // This causes huge values in the inNetPool.droppedDeliveryStatusDelay stat
                                            // so it needs to be caught in InNetMessagePool.
         dsm.setMessageExpiration(_context.clock().now() + DATA_MESSAGE_TIMEOUT);
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
index f0fa96e2b5eeff5cc301019964fbf18b98aff463..0202ee39fa8f72350d9e362d623aa92f471b0a51 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -87,6 +87,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     private int _mtu;
     private int _mtu_ipv6;
     private boolean _mismatchLogged;
+    private final int _networkID;
 
     /**
      *  Do we have a public IPv6 address?
@@ -218,6 +219,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
 
     public UDPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh) {
         super(ctx);
+        _networkID = ctx.router().getNetworkID();
         _dhFactory = dh;
         _log = ctx.logManager().getLog(UDPTransport.class);
         _peersByIdent = new ConcurrentHashMap<Hash, PeerState>(128);
@@ -1289,7 +1291,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             if (entry == null)
                 return;
             if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO &&
-                ((RouterInfo) entry).getNetworkId() != Router.NETWORK_ID) {
+                ((RouterInfo) entry).getNetworkId() != _networkID) {
                 // this is pre-0.6.1.10, so it isn't going to happen any more
 
                 /*
diff --git a/router/java/src/net/i2p/router/util/ArraySet.java b/router/java/src/net/i2p/router/util/ArraySet.java
new file mode 100644
index 0000000000000000000000000000000000000000..a471e0e5f339a55e99da3c4a0c0b1fa4666606fc
--- /dev/null
+++ b/router/java/src/net/i2p/router/util/ArraySet.java
@@ -0,0 +1,281 @@
+package net.i2p.router.util;
+
+import java.io.Serializable;
+import java.util.AbstractSet;
+import java.util.ConcurrentModificationException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ *  A small, fast Set with a maximum size, backed by a fixed-size array.
+ *  Unsynchronized, not thread-safe.
+ *  Null elements are not permitted.
+ *  Not appropriate for large Sets.
+ *
+ *  @since 0.9.25
+ */
+public class ArraySet<E> extends AbstractSet<E> implements Set<E> {
+    public static final int MAX_CAPACITY = 32;
+    private final Object[] _entries;
+    private final boolean _throwOnFull;
+    private int _size;
+    private int _overflowIndex;
+    private transient int modCount;
+
+    /**
+     *  A fixed capacity of MAX_CAPACITY.
+     *  Adds over capacity will throw a SetFullException.
+     */
+    public ArraySet() {
+        this(MAX_CAPACITY);
+    }
+
+    /**
+     *  A fixed capacity of MAX_CAPACITY.
+     *  Adds over capacity will throw a SetFullException.
+     *  @throws SetFullException if more than MAX_CAPACITY unique elements in c.
+     */
+    public ArraySet(Collection<? extends E> c) {
+        this();
+        addAll(c);
+    }
+
+    /**
+     *  Adds over capacity will throw a SetFullException.
+     *
+     *  @param capacity the maximum size
+     *  @throws IllegalArgumentException if capacity less than 1 or more than MAX_CAPACITY.
+     */
+    public ArraySet(int capacity) {
+        this(capacity, true);
+    }
+
+    /**
+     *  If throwOnFull is false,
+     *  adds over capacity will overwrite starting at slot zero.
+     *  This breaks the AbstractCollection invariant that
+     *  "a Collection will always contain the specified element after add() returns",
+     *  but it prevents unexpected exceptions.
+     *  If throwOnFull is true, adds over capacity will throw a SetFullException.
+     *
+     *  @param capacity the maximum size
+     *  @throws IllegalArgumentException if capacity less than 1 or more than MAX_CAPACITY.
+     */
+    public ArraySet(int capacity, boolean throwOnFull) {
+        if (capacity <= 0 || capacity > MAX_CAPACITY)
+            throw new IllegalArgumentException("bad capacity");
+        _entries = new Object[capacity];
+        _throwOnFull = throwOnFull;
+    }
+
+    /**
+     *  @return -1 if not found or if o is null
+     */
+    private int indexOf(Object o) {
+        if (o != null) {
+            for (int i = 0; i < _size; i++) {
+                if (o.equals(_entries[i]))
+                    return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     *  @throws SetFullException if throwOnFull was true in constructor
+     *  @throws NullPointerException if o is null
+     */
+    @Override
+    public boolean add(E o) {
+        if (o == null)
+            throw new NullPointerException();
+        int i = indexOf(o);
+        if (i >= 0) {
+            _entries[i] = o;
+            return false;
+        }
+        if (_size >= _entries.length) {
+            if (_throwOnFull)
+                throw new SetFullException();
+            i = _overflowIndex++;
+            if (i >= _entries.length) {
+                i = 0;
+                _overflowIndex = 0;
+            }
+        } else {
+            modCount++;
+            i = _size++;
+        }
+        _entries[i] = o;
+        return true;
+    }
+
+    @Override
+    public void clear() {
+        if (_size != 0) {
+            modCount++;
+            for (int i = 0; i < _size; i++) {
+                _entries[i] = null;
+            }
+            _size = 0;
+        }
+    }
+
+    @Override
+    public boolean contains(Object o) {
+        return indexOf(o) >= 0;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return _size <= 0;
+    }
+
+    @Override
+    public boolean remove(Object o) {
+        int i = indexOf(o);
+        if (i < 0)
+            return false;
+        modCount++;
+        _size--;
+        for (int j = i; j < _size; j++) {
+            _entries[j] = _entries[j + 1];
+        }
+        _entries[_size] = null;
+        return true;
+    }
+
+    public int size() {
+        return _size;
+    }
+
+    /**
+     *  Supports remove.
+     *  Supports comodification checks.
+     */
+    public Iterator<E> iterator() {
+        return new ASIterator();
+    }
+
+    public static class SetFullException extends IllegalStateException {
+        private static final long serialVersionUID = 9087390587254111L;
+    }
+
+    /**
+     * Modified from CachedIteratorArrayList
+     */
+    private class ASIterator implements Iterator<E>, Serializable {
+        /**
+         * Index of element to be returned by subsequent call to next.
+         */
+        int cursor = 0;
+
+        /**
+         * Index of element returned by most recent call to next or
+         * previous.  Reset to -1 if this element is deleted by a call
+         * to remove.
+         */
+        int lastRet = -1;
+
+        /**
+         * The modCount value that the iterator believes that the backing
+         * List should have.  If this expectation is violated, the iterator
+         * has detected concurrent modification.
+         */
+        int expectedModCount = modCount;
+        
+        public boolean hasNext() {
+            return cursor != _size;
+        }
+
+        @SuppressWarnings("unchecked")
+        public E next() {
+            checkForComodification();
+            try {
+                int i = cursor;
+                E next = (E) _entries[i];
+                lastRet = i;
+                cursor = i + 1;
+                return next;
+            } catch (IndexOutOfBoundsException e) {
+                checkForComodification();
+                throw new NoSuchElementException();
+            }
+        }
+
+        public void remove() {
+            if (lastRet < 0)
+                throw new IllegalStateException();
+            checkForComodification();
+
+            try {
+                ArraySet.this.remove(lastRet);
+                if (lastRet < cursor)
+                    cursor--;
+                lastRet = -1;
+                expectedModCount = modCount;
+            } catch (IndexOutOfBoundsException e) {
+                throw new ConcurrentModificationException();
+            }
+        }
+
+        final void checkForComodification() {
+            if (modCount != expectedModCount)
+                throw new ConcurrentModificationException();
+        }
+    }
+
+    /**
+     *  About 3x faster than HashSet.
+     */
+/****
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static void main(String[] args) {
+        if (args.length > 0) {
+            System.out.println("Test with overwrite");
+            Set s = new ArraySet(4, false);
+            for (int i = 0; i < args.length; i++) {
+                System.out.println("Added " + args[i] + "? " + s.add(args[i]));
+                System.out.println("Size is now " + s.size());
+            }
+            // toString tests the iterator
+            System.out.println("Set now contains" + s);
+            for (int i = 0; i < args.length; i++) {
+                System.out.println("Removed " + args[i] + "? " + s.remove(args[i]));
+                System.out.println("Size is now " + s.size());
+            }
+            System.out.println("\nTest with throw on full");
+            s = new ArraySet(4);
+            for (int i = 0; i < args.length; i++) {
+                System.out.println("Added " + args[i] + "? " + s.add(args[i]));
+                System.out.println("Size is now " + s.size());
+            }
+            // toString tests the iterator
+            System.out.println("Set now contains" + s);
+            for (int i = 0; i < args.length; i++) {
+                System.out.println("Removed " + args[i] + "? " + s.remove(args[i]));
+                System.out.println("Size is now " + s.size());
+            }
+        }
+
+        //java.util.List c = java.util.Arrays.asList(new String[] {"foo", "bar", "baz", "splat", "barf", "baz", "moose", "bear", "cat", "dog"} );
+        java.util.List c = java.util.Arrays.asList(new String[] {"foo", "bar"} );
+        long start = System.currentTimeMillis();
+        Set s = new java.util.HashSet(c);
+        int runs = 10000000;
+        for (int i = 0; i < runs; i++) {
+            s = new java.util.HashSet(s);
+        }
+        System.out.println("HashSet took " + (System.currentTimeMillis() - start));
+
+        start = System.currentTimeMillis();
+        s = new ArraySet(c);
+        for (int i = 0; i < runs; i++) {
+            s = new ArraySet(s);
+        }
+        System.out.println("ArraySet took " + (System.currentTimeMillis() - start));
+    }
+****/
+}