i2ptunnel: New form for blinding info (WIP)

This commit is contained in:
zzz
2019-09-12 17:42:44 +00:00
parent e66d64d89b
commit 8d104f7fea
5 changed files with 176 additions and 23 deletions

View File

@ -1163,7 +1163,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER))) { Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER))) {
out.write(ERR_HELPER_DISABLED.getBytes("UTF-8")); out.write(ERR_HELPER_DISABLED.getBytes("UTF-8"));
} else { } else {
LocalHTTPServer.serveLocalFile(out, method, internalPath, internalRawQuery, _proxyNonce); LocalHTTPServer.serveLocalFile(_context, sockMgr, out, method, internalPath, internalRawQuery, _proxyNonce);
} }
} catch (IOException ioe) { } catch (IOException ioe) {
// ignore // ignore
@ -1267,26 +1267,13 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
if (code != LookupResult.RESULT_SUCCESS) { if (code != LookupResult.RESULT_SUCCESS) {
if (_log.shouldWarn()) if (_log.shouldWarn())
_log.warn("Unable to resolve b33 " + destination + " error code " + code); _log.warn("Unable to resolve b33 " + destination + " error code " + code);
// TODO new form to supply missing data
if (code != LookupResult.RESULT_FAILURE) { if (code != LookupResult.RESULT_FAILURE) {
String header = getErrorPage("b32", ERR_DESTINATION_UNKNOWN); // form to supply missing data
String msg; writeB32SaveForm(out, destination, code, targetRequest);
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) {}
return; return;
} }
// fall through to standard destination unreachable error page
} }
} }
} else { } else {
@ -1504,6 +1491,63 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
writeFooter(out); 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("<table id=\"proxyNewHost\">\n<tr><td align=\"right\">" + _t("Host") +
"</td><td>" + destination + "</td></tr>\n");
out.write("<tr><td align=\"right\">" + _t("Base 32") + "</td>" +
"<td><a href=\"http://" + destination + "/\">" + destination + "</a></td></tr>");
out.write("\n</table>\n" + "<hr>");
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("<p><b>" + msg + "</b></p>");
out.write("<form method=\"GET\" action=\"http://" + LOCAL_SERVER + "/b32\">\n" +
"<input type=\"hidden\" name=\"host\" value=\"" + destination + "\">\n" +
"<input type=\"hidden\" name=\"url\" value=\"" + targetRequest + "\">\n" +
"<input type=\"hidden\" name=\"code\" value=\"" + code + "\">\n" +
"<input type=\"hidden\" name=\"nonce\" value=\"" + _proxyNonce + "\">\n");
if (code == LookupResult.RESULT_KEY_REQUIRED || code == LookupResult.RESULT_SECRET_AND_KEY_REQUIRED) {
String label = _t("Generate");
out.write("<h4>" + _t("Encryption key") + "</h4>\n<p>" +
"<p>" + _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.") +
"</p>\n" +
"<input type=\"text\" size=\"55\" name=\"privkey\" value=\"\"></p>\n" +
"<p>" + _t("Generate new DH encryption key") +
"<button type=\"submit\" class=\"accept\" name=\"action\" value=\"newdh\">" + label + "</button>\n");
//"<p>" + _t("Generate new PSK encryption key") +
//"<button type=\"submit\" class=\"accept\" name=\"action\" value=\"newpsk\">" + label + "</button>\n");
}
if (code == LookupResult.RESULT_SECRET_REQUIRED || code == LookupResult.RESULT_SECRET_AND_KEY_REQUIRED) {
out.write("<h4>" + _t("Lookup password") + "</h4>\n<p>" +
"<input type=\"text\" size=\"55\" name=\"secret\" value=\"\"></p>\n");
}
// FIXME wasn't escaped
String label = _t("Save & continue").replace("&", "&amp;");
out.write("<div class=\"formaction\"><button type=\"submit\" class=\"accept\" name=\"action\" value=\"save\">" +
label + "</button></div>\n" +
"</form>\n</div>\n");
writeFooter(out);
}
/** /**
* Read the first line unbuffered. * Read the first line unbuffered.
* After that, switch to a BufferedReader, unless the method is "POST". * After that, switch to a BufferedReader, unless the method is "POST".

View File

@ -12,10 +12,21 @@ import java.util.Properties;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.client.naming.NamingService; 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.DataFormatException;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
import net.i2p.data.Destination; import net.i2p.data.Destination;
import net.i2p.data.PrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.i2ptunnel.I2PTunnelHTTPClient; import net.i2p.i2ptunnel.I2PTunnelHTTPClient;
import net.i2p.util.FileUtil; import net.i2p.util.FileUtil;
import net.i2p.util.PortMapper; import net.i2p.util.PortMapper;
@ -51,6 +62,14 @@ public abstract class LocalHTTPServer {
"\r\n"+ "\r\n"+
"Add to addressbook failed - bad parameters"; "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 = private final static String OK =
"HTTP/1.1 200 OK\r\n" + "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/plain\r\n" + "Content-Type: text/plain\r\n" +
@ -79,10 +98,12 @@ public abstract class LocalHTTPServer {
* uncaught vulnerabilities. * uncaught vulnerabilities.
* Restrict to the /themes/ directory for now. * Restrict to the /themes/ directory for now.
* *
* @param sockMgr only for /b32, otherwise ignored
* @param targetRequest decoded path only, non-null * @param targetRequest decoded path only, non-null
* @param query raw (encoded), may be 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 { String query, String proxyNonce) throws IOException {
//System.err.println("targetRequest: \"" + targetRequest + "\""); //System.err.println("targetRequest: \"" + targetRequest + "\"");
// a home page message for the curious... // a home page message for the curious...
@ -102,8 +123,8 @@ public abstract class LocalHTTPServer {
} }
// theme hack // theme hack
if (filename.startsWith("console/default/")) if (filename.startsWith("console/default/"))
filename = filename.replaceFirst("default", I2PAppContext.getGlobalContext().getProperty("routerconsole.theme", "light")); filename = filename.replaceFirst("default", context.getProperty("routerconsole.theme", "light"));
File themesDir = new File(I2PAppContext.getGlobalContext().getBaseDir(), "docs/themes"); File themesDir = new File(context.getBaseDir(), "docs/themes");
File file = new File(themesDir, filename); File file = new File(themesDir, filename);
if (file.exists() && !file.isDirectory()) { if (file.exists() && !file.isDirectory()) {
String type; String type;
@ -167,7 +188,7 @@ public abstract class LocalHTTPServer {
//System.err.println("book : \"" + book + "\""); //System.err.println("book : \"" + book + "\"");
//System.err.println("nonce : \"" + nonce + "\""); //System.err.println("nonce : \"" + nonce + "\"");
if (proxyNonce.equals(nonce) && url != null && host != null && dest != null) { if (proxyNonce.equals(nonce) && url != null && host != null && dest != null) {
NamingService ns = I2PAppContext.getGlobalContext().namingService(); NamingService ns = context.namingService();
Properties nsOptions = new Properties(); Properties nsOptions = new Properties();
nsOptions.setProperty("list", book); nsOptions.setProperty("list", book);
if (referer != null && referer.startsWith("http")) { if (referer != null && referer.startsWith("http")) {
@ -182,6 +203,88 @@ public abstract class LocalHTTPServer {
return; return;
} }
out.write(ERR_ADD.getBytes("UTF-8")); 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<String, String> opts = new HashMap<String, String>(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 { } else {
out.write(ERR_404.getBytes("UTF-8")); out.write(ERR_404.getBytes("UTF-8"));
} }

View File

@ -291,7 +291,7 @@ public class BlindData {
else else
buf.append("none"); buf.append("none");
if (_authKey != null) if (_authKey != null)
buf.append("\n\tAuth Key : ").append(_authKey); buf.append("\n\tAuth Key : ").append(_authKey);
else else
buf.append("\n\tAuth Required : ").append(_authRequired); buf.append("\n\tAuth Required : ").append(_authRequired);
if (_dest != null) if (_dest != null)

View File

@ -1,5 +1,11 @@
2019-09-12 zzz
* I2CP: BlindingInfo fixes
* i2ptunnel: New form for blinding info
2019-09-10 zzz 2019-09-10 zzz
* I2CP: New Blinding Info message (proposal 123) * 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 2019-09-08 zzz
* Transport: * Transport:

View File

@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */ /** deprecated */
public final static String ID = "Monotone"; public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION; public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 6; public final static long BUILD = 7;
/** for example "-test" */ /** for example "-test" */
public final static String EXTRA = ""; public final static String EXTRA = "";