I2CP: New Blinding Info message (proposal 123)

client- and router-side support
new session lookupDest2() method
return new b33 failure codes from lookup
show b33 alternates in tools
stub out support in HTTP client
This commit is contained in:
zzz
2019-09-10 12:37:11 +00:00
parent c99a42f0b1
commit 479461ab3b
15 changed files with 679 additions and 22 deletions

View File

@ -24,12 +24,15 @@ import net.i2p.app.ClientApp;
import net.i2p.app.ClientAppManager; import net.i2p.app.ClientAppManager;
import net.i2p.app.Outproxy; import net.i2p.app.Outproxy;
import net.i2p.client.I2PSession; import net.i2p.client.I2PSession;
import net.i2p.client.LookupResult;
import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketManager; import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.client.streaming.I2PSocketOptions; import net.i2p.client.streaming.I2PSocketOptions;
import net.i2p.crypto.Blinding;
import net.i2p.crypto.SHA256Generator; import net.i2p.crypto.SHA256Generator;
import net.i2p.data.Base32; import net.i2p.data.Base32;
import net.i2p.data.Base64; import net.i2p.data.Base64;
import net.i2p.data.BlindData;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
import net.i2p.data.Destination; import net.i2p.data.Destination;
import net.i2p.data.Hash; import net.i2p.data.Hash;
@ -1231,7 +1234,25 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
} else if (len >= 64) { } else if (len >= 64) {
if (_log.shouldInfo()) if (_log.shouldInfo())
_log.info("lookup b33 in-session " + destination); _log.info("lookup b33 in-session " + destination);
clientDest = sess.lookupDest(destination, 20*1000); try {
BlindData bd = Blinding.decode(_context, destination);
if (_log.shouldWarn())
_log.warn("Resolved b33 " + bd);
// TESTING
//sess.sendBlindingInfo(bd, 24*60*60*1000);
} catch (IllegalArgumentException iae) {
if (_log.shouldWarn())
_log.warn("Unable to resolve b33 " + destination, iae);
// TODO new error page
}
LookupResult lresult = sess.lookupDest2(destination, 20*1000);
clientDest = lresult.getDestination();
int code = lresult.getResultCode();
if (code != 0) {
if (_log.shouldWarn())
_log.warn("Unable to resolve b33 " + destination + " error code " + code);
// TODO new form
}
} else { } else {
// 61-63 chars, this won't work // 61-63 chars, this won't work
clientDest = _context.namingService().lookup(destination); clientDest = _context.namingService().lookup(destination);

View File

@ -14,6 +14,7 @@ import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import net.i2p.data.BlindData;
import net.i2p.data.Destination; import net.i2p.data.Destination;
import net.i2p.data.Hash; import net.i2p.data.Hash;
import net.i2p.data.PrivateKey; import net.i2p.data.PrivateKey;
@ -409,6 +410,17 @@ public interface I2PSession {
*/ */
public Destination lookupDest(String name, long maxWait) throws I2PSessionException; public Destination lookupDest(String name, long maxWait) throws I2PSessionException;
/**
* Ask the router to lookup a Destination by host name.
* Blocking. See above for details.
* Same as lookupDest() but with a failure code in the return value
*
* @param maxWait ms
* @since 0.9.43
* @return non-null
*/
public LookupResult lookupDest2(String name, long maxWait) throws I2PSessionException;
/** /**
* Pass updated options to the router. * Pass updated options to the router.
* Does not remove properties previously present but missing from this options parameter. * Does not remove properties previously present but missing from this options parameter.
@ -425,6 +437,13 @@ public interface I2PSession {
*/ */
public int[] bandwidthLimits() throws I2PSessionException; public int[] bandwidthLimits() throws I2PSessionException;
/**
*
* @param expiration ms from now, 0 means forever
* @since 0.9.43
*/
public void sendBlindingInfo(BlindData bd, int expiration) throws I2PSessionException;
/** /**
* Listen on specified protocol and port. * Listen on specified protocol and port.
* *

View File

@ -0,0 +1,53 @@
package net.i2p.client;
import net.i2p.data.Destination;
import net.i2p.data.i2cp.HostReplyMessage;
/**
* The return value of I2PSession.lookupDest2()
*
* @since 0.9.43
*/
public interface LookupResult {
/** getDestination() will be non-null */
public static final int RESULT_SUCCESS = HostReplyMessage.RESULT_SUCCESS;
/** general failure, probably a local hostname lookup failure, or a b32 lookup timeout */
public static final int RESULT_FAILURE = HostReplyMessage.RESULT_FAILURE;
/**
* b33 requires a lookup password but the router does not have it cached;
* please supply in a blinding info message
*/
public static final int RESULT_SECRET_REQUIRED = HostReplyMessage.RESULT_SECRET_REQUIRED;
/**
* b33 requires per-client auth private key but the router does not have it cached;
* please supply in a blinding info message
*/
public static final int RESULT_KEY_REQUIRED = HostReplyMessage.RESULT_KEY_REQUIRED;
/**
* b33 requires a lookup password and per-client auth private key but the router does not have them cached;
* please supply in a blinding info message
*/
public static final int RESULT_SECRET_AND_KEY_REQUIRED = HostReplyMessage.RESULT_SECRET_AND_KEY_REQUIRED;
/**
* b33 requires per-client auth private key, the router has a key, but decryption failed;
* please supply a new key in a blinding info message
*/
public static final int RESULT_DECRYPTION_FAILURE = HostReplyMessage.RESULT_DECRYPTION_FAILURE;
/**
* @return zero for success, nonzero for failure
*/
public int getResultCode();
/**
* @return Destination on success, null on failure
*/
public Destination getDestination();
}

View File

@ -32,7 +32,7 @@ class HostReplyMessageHandler extends HandlerImpl {
if (d != null) { if (d != null) {
session.destReceived(id, d); session.destReceived(id, d);
} else { } else {
session.destLookupFailed(id); session.destLookupFailed(id, msg.getResultCode());
} }
} }
} }

