diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index 243702ffcd..160b43afe9 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -24,12 +24,15 @@ import net.i2p.app.ClientApp; import net.i2p.app.ClientAppManager; import net.i2p.app.Outproxy; import net.i2p.client.I2PSession; +import net.i2p.client.LookupResult; import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocketManager; import net.i2p.client.streaming.I2PSocketOptions; +import net.i2p.crypto.Blinding; import net.i2p.crypto.SHA256Generator; import net.i2p.data.Base32; import net.i2p.data.Base64; +import net.i2p.data.BlindData; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; @@ -1231,7 +1234,25 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } else if (len >= 64) { if (_log.shouldInfo()) _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 { // 61-63 chars, this won't work clientDest = _context.namingService().lookup(destination); diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java index 48e00edbf7..0b0303daa2 100644 --- a/core/java/src/net/i2p/client/I2PSession.java +++ b/core/java/src/net/i2p/client/I2PSession.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Properties; import java.util.Set; +import net.i2p.data.BlindData; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.PrivateKey; @@ -409,6 +410,17 @@ public interface I2PSession { */ 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. * Does not remove properties previously present but missing from this options parameter. @@ -425,6 +437,13 @@ public interface I2PSession { */ 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. * diff --git a/core/java/src/net/i2p/client/LookupResult.java b/core/java/src/net/i2p/client/LookupResult.java new file mode 100644 index 0000000000..6b3f74ced9 --- /dev/null +++ b/core/java/src/net/i2p/client/LookupResult.java @@ -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(); + +} diff --git a/core/java/src/net/i2p/client/impl/HostReplyMessageHandler.java b/core/java/src/net/i2p/client/impl/HostReplyMessageHandler.java index 4cce11ab91..c1ebbad825 100644 --- a/core/java/src/net/i2p/client/impl/HostReplyMessageHandler.java +++ b/core/java/src/net/i2p/client/impl/HostReplyMessageHandler.java @@ -32,7 +32,7 @@ class HostReplyMessageHandler extends HandlerImpl { if (d != null) { session.destReceived(id, d); } else { - session.destLookupFailed(id); + session.destLookupFailed(id, msg.getResultCode()); } } } diff --git a/core/java/src/net/i2p/client/impl/I2PSessionImpl.java b/core/java/src/net/i2p/client/impl/I2PSessionImpl.java index f4a1063ad9..bb9168d80a 100644 --- a/core/java/src/net/i2p/client/impl/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/impl/I2PSessionImpl.java @@ -38,9 +38,11 @@ import net.i2p.client.I2PClient; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.client.I2PSessionListener; +import net.i2p.client.LookupResult; import net.i2p.crypto.EncType; import net.i2p.crypto.SigType; import net.i2p.data.Base32; +import net.i2p.data.BlindData; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; @@ -50,6 +52,7 @@ import net.i2p.data.PrivateKey; import net.i2p.data.Signature; import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPublicKey; +import net.i2p.data.i2cp.BlindingInfoMessage; import net.i2p.data.i2cp.DestLookupMessage; import net.i2p.data.i2cp.DestReplyMessage; 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 _routerSupportsHostLookup; private volatile boolean _routerSupportsLS2; + private volatile boolean _routerSupportsBlindingInfo; 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 _lookupCache = new LHMCache(CACHE_MAX_SIZE); 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 */ 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_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 */ void dateUpdated(String routerVersion) { @@ -233,7 +239,6 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 (routerVersion != null && routerVersion.length() > 0 && VersionComparator.comp(routerVersion, MIN_FAST_VERSION) >= 0); _routerSupportsHostLookup = isrc || - TEST_LOOKUP || (routerVersion != null && routerVersion.length() > 0 && VersionComparator.comp(routerVersion, MIN_HOST_LOOKUP_VERSION) >= 0); _routerSupportsSubsessions = isrc || @@ -242,6 +247,10 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 _routerSupportsLS2 = isrc || (routerVersion != null && routerVersion.length() > 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) { if (_state == State.OPENING) { 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; private static final int BUF_SIZE = 32*1024; + private static final SessionId DUMMY_SESSION = new SessionId(65535); /** * for extension by SimpleSession (no dest) @@ -1500,6 +1510,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 if (h.equals(w.hash)) { synchronized (w) { w.destination = d; + w.code = LookupResult.RESULT_SUCCESS; w.notifyAll(); } } @@ -1515,6 +1526,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 for (LookupWaiter w : _pendingLookups) { if (h.equals(w.hash)) { synchronized (w) { + w.code = LookupResult.RESULT_FAILURE; w.notifyAll(); } } @@ -1539,6 +1551,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 } synchronized (w) { w.destination = d; + w.code = LookupResult.RESULT_SUCCESS; w.notifyAll(); } } @@ -1550,10 +1563,11 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 * on reception of HostReplyMessage * @since 0.9.11 */ - void destLookupFailed(long nonce) { + void destLookupFailed(long nonce, int code) { for (LookupWaiter w : _pendingLookups) { if (nonce == w.nonce) { synchronized (w) { + w.code = code; w.notifyAll(); } } @@ -1581,6 +1595,11 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 public final long nonce; /** the reply; synch on this */ public Destination destination; + /** + * the return code; sync on this + * @since 0.9.43 + */ + public int code; public LookupWaiter(Hash h) { this(h, -1); @@ -1599,6 +1618,16 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 this.name = name; 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 */ @@ -1657,7 +1686,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 _log.info("Sending HostLookup for " + h); SessionId id = _sessionId; if (id == null) - id = new SessionId(65535); + id = DUMMY_SESSION; sendMessage_unchecked(new HostLookupMessage(id, h, nonce, maxWait)); } else { if (_log.shouldLog(Log.INFO)) @@ -1708,12 +1737,50 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 * @return null on failure */ 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) return null; // Shortcut for b64 if (name.length() >= 516) { try { - return new Destination(name); + return new LookupWaiter(new Destination(name)); } catch (DataFormatException dfe) { return null; } @@ -1724,7 +1791,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 synchronized (_lookupCache) { Destination rv = _lookupCache.get(name); if (rv != null) - return rv; + return new LookupWaiter(rv); } if (isClosed()) { if (_log.shouldLog(Log.INFO)) @@ -1734,7 +1801,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 if (!_routerSupportsHostLookup) { // do them a favor and convert to Hash lookup 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 if (_log.shouldLog(Log.WARN)) _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; LookupWaiter waiter = new LookupWaiter(name, nonce); _pendingLookups.offer(waiter); - Destination rv = null; try { if (_log.shouldLog(Log.INFO)) _log.info("Sending HostLookup for " + name); SessionId id = _sessionId; if (id == null) - id = new SessionId(65535); + id = DUMMY_SESSION; sendMessage_unchecked(new HostLookupMessage(id, name, nonce, maxWait)); try { synchronized (waiter) { waiter.wait(maxWait); - rv = waiter.destination; + return waiter; } } catch (InterruptedException ie) { throw new I2PSessionException("Interrupted", ie); @@ -1762,7 +1828,6 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 } finally { _pendingLookups.remove(waiter); } - return rv; } /** @@ -1793,6 +1858,22 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2 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() { _lastActivity = _context.clock().now(); if (_isReduced) { diff --git a/core/java/src/net/i2p/client/impl/LkupResult.java b/core/java/src/net/i2p/client/impl/LkupResult.java new file mode 100644 index 0000000000..0cc71bcdbf --- /dev/null +++ b/core/java/src/net/i2p/client/impl/LkupResult.java @@ -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; } + +} diff --git a/core/java/src/net/i2p/client/impl/SubSession.java b/core/java/src/net/i2p/client/impl/SubSession.java index d06e2d798c..451a351d2b 100644 --- a/core/java/src/net/i2p/client/impl/SubSession.java +++ b/core/java/src/net/i2p/client/impl/SubSession.java @@ -263,8 +263,8 @@ class SubSession extends I2PSessionMuxedImpl { * on reception of HostReplyMessage */ @Override - void destLookupFailed(long nonce) { - _primary.destLookupFailed(nonce); + void destLookupFailed(long nonce, int code) { + _primary.destLookupFailed(nonce, code); } /** diff --git a/core/java/src/net/i2p/crypto/Blinding.java b/core/java/src/net/i2p/crypto/Blinding.java index 8ea3101ab6..816bbbfc73 100644 --- a/core/java/src/net/i2p/crypto/Blinding.java +++ b/core/java/src/net/i2p/crypto/Blinding.java @@ -199,6 +199,7 @@ public final class Blinding { * See proposal 149. * * @param address ending with ".b32.i2p" + * @return BlindData structure, use getUnblindedPubKey() for the result * @throws IllegalArgumentException on bad inputs or unsupported SigTypes * @since 0.9.40 */ diff --git a/core/java/src/net/i2p/data/BlindData.java b/core/java/src/net/i2p/data/BlindData.java index 1911659533..1959e670f6 100644 --- a/core/java/src/net/i2p/data/BlindData.java +++ b/core/java/src/net/i2p/data/BlindData.java @@ -278,6 +278,7 @@ public class BlindData { buf.append("[BlindData: "); buf.append("\n\tSigningPublicKey: ").append(_clearSPK); 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\tBlinded Hash : ").append(_blindHash); if (_secret != null) @@ -298,7 +299,12 @@ public class BlindData { else buf.append("\n\tDestination : unknown"); 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(']'); return buf.toString(); } diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java index 3cb20d2998..9e3d63ed87 100644 --- a/core/java/src/net/i2p/data/PrivateKeyFile.java +++ b/core/java/src/net/i2p/data/PrivateKeyFile.java @@ -845,9 +845,9 @@ public class PrivateKeyFile { @Override public String toString() { StringBuilder s = new StringBuilder(128); - s.append("Dest: "); + s.append("Destination: "); s.append(this.dest != null ? this.dest.toBase64() : "null"); - s.append("\nB32: "); + s.append("\nB32 : "); s.append(this.dest != null ? this.dest.toBase32() : "null"); if (dest != null) { SigningPublicKey spk = dest.getSigningPublicKey(); @@ -856,6 +856,9 @@ public class PrivateKeyFile { type == SigType.RedDSA_SHA512_Ed25519) { I2PAppContext ctx = I2PAppContext.getGlobalContext(); 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: "); diff --git a/core/java/src/net/i2p/data/i2cp/BlindingInfoMessage.java b/core/java/src/net/i2p/data/i2cp/BlindingInfoMessage.java new file mode 100644 index 0000000000..20da15c99b --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/BlindingInfoMessage.java @@ -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(); + } +} diff --git a/history.txt b/history.txt index dcb3808428..d76739f452 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,6 @@ +2019-09-10 zzz + * I2CP: New Blinding Info message (proposal 123) + 2019-09-08 zzz * Transport: - Don't automatically transition from firewalled diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 4e28a2c8d2..c10128fe62 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 = 5; + public final static long BUILD = 6; /** for example "-test" */ public final static String EXTRA = ""; diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java index 7d3cf5e930..6c54e51b7e 100644 --- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -15,6 +15,7 @@ import net.i2p.CoreVersion; import net.i2p.crypto.EncType; import net.i2p.crypto.SigType; import net.i2p.data.Base64; +import net.i2p.data.BlindData; import net.i2p.data.DatabaseEntry; import net.i2p.data.DataHelper; import net.i2p.data.Destination; @@ -25,7 +26,9 @@ import net.i2p.data.LeaseSet2; import net.i2p.data.Payload; import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; +import net.i2p.data.SigningPublicKey; import net.i2p.data.i2cp.BandwidthLimitsMessage; +import net.i2p.data.i2cp.BlindingInfoMessage; import net.i2p.data.i2cp.CreateLeaseSetMessage; import net.i2p.data.i2cp.CreateLeaseSet2Message; import net.i2p.data.i2cp.CreateSessionMessage; @@ -152,6 +155,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi case GetBandwidthLimitsMessage.MESSAGE_TYPE: handleGetBWLimits((GetBandwidthLimitsMessage)message); break; + case BlindingInfoMessage.MESSAGE_TYPE: + handleBlindingInfo((BlindingInfoMessage)message); + break; default: if (_log.shouldLog(Log.ERROR)) _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. * This could someday give a different answer to each client. * But it's not enforced anywhere. + * + * protected for unit test override */ protected void handleGetBWLimits(GetBandwidthLimitsMessage message) { 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); + } + } + } } diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java index fd442b0d32..ec3c2386f0 100644 --- a/router/java/src/net/i2p/router/client/LookupDestJob.java +++ b/router/java/src/net/i2p/router/client/LookupDestJob.java @@ -93,6 +93,7 @@ class LookupDestJob extends JobImpl { SigningPublicKey spk = bd.getUnblindedPubKey(); BlindData bd2 = getContext().netDb().getBlindData(spk); if (bd2 != null) { + // BlindData from database may have privkey or secret bd = bd2; } else { getContext().netDb().setBlindData(bd); @@ -132,6 +133,20 @@ class LookupDestJob extends JobImpl { returnDest(d); 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) { // inline, ignore timeout @@ -151,7 +166,7 @@ class LookupDestJob extends JobImpl { getContext().netDb().lookupDestination(_hash, done, _timeout, _fromLocalDest); } else { // 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 */ 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; if (_reqID >= 0) - msg = new HostReplyMessage(_sessID, HostReplyMessage.RESULT_FAILURE, _reqID); + msg = new HostReplyMessage(_sessID, code, _reqID); else if (_hash != null) msg = new DestReplyMessage(_hash); else