diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index c73589e60..63df4acce 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -1163,7 +1163,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER))) { out.write(ERR_HELPER_DISABLED.getBytes("UTF-8")); } else { - LocalHTTPServer.serveLocalFile(out, method, internalPath, internalRawQuery, _proxyNonce); + LocalHTTPServer.serveLocalFile(_context, sockMgr, out, method, internalPath, internalRawQuery, _proxyNonce); } } catch (IOException ioe) { // ignore @@ -1267,26 +1267,13 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn if (code != LookupResult.RESULT_SUCCESS) { if (_log.shouldWarn()) _log.warn("Unable to resolve b33 " + destination + " error code " + code); - // TODO new form to supply missing data if (code != LookupResult.RESULT_FAILURE) { - String header = getErrorPage("b32", ERR_DESTINATION_UNKNOWN); - String msg; - if (code == LookupResult.RESULT_SECRET_REQUIRED) - msg = "b32 address requires lookup password"; - else if (code == LookupResult.RESULT_KEY_REQUIRED) - msg = "b32 address requires encryption key"; - else if (code == LookupResult.RESULT_SECRET_AND_KEY_REQUIRED) - msg = "b32 address requires encryption key and lookup password"; - else if (code == LookupResult.RESULT_DECRYPTION_FAILURE) - msg = "b32 address decryption failure, check encryption key"; - else - msg = "lookup failure code " + code; - try { - writeErrorMessage(header, msg, out, targetRequest, false, destination); - } catch (IOException ioe) {} + // form to supply missing data + writeB32SaveForm(out, destination, code, targetRequest); return; } + // fall through to standard destination unreachable error page } } } else { @@ -1504,6 +1491,63 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn writeFooter(out); } + /** @since 0.9.43 */ + private void writeB32SaveForm(OutputStream outs, String destination, int code, + String targetRequest) throws IOException { + if(outs == null) + return; + Writer out = new BufferedWriter(new OutputStreamWriter(outs, "UTF-8")); + String header = getErrorPage("b32", ERR_DESTINATION_UNKNOWN); + out.write(header); + out.write("\n\n"); + out.write("" + + ""); + out.write("\n
" + _t("Host") + + "" + destination + "
" + _t("Base 32") + "" + destination + "
\n" + "
"); + String msg; + if (code == LookupResult.RESULT_SECRET_REQUIRED) + msg = _t("b32 address requires lookup password"); + else if (code == LookupResult.RESULT_KEY_REQUIRED) + msg = _t("b32 address requires encryption key"); + else if (code == LookupResult.RESULT_SECRET_AND_KEY_REQUIRED) + msg = _t("b32 address requires encryption key and lookup password"); + else if (code == LookupResult.RESULT_DECRYPTION_FAILURE) + msg = _t("b32 address decryption failure, check encryption key"); + else + msg = "lookup failure code " + code; + out.write("

" + msg + "

"); + out.write("
\n" + + "\n" + + "\n" + + "\n" + + "\n"); + + if (code == LookupResult.RESULT_KEY_REQUIRED || code == LookupResult.RESULT_SECRET_AND_KEY_REQUIRED) { + String label = _t("Generate"); + out.write("

" + _t("Encryption key") + "

\n

" + + "

" + _t("You must either enter a PSK encryption key provided by the server operator, or generate a DH encryption key and send that to the server operator.") + + ' ' + _t("Ask the server operator for help.") + + "

\n" + + + "

\n" + + "

" + _t("Generate new DH encryption key") + + "\n"); + //"

" + _t("Generate new PSK encryption key") + + //"\n"); + } + if (code == LookupResult.RESULT_SECRET_REQUIRED || code == LookupResult.RESULT_SECRET_AND_KEY_REQUIRED) { + out.write("

" + _t("Lookup password") + "

\n

" + + "

\n"); + } + + // FIXME wasn't escaped + String label = _t("Save & continue").replace("&", "&"); + out.write("
\n" + + "
\n\n"); + writeFooter(out); + } + /** * Read the first line unbuffered. * After that, switch to a BufferedReader, unless the method is "POST". diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java index c8146c97d..a123701a6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java @@ -12,10 +12,21 @@ import java.util.Properties; import java.util.StringTokenizer; import net.i2p.I2PAppContext; +import net.i2p.client.I2PSession; +import net.i2p.client.I2PSessionException; import net.i2p.client.naming.NamingService; +import net.i2p.client.streaming.I2PSocketManager; +import net.i2p.crypto.Blinding; +import net.i2p.crypto.EncType; +import net.i2p.crypto.KeyPair; +import net.i2p.crypto.SigType; +import net.i2p.data.Base64; +import net.i2p.data.BlindData; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; +import net.i2p.data.PrivateKey; +import net.i2p.data.SigningPublicKey; import net.i2p.i2ptunnel.I2PTunnelHTTPClient; import net.i2p.util.FileUtil; import net.i2p.util.PortMapper; @@ -51,6 +62,14 @@ public abstract class LocalHTTPServer { "\r\n"+ "Add to addressbook failed - bad parameters"; + private final static String ERR_B32 = + "HTTP/1.1 409 Bad\r\n"+ + "Content-Type: text/plain\r\n"+ + "Connection: close\r\n"+ + "Proxy-Connection: close\r\n"+ + "\r\n"+ + "B32 update failed - bad parameters"; + private final static String OK = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + @@ -79,10 +98,12 @@ public abstract class LocalHTTPServer { * uncaught vulnerabilities. * Restrict to the /themes/ directory for now. * + * @param sockMgr only for /b32, otherwise ignored * @param targetRequest decoded path only, non-null * @param query raw (encoded), may be null */ - public static void serveLocalFile(OutputStream out, String method, String targetRequest, + public static void serveLocalFile(I2PAppContext context, I2PSocketManager sockMgr, + OutputStream out, String method, String targetRequest, String query, String proxyNonce) throws IOException { //System.err.println("targetRequest: \"" + targetRequest + "\""); // a home page message for the curious... @@ -102,8 +123,8 @@ public abstract class LocalHTTPServer { } // theme hack if (filename.startsWith("console/default/")) - filename = filename.replaceFirst("default", I2PAppContext.getGlobalContext().getProperty("routerconsole.theme", "light")); - File themesDir = new File(I2PAppContext.getGlobalContext().getBaseDir(), "docs/themes"); + filename = filename.replaceFirst("default", context.getProperty("routerconsole.theme", "light")); + File themesDir = new File(context.getBaseDir(), "docs/themes"); File file = new File(themesDir, filename); if (file.exists() && !file.isDirectory()) { String type; @@ -167,7 +188,7 @@ public abstract class LocalHTTPServer { //System.err.println("book : \"" + book + "\""); //System.err.println("nonce : \"" + nonce + "\""); if (proxyNonce.equals(nonce) && url != null && host != null && dest != null) { - NamingService ns = I2PAppContext.getGlobalContext().namingService(); + NamingService ns = context.namingService(); Properties nsOptions = new Properties(); nsOptions.setProperty("list", book); if (referer != null && referer.startsWith("http")) { @@ -182,6 +203,88 @@ public abstract class LocalHTTPServer { return; } out.write(ERR_ADD.getBytes("UTF-8")); + + } else if (targetRequest.equals("/b32")) { + // Send a blinding info message (form submit) + // Parameters are url, host, dest, nonce, and master | router | private. + // Do the add and redirect. + if (query == null) { + out.write(ERR_ADD.getBytes("UTF-8")); + return; + } + Map opts = new HashMap(8); + // FIXME + // this only works if all keys are followed by =value + StringTokenizer tok = new StringTokenizer(query, "=&;"); + while (tok.hasMoreTokens()) { + String k = tok.nextToken(); + if (!tok.hasMoreTokens()) + break; + String v = tok.nextToken(); + opts.put(decode(k), decode(v)); + } + + String err = null; + String url = opts.get("url"); + String host = opts.get("host"); + String nonce = opts.get("nonce"); + String code = opts.get("code"); + String privkey = opts.get("privkey"); + String secret = opts.get("secret"); + String action = opts.get("action"); + if (proxyNonce.equals(nonce) && url != null && host != null && code != null) { + boolean success = true; + PrivateKey privateKey = null; + if (!code.equals("2") && !code.equals("4")) { + secret = null; + } else if (secret == null || secret.length() == 0) { + err = "Missing lookup password"; + success = false; + } + + int authType = BlindData.AUTH_NONE; + if (!code.equals("3") && !code.equals("4")) { + privkey = null; + } else if ("newdh".equals(action) || "newpsk".equals(action)) { + // newpsk probably not required + KeyPair kp = context.keyGenerator().generatePKIKeys(EncType.ECIES_X25519); + privateKey = kp.getPrivate(); + authType = action.equals("newdh") ? BlindData.AUTH_DH : BlindData.AUTH_PSK; + } else if (privkey == null || privkey.length() == 0) { + err = "Missing private key"; + success = false; + } else { + byte[] data = Base64.decode(privkey); + if (data == null || data.length != 32) { + err = "Bad private key"; + success = false; + } else { + privateKey = new PrivateKey(EncType.ECIES_X25519, data); + authType = BlindData.AUTH_PSK; + } + } + + if (success) { + try { + // get spk and blind type + BlindData bd = Blinding.decode(context, host); + SigningPublicKey spk = bd.getUnblindedPubKey(); + SigType bt = bd.getBlindedSigType(); + bd = new BlindData(context, spk, bt, secret, authType, privateKey); + I2PSession sess = sockMgr.getSession(); + sess.sendBlindingInfo(bd, 365*60*60*1000); + writeRedirectPage(out, success, host, "FIXME", url); + return; + } catch (IllegalArgumentException iae) { + err = iae.toString(); + } catch (I2PSessionException ise) { + err = ise.toString(); + } + } + } + out.write(ERR_B32.getBytes("UTF-8")); + if (err != null) + out.write(("\n\n" + err + "\n\nGo back and fix it").getBytes("UTF-8")); } else { out.write(ERR_404.getBytes("UTF-8")); } diff --git a/core/java/src/net/i2p/data/BlindData.java b/core/java/src/net/i2p/data/BlindData.java index 1959e670f..7ac2332b1 100644 --- a/core/java/src/net/i2p/data/BlindData.java +++ b/core/java/src/net/i2p/data/BlindData.java @@ -291,7 +291,7 @@ public class BlindData { else buf.append("none"); if (_authKey != null) - buf.append("\n\tAuth Key : ").append(_authKey); + buf.append("\n\tAuth Key : ").append(_authKey); else buf.append("\n\tAuth Required : ").append(_authRequired); if (_dest != null) diff --git a/history.txt b/history.txt index d76739f45..8a28a5783 100644 --- a/history.txt +++ b/history.txt @@ -1,5 +1,11 @@ +2019-09-12 zzz + * I2CP: BlindingInfo fixes + * i2ptunnel: New form for blinding info + 2019-09-10 zzz * I2CP: New Blinding Info message (proposal 123) + * i2ptunnel: New b32 error page + * Util: Fix AIOOBE on bad input to base 32 decode 2019-09-08 zzz * Transport: diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index c10128fe6..fecba78d6 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 6; + public final static long BUILD = 7; /** for example "-test" */ public final static String EXTRA = "";