View File

@ -38,9 +38,11 @@ import net.i2p.client.I2PClient;
import net.i2p.client.I2PSession; import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException; import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PSessionListener; import net.i2p.client.I2PSessionListener;
import net.i2p.client.LookupResult;
import net.i2p.crypto.EncType; import net.i2p.crypto.EncType;
import net.i2p.crypto.SigType; import net.i2p.crypto.SigType;
import net.i2p.data.Base32; import net.i2p.data.Base32;
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;
@ -50,6 +52,7 @@ import net.i2p.data.PrivateKey;
import net.i2p.data.Signature; import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey; import net.i2p.data.SigningPublicKey;
import net.i2p.data.i2cp.BlindingInfoMessage;
import net.i2p.data.i2cp.DestLookupMessage; import net.i2p.data.i2cp.DestLookupMessage;
import net.i2p.data.i2cp.DestReplyMessage; import net.i2p.data.i2cp.DestReplyMessage;
import net.i2p.data.i2cp.GetBandwidthLimitsMessage; import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
@ -198,6 +201,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
private volatile boolean _routerSupportsFastReceive; private volatile boolean _routerSupportsFastReceive;
private volatile boolean _routerSupportsHostLookup; private volatile boolean _routerSupportsHostLookup;
private volatile boolean _routerSupportsLS2; private volatile boolean _routerSupportsLS2;
private volatile boolean _routerSupportsBlindingInfo;
protected static final int CACHE_MAX_SIZE = SystemVersion.isAndroid() ? 32 : 128; protected static final int CACHE_MAX_SIZE = SystemVersion.isAndroid() ? 32 : 128;
/** /**
@ -206,7 +210,8 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
*/ */
private static final Map<Object, Destination> _lookupCache = new LHMCache<Object, Destination>(CACHE_MAX_SIZE); private static final Map<Object, Destination> _lookupCache = new LHMCache<Object, Destination>(CACHE_MAX_SIZE);
private static final String MIN_HOST_LOOKUP_VERSION = "0.9.11"; private static final String MIN_HOST_LOOKUP_VERSION = "0.9.11";
private static final boolean TEST_LOOKUP = false; // todo change to false for release
private static final boolean TEST_BLINDINFO = true;
/** SSL interface (only) @since 0.8.3 */ /** SSL interface (only) @since 0.8.3 */
protected static final String PROP_ENABLE_SSL = "i2cp.SSL"; protected static final String PROP_ENABLE_SSL = "i2cp.SSL";
@ -225,6 +230,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
private static final String MIN_FAST_VERSION = "0.9.4"; private static final String MIN_FAST_VERSION = "0.9.4";
private static final String MIN_LS2_VERSION = "0.9.38"; private static final String MIN_LS2_VERSION = "0.9.38";
private static final String MIN_BLINDINFO_VERSION = "0.9.43";
/** @param routerVersion as rcvd in the SetDateMessage, may be null for very old routers */ /** @param routerVersion as rcvd in the SetDateMessage, may be null for very old routers */
void dateUpdated(String routerVersion) { void dateUpdated(String routerVersion) {
@ -233,7 +239,6 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
(routerVersion != null && routerVersion.length() > 0 && (routerVersion != null && routerVersion.length() > 0 &&
VersionComparator.comp(routerVersion, MIN_FAST_VERSION) >= 0); VersionComparator.comp(routerVersion, MIN_FAST_VERSION) >= 0);
_routerSupportsHostLookup = isrc || _routerSupportsHostLookup = isrc ||
TEST_LOOKUP ||
(routerVersion != null && routerVersion.length() > 0 && (routerVersion != null && routerVersion.length() > 0 &&
VersionComparator.comp(routerVersion, MIN_HOST_LOOKUP_VERSION) >= 0); VersionComparator.comp(routerVersion, MIN_HOST_LOOKUP_VERSION) >= 0);
_routerSupportsSubsessions = isrc || _routerSupportsSubsessions = isrc ||
@ -242,6 +247,10 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
_routerSupportsLS2 = isrc || _routerSupportsLS2 = isrc ||
(routerVersion != null && routerVersion.length() > 0 && (routerVersion != null && routerVersion.length() > 0 &&
VersionComparator.comp(routerVersion, MIN_LS2_VERSION) >= 0); VersionComparator.comp(routerVersion, MIN_LS2_VERSION) >= 0);
_routerSupportsBlindingInfo = isrc ||
TEST_BLINDINFO ||
(routerVersion != null && routerVersion.length() > 0 &&
VersionComparator.comp(routerVersion, MIN_BLINDINFO_VERSION) >= 0);
synchronized (_stateLock) { synchronized (_stateLock) {
if (_state == State.OPENING) { if (_state == State.OPENING) {
changeState(State.GOTDATE); changeState(State.GOTDATE);
@ -252,6 +261,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
public static final int LISTEN_PORT = I2PClient.DEFAULT_LISTEN_PORT; public static final int LISTEN_PORT = I2PClient.DEFAULT_LISTEN_PORT;
private static final int BUF_SIZE = 32*1024; private static final int BUF_SIZE = 32*1024;
private static final SessionId DUMMY_SESSION = new SessionId(65535);
/** /**
* for extension by SimpleSession (no dest) * for extension by SimpleSession (no dest)
@ -1500,6 +1510,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
if (h.equals(w.hash)) { if (h.equals(w.hash)) {
synchronized (w) { synchronized (w) {
w.destination = d; w.destination = d;
w.code = LookupResult.RESULT_SUCCESS;
w.notifyAll(); w.notifyAll();
} }
} }
@ -1515,6 +1526,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
for (LookupWaiter w : _pendingLookups) { for (LookupWaiter w : _pendingLookups) {
if (h.equals(w.hash)) { if (h.equals(w.hash)) {
synchronized (w) { synchronized (w) {
w.code = LookupResult.RESULT_FAILURE;
w.notifyAll(); w.notifyAll();
} }
} }
@ -1539,6 +1551,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
} }
synchronized (w) { synchronized (w) {
w.destination = d; w.destination = d;
w.code = LookupResult.RESULT_SUCCESS;
w.notifyAll(); w.notifyAll();
} }
} }
@ -1550,10 +1563,11 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
* on reception of HostReplyMessage * on reception of HostReplyMessage
* @since 0.9.11 * @since 0.9.11
*/ */
void destLookupFailed(long nonce) { void destLookupFailed(long nonce, int code) {
for (LookupWaiter w : _pendingLookups) { for (LookupWaiter w : _pendingLookups) {
if (nonce == w.nonce) { if (nonce == w.nonce) {
synchronized (w) { synchronized (w) {
w.code = code;
w.notifyAll(); w.notifyAll();
} }
} }
@ -1581,6 +1595,11 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
public final long nonce; public final long nonce;
/** the reply; synch on this */ /** the reply; synch on this */
public Destination destination; public Destination destination;
/**
* the return code; sync on this
* @since 0.9.43
*/
public int code;
public LookupWaiter(Hash h) { public LookupWaiter(Hash h) {
this(h, -1); this(h, -1);
@ -1599,6 +1618,16 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
this.name = name; this.name = name;
this.nonce = nonce; this.nonce = nonce;
} }
/** Dummy, completed
* @since 0.9.43
*/
public LookupWaiter(Destination d) {
hash = null;
name = null;
nonce = 0;
destination = d;
}
} }
/** @since 0.9.20 */ /** @since 0.9.20 */
@ -1657,7 +1686,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
_log.info("Sending HostLookup for " + h); _log.info("Sending HostLookup for " + h);
SessionId id = _sessionId; SessionId id = _sessionId;
if (id == null) if (id == null)
id = new SessionId(65535); id = DUMMY_SESSION;
sendMessage_unchecked(new HostLookupMessage(id, h, nonce, maxWait)); sendMessage_unchecked(new HostLookupMessage(id, h, nonce, maxWait));
} else { } else {
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
@ -1708,12 +1737,50 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
* @return null on failure * @return null on failure
*/ */
public Destination lookupDest(String name, long maxWait) throws I2PSessionException { public Destination lookupDest(String name, long maxWait) throws I2PSessionException {
LookupWaiter waiter = x_lookupDest(name, maxWait);
if (waiter == null)
return null;
synchronized(waiter) {
return waiter.destination;
}
}
/**
* Ask the router to lookup a Destination by host name.
* Blocking. See above for details.
* Same as lookupDest() but with a failure code in the return value
*
* @param maxWait ms
* @since 0.9.43
* @return non-null
*/
public LookupResult lookupDest2(String name, long maxWait) throws I2PSessionException {
LookupWaiter waiter = x_lookupDest(name, maxWait);
if (waiter == null)
return new LkupResult(LookupResult.RESULT_FAILURE, null);
synchronized(waiter) {
int code = waiter.code;
Destination d = waiter.destination;
if (d == null && code == LookupResult.RESULT_SUCCESS)
code = LookupResult.RESULT_FAILURE;
return new LkupResult(code, d);
}
}
/**
* Ask the router to lookup a Destination by host name.
* Blocking. See above for details.
* @param maxWait ms
* @since 0.9.11
* @return null on failure
*/
private LookupWaiter x_lookupDest(String name, long maxWait) throws I2PSessionException {
if (name.length() == 0) if (name.length() == 0)
return null; return null;
// Shortcut for b64 // Shortcut for b64
if (name.length() >= 516) { if (name.length() >= 516) {
try { try {
return new Destination(name); return new LookupWaiter(new Destination(name));
} catch (DataFormatException dfe) { } catch (DataFormatException dfe) {
return null; return null;
} }
@ -1724,7 +1791,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
synchronized (_lookupCache) { synchronized (_lookupCache) {
Destination rv = _lookupCache.get(name); Destination rv = _lookupCache.get(name);
if (rv != null) if (rv != null)
return rv; return new LookupWaiter(rv);
} }
if (isClosed()) { if (isClosed()) {
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
@ -1734,7 +1801,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
if (!_routerSupportsHostLookup) { if (!_routerSupportsHostLookup) {
// do them a favor and convert to Hash lookup // do them a favor and convert to Hash lookup
if (name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p")) if (name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p"))
return lookupDest(Hash.create(Base32.decode(name.toLowerCase(Locale.US).substring(0, 52))), maxWait); return new LookupWaiter(lookupDest(Hash.create(Base32.decode(name.toLowerCase(Locale.US).substring(0, 52))), maxWait));
// else unsupported // else unsupported
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("Router does not support HostLookup for " + name); _log.warn("Router does not support HostLookup for " + name);
@ -1743,18 +1810,17 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
int nonce = _lookupID.incrementAndGet() & 0x7fffffff; int nonce = _lookupID.incrementAndGet() & 0x7fffffff;
LookupWaiter waiter = new LookupWaiter(name, nonce); LookupWaiter waiter = new LookupWaiter(name, nonce);
_pendingLookups.offer(waiter); _pendingLookups.offer(waiter);
Destination rv = null;
try { try {
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info("Sending HostLookup for " + name); _log.info("Sending HostLookup for " + name);
SessionId id = _sessionId; SessionId id = _sessionId;
if (id == null) if (id == null)
id = new SessionId(65535); id = DUMMY_SESSION;
sendMessage_unchecked(new HostLookupMessage(id, name, nonce, maxWait)); sendMessage_unchecked(new HostLookupMessage(id, name, nonce, maxWait));
try { try {
synchronized (waiter) { synchronized (waiter) {
waiter.wait(maxWait); waiter.wait(maxWait);
rv = waiter.destination; return waiter;
} }
} catch (InterruptedException ie) { } catch (InterruptedException ie) {
throw new I2PSessionException("Interrupted", ie); throw new I2PSessionException("Interrupted", ie);
@ -1762,7 +1828,6 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
} finally { } finally {
_pendingLookups.remove(waiter); _pendingLookups.remove(waiter);
} }
return rv;
} }
/** /**
@ -1793,6 +1858,22 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
return _bwLimits; return _bwLimits;
} }
/**
*
* @param expiration ms from now, 0 means forever
* @since 0.9.43
*/
public void sendBlindingInfo(BlindData bd, int expiration) throws I2PSessionException {
if (!_routerSupportsBlindingInfo)
throw new I2PSessionException("Router does not support BlindingInfo");
if (_log.shouldInfo())
_log.info("Sending BlindingInfo");
SessionId id = _sessionId;
if (id == null)
id = DUMMY_SESSION;
sendMessage_unchecked(new BlindingInfoMessage(bd, id, expiration));
}
protected void updateActivity() { protected void updateActivity() {
_lastActivity = _context.clock().now(); _lastActivity = _context.clock().now();
if (_isReduced) { if (_isReduced) {

View File

@ -0,0 +1,31 @@
package net.i2p.client.impl;
import net.i2p.client.LookupResult;
import net.i2p.data.Destination;
/**
* The return value of I2PSession.lookupDest2()
*
* @since 0.9.43
*/
public class LkupResult implements LookupResult {
private final int _code;
private final Destination _dest;
LkupResult(int code, Destination dest) {
_code = code;
_dest = dest;
}
/**
* @return zero for success, nonzero for failure
*/
public int getResultCode() { return _code; }
/**
* @return Destination on success, null on failure
*/
public Destination getDestination() { return _dest; }
}

View File

@ -263,8 +263,8 @@ class SubSession extends I2PSessionMuxedImpl {
* on reception of HostReplyMessage * on reception of HostReplyMessage
*/ */
@Override @Override
void destLookupFailed(long nonce) { void destLookupFailed(long nonce, int code) {
_primary.destLookupFailed(nonce); _primary.destLookupFailed(nonce, code);
} }
/** /**

View File

@ -199,6 +199,7 @@ public final class Blinding {
* See proposal 149. * See proposal 149.
* *
* @param address ending with ".b32.i2p" * @param address ending with ".b32.i2p"
* @return BlindData structure, use getUnblindedPubKey() for the result
* @throws IllegalArgumentException on bad inputs or unsupported SigTypes * @throws IllegalArgumentException on bad inputs or unsupported SigTypes
* @since 0.9.40 * @since 0.9.40
*/ */

View File

@ -278,6 +278,7 @@ public class BlindData {
buf.append("[BlindData: "); buf.append("[BlindData: ");
buf.append("\n\tSigningPublicKey: ").append(_clearSPK); buf.append("\n\tSigningPublicKey: ").append(_clearSPK);
buf.append("\n\tAlpha : ").append(_alpha); buf.append("\n\tAlpha : ").append(_alpha);
buf.append("\n\tAlpha valid for : ").append((new Date(_date)).toString());
buf.append("\n\tBlindedPublicKey: ").append(_blindSPK); buf.append("\n\tBlindedPublicKey: ").append(_blindSPK);
buf.append("\n\tBlinded Hash : ").append(_blindHash); buf.append("\n\tBlinded Hash : ").append(_blindHash);
if (_secret != null) if (_secret != null)
@ -298,7 +299,12 @@ public class BlindData {
else else
buf.append("\n\tDestination : unknown"); buf.append("\n\tDestination : unknown");
buf.append("\n\tB32 : ").append(toBase32()); buf.append("\n\tB32 : ").append(toBase32());
buf.append("\n\tCreated : ").append((new Date(_date)).toString()); if (!_authRequired)
buf.append("\n\t + auth : ").append(Blinding.encode(_clearSPK, _secretRequired, true));
if (!_secretRequired)
buf.append("\n\t + secret : ").append(Blinding.encode(_clearSPK, true, _authRequired));
if (!(_authRequired || _secretRequired))
buf.append("\n\t + auth,secret : ").append(Blinding.encode(_clearSPK, true, true));
buf.append(']'); buf.append(']');
return buf.toString(); return buf.toString();
} }

View File

@ -845,9 +845,9 @@ public class PrivateKeyFile {
@Override @Override
public String toString() { public String toString() {
StringBuilder s = new StringBuilder(128); StringBuilder s = new StringBuilder(128);
s.append("Dest: "); s.append("Destination: ");
s.append(this.dest != null ? this.dest.toBase64() : "null"); s.append(this.dest != null ? this.dest.toBase64() : "null");
s.append("\nB32: "); s.append("\nB32 : ");
s.append(this.dest != null ? this.dest.toBase32() : "null"); s.append(this.dest != null ? this.dest.toBase32() : "null");
if (dest != null) { if (dest != null) {
SigningPublicKey spk = dest.getSigningPublicKey(); SigningPublicKey spk = dest.getSigningPublicKey();
@ -856,6 +856,9 @@ public class PrivateKeyFile {
type == SigType.RedDSA_SHA512_Ed25519) { type == SigType.RedDSA_SHA512_Ed25519) {
I2PAppContext ctx = I2PAppContext.getGlobalContext(); I2PAppContext ctx = I2PAppContext.getGlobalContext();
s.append("\nBlinded B32: ").append(Blinding.encode(spk)); s.append("\nBlinded B32: ").append(Blinding.encode(spk));
s.append("\n + auth key: ").append(Blinding.encode(spk, false, true));
s.append("\n + password: ").append(Blinding.encode(spk, true, false));
s.append("\n + auth/pw : ").append(Blinding.encode(spk, true, true));
} }
} }
s.append("\nContains: "); s.append("\nContains: ");

View File

@ -0,0 +1,370 @@
package net.i2p.data.i2cp;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import net.i2p.I2PAppContext;
import net.i2p.crypto.EncType;
import net.i2p.crypto.SigType;
import net.i2p.data.BlindData;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.PrivateKey;
import net.i2p.data.SigningPublicKey;
/**
* Advise the router that the endpoint is blinded.
* Client to router. There is no reply.
* Preliminary - subject to change -
* See proposal 123.
*
* @see BlindData
* @see net.i2p.crypto.Blinding
*
* @since 0.9.43; do not send to routers older than 0.9.43.
*/
public class BlindingInfoMessage extends I2CPMessageImpl {
public final static int MESSAGE_TYPE = 42;
private SessionId _sessionId;
private int _endpointType;
private int _authType;
private SigType _blindType;
private long _expiration;
private Hash _hash;
private String _host;
private Destination _dest;
private SigningPublicKey _pubkey;
private PrivateKey _privkey;
private String _secret;
private BlindData _blindData;
private static final int FLAG_AUTH = 0x0f;
private static final int FLAG_SECRET = 0x10;
public static final int TYPE_HASH = 0;
public static final int TYPE_HOST = 1;
public static final int TYPE_DEST = 2;
public static final int TYPE_KEY = 3;
public BlindingInfoMessage() {}
/**
* @param expiration ms from now or 0 for forever
*/
public BlindingInfoMessage(BlindData bd,
SessionId id, int expiration) {
this(id, expiration, bd.getAuthType(), bd.getBlindedSigType(), bd.getAuthPrivKey(), bd.getSecret());
Destination dest = bd.getDestination();
if (dest != null) {
_dest = dest;
_hash = dest.calculateHash();
_pubkey = dest.getSigningPublicKey();
_endpointType = TYPE_DEST;
} else if (bd.getUnblindedPubKey() != null) {
_pubkey = bd.getUnblindedPubKey();
_endpointType = TYPE_KEY;
} else {
throw new IllegalArgumentException();
}
_blindData = bd;
}
/**
* @param authType 0 (none), 1 (DH), 3 (PSK)
* @param expiration ms from now or 0 for forever
* @param privKey null for auth none, non-null for DH/PSK
* @param secret may be null
*/
public BlindingInfoMessage(Hash h,
SessionId id, int expiration,
int authType, SigType blindType,
PrivateKey privKey, String secret) {
this(id, expiration, authType, blindType, privKey, secret);
if (h == null || h.getData() == null)
throw new IllegalArgumentException();
_hash = h;
_endpointType = TYPE_HASH;
}
/**
* @param h hostname
* @param authType 0 (none), 1 (DH), 3 (PSK)
* @param expiration ms from now or 0 for forever
* @param privKey null for auth none, non-null for DH/PSK
* @param secret may be null
*/
public BlindingInfoMessage(String h,
SessionId id, int expiration,
int authType, SigType blindType,
PrivateKey privKey, String secret) {
this(id, expiration, authType, blindType, privKey, secret);
if (h == null)
throw new IllegalArgumentException();
_host = h;
_endpointType = TYPE_HOST;
}
/**
* @param authType 0 (none), 1 (DH), 3 (PSK)
* @param expiration ms from now or 0 for forever
* @param privKey null for auth none, non-null for DH/PSK
* @param secret may be null
*/
public BlindingInfoMessage(Destination d,
SessionId id, int expiration,
int authType, SigType blindType,
PrivateKey privKey, String secret) {
this(id, expiration, authType, blindType, privKey, secret);
if (d == null || d.getSigningPublicKey() == null)
throw new IllegalArgumentException();
_dest = d;
_hash = d.calculateHash();
_pubkey = d.getSigningPublicKey();
_endpointType = TYPE_DEST;
}
/**
* @param authType 0 (none), 1 (DH), 3 (PSK)
* @param expiration ms from now or 0 for forever
* @param privKey null for auth none, non-null for DH/PSK
* @param secret may be null
*/
public BlindingInfoMessage(SigningPublicKey s,
SessionId id, int expiration,
int authType, SigType blindType,
PrivateKey privKey, String secret) {
this(id, expiration, authType, blindType, privKey, secret);
if (s == null || s.getData() == null)
throw new IllegalArgumentException();
_pubkey = s;
_endpointType = TYPE_KEY;
}
private BlindingInfoMessage(SessionId id, int expiration, int authType, SigType blindType,
PrivateKey privKey, String secret) {
if (id == null || blindType == null)
throw new IllegalArgumentException();
if (authType != BlindData.AUTH_NONE && authType != BlindData.AUTH_DH &&
authType != BlindData.AUTH_PSK)
throw new IllegalArgumentException();
if (authType == BlindData.AUTH_NONE && privKey != null)
throw new IllegalArgumentException("no key required");
if (authType != BlindData.AUTH_NONE && privKey == null)
throw new IllegalArgumentException("key required");
_sessionId = id;
_authType = authType;
_blindType = blindType;
if (expiration > 0)
_expiration = expiration + I2PAppContext.getGlobalContext().clock().now();
_privkey = privKey;
_secret = secret;
}
public SessionId getSessionId() {
return _sessionId;
}
/**
* Return the SessionId for this message.
*/
@Override
public SessionId sessionId() {
return _sessionId;
}
/**
* @return ms 1 to 2**32 - 1
*/
public long getTimeout() {
return _expiration;
}
/**
* @return 0 (none), 1 (DH), 3 (PSK)
*/
public int getAuthType() {
return _authType;
}
/**
* @return 0 (hash) or 1 (host) or 2 (dest) or 3 (key)
*/
public int getEndpointType() {
return _endpointType;
}
/**
* @return only valid if endpoint type == 0 or 2
*/
public Hash getHash() {
return _hash;
}
/**
* @return only valid if endpoint type == 1
*/
public String getHostname() {
return _host;
}
/**
* @return only valid if endpoint type == 2
*/
public String getDestination() {
return _host;
}
/**
* @return only valid if endpoint type == 2 or 3
*/
public SigningPublicKey getSigningPublicKey() {
return _pubkey;
}
/**
* @return private key or null
*/
public PrivateKey getPrivateKey() {
return _privkey;
}
/**
* @return secret or null
*/
public String getSecret() {
return _secret;
}
/**
* @return blind data or null if not enough info
*/
public BlindData getBlindData() {
if (_blindData != null)
return _blindData;
if (_endpointType == TYPE_DEST)
_blindData = new BlindData(I2PAppContext.getGlobalContext(), _dest, _blindType, _secret, _authType, _privkey);
else if (_endpointType == TYPE_KEY)
_blindData = new BlindData(I2PAppContext.getGlobalContext(), _pubkey, _blindType, _secret, _authType, _privkey);
return _blindData;
}
protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
try {
_sessionId = new SessionId();
_sessionId.readBytes(in);
int flags = in.read();
_authType = flags & FLAG_AUTH;
boolean hasSecret = (flags & FLAG_SECRET) != 0;
_endpointType = in.read();
int bt = (int) DataHelper.readLong(in, 2);
_blindType = SigType.getByCode(bt);
if (_blindType == null)
throw new I2CPMessageException("unsupported sig type " + bt);
_expiration = DataHelper.readLong(in, 4);
if (_endpointType == TYPE_HASH) {
_hash = Hash.create(in);
} else if (_endpointType == TYPE_HOST) {
_host = DataHelper.readString(in);
if (_host.length() == 0)
throw new I2CPMessageException("bad host");
} else if (_endpointType == TYPE_DEST) {
_dest = Destination.create(in);
} else if (_endpointType == TYPE_KEY) {
int st = (int) DataHelper.readLong(in, 2);
SigType sigt = SigType.getByCode(st);
if (sigt == null)
throw new I2CPMessageException("unsupported sig type " + st);
int len = sigt.getPubkeyLen();
byte[] key = new byte[len];
DataHelper.read(in, key);
_pubkey = new SigningPublicKey(sigt, key);
} else {
throw new I2CPMessageException("bad type");
}
if (_authType == BlindData.AUTH_DH || _authType == BlindData.AUTH_PSK) {
byte[] key = new byte[32];
DataHelper.read(in, key);
_privkey = new PrivateKey(EncType.ECIES_X25519, key);
} else if (_authType != BlindData.AUTH_NONE) {
throw new I2CPMessageException("bad auth type " + _authType);
}
if (hasSecret)
_secret = DataHelper.readString(in);
} catch (DataFormatException dfe) {
throw new I2CPMessageException("bad data", dfe);
}
}
protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
int len;
if (_endpointType == TYPE_HASH) {
if (_hash == null)
throw new I2CPMessageException("Unable to write out the message as there is not enough data");
len = 11 + Hash.HASH_LENGTH;
} else if (_endpointType == TYPE_HOST) {
if (_host == null)
throw new I2CPMessageException("Unable to write out the message as there is not enough data");
len = 12 + _host.length();
} else {
throw new I2CPMessageException("bad type");
}
ByteArrayOutputStream os = new ByteArrayOutputStream(len);
try {
_sessionId.writeBytes(os);
byte flags = (byte) _authType;
if (_secret != null)
flags |= FLAG_SECRET;
os.write(flags);
os.write((byte) _endpointType);
DataHelper.writeLong(os, 2, _blindType.getCode());
DataHelper.writeLong(os, 4, _expiration);
if (_endpointType == TYPE_HASH) {
_hash.writeBytes(os);
} else if (_endpointType == TYPE_HOST) {
DataHelper.writeString(os, _host);
} else if (_endpointType == TYPE_DEST) {
_dest.writeBytes(os);
} else {
DataHelper.writeLong(os, 2, _privkey.getType().getCode());
os.write(_privkey.getData());
DataHelper.writeString(os, _host);
}
if (_secret != null)
DataHelper.writeString(os, _secret);
} catch (DataFormatException dfe) {
throw new I2CPMessageException("bad data", dfe);
}
return os.toByteArray();
}
public int getType() {
return MESSAGE_TYPE;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("[BlindingInfoMessage: ");
buf.append("\n\tSession: ").append(_sessionId);
buf.append("\n\tTimeout: ").append(_expiration);
if (_endpointType == TYPE_HASH)
buf.append("\n\tHash: ").append(_hash.toBase32());
else if (_endpointType == TYPE_HOST)
buf.append("\n\tHost: ").append(_host);
else if (_endpointType == TYPE_DEST)
buf.append("\n\tDest: ").append(_dest);
else if (_endpointType == TYPE_KEY)
buf.append("\n\tKey: ").append(_pubkey);
if (_privkey != null)
buf.append("\n\tPriv Key: ").append(_privkey);
if (_secret != null)
buf.append("\n\tSecret: ").append(_secret);
buf.append("]");
return buf.toString();
}
}

View File

@ -1,3 +1,6 @@
2019-09-10 zzz
* I2CP: New Blinding Info message (proposal 123)
2019-09-08 zzz 2019-09-08 zzz
* Transport: * Transport:
- Don't automatically transition from firewalled - Don't automatically transition from firewalled

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 = 5; public final static long BUILD = 6;
/** for example "-test" */ /** for example "-test" */
public final static String EXTRA = ""; public final static String EXTRA = "";

View File

@ -15,6 +15,7 @@ import net.i2p.CoreVersion;
import net.i2p.crypto.EncType; import net.i2p.crypto.EncType;
import net.i2p.crypto.SigType; import net.i2p.crypto.SigType;
import net.i2p.data.Base64; import net.i2p.data.Base64;
import net.i2p.data.BlindData;
import net.i2p.data.DatabaseEntry; import net.i2p.data.DatabaseEntry;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
import net.i2p.data.Destination; import net.i2p.data.Destination;
@ -25,7 +26,9 @@ import net.i2p.data.LeaseSet2;
import net.i2p.data.Payload; import net.i2p.data.Payload;
import net.i2p.data.PrivateKey; import net.i2p.data.PrivateKey;
import net.i2p.data.PublicKey; import net.i2p.data.PublicKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.data.i2cp.BandwidthLimitsMessage; import net.i2p.data.i2cp.BandwidthLimitsMessage;
import net.i2p.data.i2cp.BlindingInfoMessage;
import net.i2p.data.i2cp.CreateLeaseSetMessage; import net.i2p.data.i2cp.CreateLeaseSetMessage;
import net.i2p.data.i2cp.CreateLeaseSet2Message; import net.i2p.data.i2cp.CreateLeaseSet2Message;
import net.i2p.data.i2cp.CreateSessionMessage; import net.i2p.data.i2cp.CreateSessionMessage;
@ -152,6 +155,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
case GetBandwidthLimitsMessage.MESSAGE_TYPE: case GetBandwidthLimitsMessage.MESSAGE_TYPE:
handleGetBWLimits((GetBandwidthLimitsMessage)message); handleGetBWLimits((GetBandwidthLimitsMessage)message);
break; break;
case BlindingInfoMessage.MESSAGE_TYPE:
handleBlindingInfo((BlindingInfoMessage)message);
break;
default: default:
if (_log.shouldLog(Log.ERROR)) if (_log.shouldLog(Log.ERROR))
_log.error("Unhandled I2CP type received: " + message.getType()); _log.error("Unhandled I2CP type received: " + message.getType());
@ -774,6 +780,8 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
* Divide router limit by 1.75 for overhead. * Divide router limit by 1.75 for overhead.
* This could someday give a different answer to each client. * This could someday give a different answer to each client.
* But it's not enforced anywhere. * But it's not enforced anywhere.
*
* protected for unit test override
*/ */
protected void handleGetBWLimits(GetBandwidthLimitsMessage message) { protected void handleGetBWLimits(GetBandwidthLimitsMessage message) {
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
@ -789,4 +797,41 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
} }
} }
/**
*
* @since 0.9.43
*/
private void handleBlindingInfo(BlindingInfoMessage message) {
if (_log.shouldInfo())
_log.info("Got Blinding info");
BlindData bd = message.getBlindData();
SigningPublicKey spk = bd.getUnblindedPubKey();
if (spk == null || bd == null) {
// hash or hostname lookup? don't support for now
if (_log.shouldWarn())
_log.warn("Unsupported BlindingInfo type: " + message);
return;
}
BlindData obd = _context.netDb().getBlindData(spk);
if (obd == null) {
_context.netDb().setBlindData(bd);
if (_log.shouldWarn())
_log.warn("New: " + bd);
} else {
// update if changed
PrivateKey okey = obd.getAuthPrivKey();
PrivateKey nkey = bd.getAuthPrivKey();
String osec = obd.getSecret();
String nsec = bd.getSecret();
if ((nkey != null && !nkey.equals(okey)) ||
(nsec != null && !nsec.equals(osec))) {
_context.netDb().setBlindData(bd);
if (_log.shouldWarn())
_log.warn("Updated: " + bd);
} else {
if (_log.shouldWarn())
_log.warn("No change: " + obd);
}
}
}
} }

View File

@ -93,6 +93,7 @@ class LookupDestJob extends JobImpl {
SigningPublicKey spk = bd.getUnblindedPubKey(); SigningPublicKey spk = bd.getUnblindedPubKey();
BlindData bd2 = getContext().netDb().getBlindData(spk); BlindData bd2 = getContext().netDb().getBlindData(spk);
if (bd2 != null) { if (bd2 != null) {
// BlindData from database may have privkey or secret
bd = bd2; bd = bd2;
} else { } else {
getContext().netDb().setBlindData(bd); getContext().netDb().setBlindData(bd);
@ -132,6 +133,20 @@ class LookupDestJob extends JobImpl {
returnDest(d); returnDest(d);
return; return;
} }
boolean fail1 = _blindData.getAuthRequired() && _blindData.getAuthPrivKey() == null;
boolean fail2 = _blindData.getSecretRequired() && _blindData.getSecret() == null;
if (fail1 || fail2) {
int code;
if (fail1 && fail2)
code = HostReplyMessage.RESULT_SECRET_AND_KEY_REQUIRED;
else if (fail1)
code = HostReplyMessage.RESULT_KEY_REQUIRED;
else
code = HostReplyMessage.RESULT_SECRET_REQUIRED;
if (_log.shouldDebug())
_log.debug("Failed b33 lookup " + _name + " with code " + code);
returnFail(code);
}
} }
if (_name != null) { if (_name != null) {
// inline, ignore timeout // inline, ignore timeout
@ -151,7 +166,7 @@ class LookupDestJob extends JobImpl {
getContext().netDb().lookupDestination(_hash, done, _timeout, _fromLocalDest); getContext().netDb().lookupDestination(_hash, done, _timeout, _fromLocalDest);
} else { } else {
// blinding decode fail // blinding decode fail
returnFail(); returnFail(HostReplyMessage.RESULT_DECRYPTION_FAILURE);
} }
} }
@ -198,13 +213,22 @@ class LookupDestJob extends JobImpl {
} }
/** /**
* Return the failed hash so the client can correlate replies with requests * Return the request ID or failed hash so the client can correlate replies with requests
* @since 0.8.3 * @since 0.8.3
*/ */
private void returnFail() { private void returnFail() {
returnFail(HostReplyMessage.RESULT_FAILURE);
}
/**
* Return the request ID or failed hash so the client can correlate replies with requests
* @param code failure code, greater than zero, only used for HostReplyMessage
* @since 0.9.43
*/
private void returnFail(int code) {
I2CPMessage msg; I2CPMessage msg;
if (_reqID >= 0) if (_reqID >= 0)
msg = new HostReplyMessage(_sessID, HostReplyMessage.RESULT_FAILURE, _reqID); msg = new HostReplyMessage(_sessID, code, _reqID);
else if (_hash != null) else if (_hash != null)
msg = new DestReplyMessage(_hash); msg = new DestReplyMessage(_hash);
else else