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,