forked from I2P_Developers/i2p.i2p
Crypto: New utils to support private key import/export
Console: New /configfamily, /exportfamily
This commit is contained in:
@ -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 + '.');
|
||||||
|
}
|
||||||
|
}
|
@ -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, "");
|
||||||
|
}
|
||||||
|
}
|
@ -19,13 +19,13 @@ public class ConfigNavHelper extends HelperBase {
|
|||||||
private static final String pages[] =
|
private static final String pages[] =
|
||||||
{"", "net", "ui", "sidebar", "home", "service", "update", "tunnels",
|
{"", "net", "ui", "sidebar", "home", "service", "update", "tunnels",
|
||||||
"clients", "peer", "keyring", "logging", "stats",
|
"clients", "peer", "keyring", "logging", "stats",
|
||||||
"reseed", "advanced" };
|
"reseed", "advanced", "family" };
|
||||||
|
|
||||||
private static final String titles[] =
|
private static final String titles[] =
|
||||||
{_x("Bandwidth"), _x("Network"), _x("UI"), _x("Summary Bar"), _x("Home Page"),
|
{_x("Bandwidth"), _x("Network"), _x("UI"), _x("Summary Bar"), _x("Home Page"),
|
||||||
_x("Service"), _x("Update"), _x("Tunnels"),
|
_x("Service"), _x("Update"), _x("Tunnels"),
|
||||||
_x("Clients"), _x("Peers"), _x("Keyring"), _x("Logging"), _x("Stats"),
|
_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 */
|
/** @since 0.9.19 */
|
||||||
private static class Tab {
|
private static class Tab {
|
||||||
|
87
apps/routerconsole/jsp/configfamily.jsp
Normal file
87
apps/routerconsole/jsp/configfamily.jsp
Normal file
@ -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>
|
35
apps/routerconsole/jsp/exportfamily.jsp
Normal file
35
apps/routerconsole/jsp/exportfamily.jsp
Normal file
@ -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
|
||||||
|
%>
|
@ -9,12 +9,19 @@ import java.io.OutputStreamWriter;
|
|||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.X509Certificate;
|
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 java.util.Locale;
|
||||||
|
|
||||||
import javax.naming.InvalidNameException;
|
import javax.naming.InvalidNameException;
|
||||||
@ -24,6 +31,7 @@ import javax.security.auth.x500.X500Principal;
|
|||||||
|
|
||||||
import net.i2p.I2PAppContext;
|
import net.i2p.I2PAppContext;
|
||||||
import net.i2p.data.Base64;
|
import net.i2p.data.Base64;
|
||||||
|
import net.i2p.data.DataHelper;
|
||||||
import net.i2p.util.Log;
|
import net.i2p.util.Log;
|
||||||
import net.i2p.util.SecureFileOutputStream;
|
import net.i2p.util.SecureFileOutputStream;
|
||||||
import net.i2p.util.SystemVersion;
|
import net.i2p.util.SystemVersion;
|
||||||
@ -235,4 +243,88 @@ public class CertUtil {
|
|||||||
try { if (fis != null) fis.close(); } catch (IOException foo) {}
|
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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import java.security.cert.CertificateExpiredException;
|
|||||||
import java.security.cert.CertificateNotYetValidException;
|
import java.security.cert.CertificateNotYetValidException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import net.i2p.I2PAppContext;
|
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
|
* Get a cert out of a keystore
|
||||||
*
|
*
|
||||||
@ -644,8 +742,16 @@ public class KeyStoreUtil {
|
|||||||
*/
|
*/
|
||||||
/****
|
/****
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
File ksf = (args.length > 0) ? new File(args[0]) : null;
|
|
||||||
try {
|
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()) {
|
if (ksf != null && !ksf.exists()) {
|
||||||
createKeyStore(ksf, DEFAULT_KEYSTORE_PASSWORD);
|
createKeyStore(ksf, DEFAULT_KEYSTORE_PASSWORD);
|
||||||
System.out.println("Created empty keystore " + ksf);
|
System.out.println("Created empty keystore " + ksf);
|
||||||
@ -674,5 +780,22 @@ public class KeyStoreUtil {
|
|||||||
e.printStackTrace();
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
****/
|
****/
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,17 @@ public class Base64 {
|
|||||||
return safeDecode(s, false);
|
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. */
|
/** Maximum line length (76) of Base64 output. */
|
||||||
private final static int MAX_LINE_LENGTH = 76;
|
private final static int MAX_LINE_LENGTH = 76;
|
||||||
|
|
||||||
|
@ -47,12 +47,13 @@ public class FamilyKeyCrypto {
|
|||||||
private final SigningPrivateKey _privkey;
|
private final SigningPrivateKey _privkey;
|
||||||
private final SigningPublicKey _pubkey;
|
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";
|
public static final String PROP_FAMILY_NAME = "netdb.family.name";
|
||||||
private static final String PROP_KEY_PASSWORD = "netdb.family.keyPassword";
|
public static final String PROP_KEY_PASSWORD = "netdb.family.keyPassword";
|
||||||
private static final String CERT_SUFFIX = ".crt";
|
public static final String CERT_SUFFIX = ".crt";
|
||||||
private static final String KEYSTORE_PREFIX = "family-";
|
public static final String KEYSTORE_PREFIX = "family-";
|
||||||
private static final String KEYSTORE_SUFFIX = ".ks";
|
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
|
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 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
|
// 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)
|
// make a random 48 character password (30 * 8 / 5)
|
||||||
String keyPassword = KeyStoreUtil.randomString();
|
String keyPassword = KeyStoreUtil.randomString();
|
||||||
// and one for the cname
|
// 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",
|
boolean success = KeyStoreUtil.createKeys(ks, KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD, _fname, cname, "family",
|
||||||
DEFAULT_KEY_VALID_DAYS, DEFAULT_KEY_ALGORITHM,
|
DEFAULT_KEY_VALID_DAYS, DEFAULT_KEY_ALGORITHM,
|
||||||
|
Reference in New Issue
Block a user