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/jsp/configfamily.jsp b/apps/routerconsole/jsp/configfamily.jsp
new file mode 100644
index 0000000000000000000000000000000000000000..1646e8617315f5763314303d734828373d8172c6
--- /dev/null
+++ b/apps/routerconsole/jsp/configfamily.jsp
@@ -0,0 +1,87 @@
+<%@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("Select private 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("Create a family key certificate 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/exportfamily.jsp b/apps/routerconsole/jsp/exportfamily.jsp
new file mode 100644
index 0000000000000000000000000000000000000000..e271b600b52e147d5ca00858ce1c91cb59073e10
--- /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 + ".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/core/java/src/net/i2p/crypto/CertUtil.java b/core/java/src/net/i2p/crypto/CertUtil.java
index 0b5dfe66926c48c99564d59c999b0be67a4291a3..dac0160deb362db649c7ea09f13057cb451a559b 100644
--- a/core/java/src/net/i2p/crypto/CertUtil.java
+++ b/core/java/src/net/i2p/crypto/CertUtil.java
@@ -9,12 +9,19 @@ 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.X509Certificate;
+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 +31,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;
@@ -235,4 +243,88 @@ 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) {}
+        }
+    }
 }
diff --git a/core/java/src/net/i2p/crypto/KeyStoreUtil.java b/core/java/src/net/i2p/crypto/KeyStoreUtil.java
index 316b123091a770e06f8896e60552aa25034bc023..7cbb59a99d46c8032b87d7c2916414f2c3b7fc53 100644
--- a/core/java/src/net/i2p/crypto/KeyStoreUtil.java
+++ b/core/java/src/net/i2p/crypto/KeyStoreUtil.java
@@ -14,6 +14,7 @@ import java.security.cert.CertificateExpiredException;
 import java.security.cert.CertificateNotYetValidException;
 import java.security.cert.X509Certificate;
 import java.util.Enumeration;
+import java.util.List;
 import java.util.Locale;
 
 import net.i2p.I2PAppContext;
@@ -559,6 +560,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
      *
@@ -644,8 +742,16 @@ public class KeyStoreUtil {
      */
 /****
     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;
+            }
+            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 +780,22 @@ 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[2];
+        String pw = args[3];
+        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);
+    }
+
 ****/
 }
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/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,