diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index 55c9419c7cf48552c0dd37ec46fba306005019fa..3ad564c5152ae2523f02c33fbd429db096b2a49d 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -1204,18 +1204,29 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } } else if("i2p".equals(host)) { clientDest = null; - } else if(destination.length() == 60 && destination.toLowerCase(Locale.US).endsWith(".b32.i2p")) { + } else if (destination.length() >= 60 && destination.toLowerCase(Locale.US).endsWith(".b32.i2p")) { // use existing session to look up for efficiency verifySocketManager(); I2PSession sess = sockMgr.getSession(); - if(!sess.isClosed()) { - byte[] hData = Base32.decode(destination.substring(0, 52)); - if(hData != null) { - if(_log.shouldLog(Log.INFO)) { - _log.info("lookup in-session " + destination); + if (!sess.isClosed()) { + int len = destination.length(); + if (len == 60) { + byte[] hData = Base32.decode(destination.substring(0, 52)); + if (hData != null) { + if (_log.shouldInfo()) + _log.info("lookup b32 in-session " + destination); + Hash hash = Hash.create(hData); + clientDest = sess.lookupDest(hash, 20*1000); + } else { + clientDest = null; } - Hash hash = Hash.create(hData); - clientDest = sess.lookupDest(hash, 20 * 1000); + } else if (len >= 64) { + if (_log.shouldInfo()) + _log.info("lookup b33 in-session " + destination); + clientDest = sess.lookupDest(destination, 20*1000); + } else { + // 61-63 chars, this won't work + clientDest = _context.namingService().lookup(destination); } } else { clientDest = _context.namingService().lookup(destination); diff --git a/core/java/src/net/i2p/client/naming/DummyNamingService.java b/core/java/src/net/i2p/client/naming/DummyNamingService.java index 73970a09dd8654f01d4bc0f7cd3133dedf223748..89a73540c0e3837da96e79d3b990b522300a22e7 100644 --- a/core/java/src/net/i2p/client/naming/DummyNamingService.java +++ b/core/java/src/net/i2p/client/naming/DummyNamingService.java @@ -69,10 +69,16 @@ public class DummyNamingService extends NamingService { } // Try Base32 decoding - if (hostname.length() == BASE32_HASH_LENGTH + 8 && hostname.toLowerCase(Locale.US).endsWith(".b32.i2p") && + if (hostname.length() >= BASE32_HASH_LENGTH + 8 && hostname.toLowerCase(Locale.US).endsWith(".b32.i2p") && _context.getBooleanPropertyDefaultTrue(PROP_B32)) { try { - d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH)); + if (hostname.length() == BASE32_HASH_LENGTH + 8) { + // b32 + d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH)); + } else { + // b33 + d = LookupDest.lookupHostname(_context, hostname); + } if (d != null) { putCache(hostname, d); return d; diff --git a/core/java/src/net/i2p/client/naming/LookupDest.java b/core/java/src/net/i2p/client/naming/LookupDest.java index c8e5381c5e35a774274ee876e04abd448cb46eca..ae91e8b6a1593783589a5dbb4c6a3a49de3ff65c 100644 --- a/core/java/src/net/i2p/client/naming/LookupDest.java +++ b/core/java/src/net/i2p/client/naming/LookupDest.java @@ -59,10 +59,49 @@ class LookupDest { ****/ /** @param h 32 byte hash */ - static Destination lookupHash(I2PAppContext ctx, byte[] h) throws I2PSessionException { + private static Destination lookupHash(I2PAppContext ctx, byte[] h) throws I2PSessionException { Hash key = Hash.create(h); Destination rv = null; I2PClient client = new I2PSimpleClient(); + Properties opts = getOpts(ctx); + I2PSession session = null; + try { + session = client.createSession(null, opts); + session.connect(); + rv = session.lookupDest(key, DEFAULT_TIMEOUT); + } finally { + if (session != null) + session.destroySession(); + } + return rv; + } + + /** + * Any hostname, but this is for long-format b32 + * + * @param hostname a "b33" hostname, 64+ chars ending with ".b32.i2p" + * @since 0.9.40 + */ + static Destination lookupHostname(I2PAppContext ctx, String hostname) throws I2PSessionException { + Destination rv = null; + I2PClient client = new I2PSimpleClient(); + Properties opts = getOpts(ctx); + I2PSession session = null; + try { + session = client.createSession(null, opts); + session.connect(); + rv = session.lookupDest(hostname, DEFAULT_TIMEOUT); + } finally { + if (session != null) + session.destroySession(); + } + return rv; + } + + /** + * @since 0.9.40 split out from above + */ + private static Properties getOpts(I2PAppContext ctx) { Properties opts = new Properties(); if (!ctx.isRouterContext()) { String s = ctx.getProperty(I2PClient.PROP_TCP_HOST); @@ -81,16 +120,7 @@ class LookupDest { if (s != null) opts.put(PROP_PW, s); } - I2PSession session = null; - try { - session = client.createSession(null, opts); - session.connect(); - rv = session.lookupDest(key, DEFAULT_TIMEOUT); - } finally { - if (session != null) - session.destroySession(); - } - return rv; + return opts; } public static void main(String args[]) throws I2PSessionException { diff --git a/core/java/src/net/i2p/client/naming/MetaNamingService.java b/core/java/src/net/i2p/client/naming/MetaNamingService.java index 8f603ba1af0d0a29f0e2bd6b231519d059111936..f86894075a60277abb9a8954ecc0b54c6c2a9ad4 100644 --- a/core/java/src/net/i2p/client/naming/MetaNamingService.java +++ b/core/java/src/net/i2p/client/naming/MetaNamingService.java @@ -103,7 +103,7 @@ public class MetaNamingService extends DummyNamingService { if (d != null) return d; // Base32 failed? - if (hostname.length() == BASE32_HASH_LENGTH + 8 && hostname.toLowerCase(Locale.US).endsWith(".b32.i2p")) + if (hostname.length() >= BASE32_HASH_LENGTH + 8 && hostname.toLowerCase(Locale.US).endsWith(".b32.i2p")) return null; for (NamingService ns : _services) { diff --git a/core/java/src/net/i2p/crypto/Blinding.java b/core/java/src/net/i2p/crypto/Blinding.java index c18736283373aeef7a78f743c706320394bb5f8f..a0cd6d83880e21d3fcaa685a7d37e923dd72ae07 100644 --- a/core/java/src/net/i2p/crypto/Blinding.java +++ b/core/java/src/net/i2p/crypto/Blinding.java @@ -4,11 +4,15 @@ import java.security.GeneralSecurityException; import java.text.SimpleDateFormat; import java.util.Locale; import java.util.TimeZone; +import java.util.zip.Checksum; +import java.util.zip.CRC32; import net.i2p.I2PAppContext; import net.i2p.crypto.eddsa.EdDSABlinding; import net.i2p.crypto.eddsa.EdDSAPrivateKey; import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.data.Base32; +import net.i2p.data.BlindData; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; @@ -176,13 +180,155 @@ public final class Blinding { return new SigningPrivateKey(TYPER, b); } + /** + * What's the default blinded type for a given unblinded type? + * + * @return non-null + * @since 0.9.40 + */ + public static SigType getDefaultBlindedType(SigType unblindedType) { + if (unblindedType == TYPE) + return TYPER; + return unblindedType; + } + + /** + * Decode a new-format b32 address. + * PRELIMINARY - Subject to change - see proposal 149 + * + * @param address ending with ".b32.i2p" + * @throws IllegalArgumentException on bad inputs + * @throws UnsupportedOperationException unless supported SigTypes + * @since 0.9.40 + */ + public static BlindData decode(I2PAppContext ctx, String address) throws RuntimeException { + address = address.toLowerCase(Locale.US); + if (!address.endsWith(".b32.i2p")) + throw new IllegalArgumentException("Not a .b32.i2p address"); + byte[] b = Base32.decode(address.substring(0, address.length() - 8)); + if (b == null) + throw new IllegalArgumentException("Bad base32 encoding"); + if (b.length < 35) + throw new IllegalArgumentException("Not a new-format address"); + return decode(ctx, b); + } + + /** + * Decode a new-format b32 address. + * PRELIMINARY - Subject to change - see proposal 149 + * + * @param b 35+ bytes + * @throws IllegalArgumentException on bad inputs + * @throws UnsupportedOperationException unless supported SigTypes + * @since 0.9.40 + */ + public static BlindData decode(I2PAppContext ctx, byte[] b) throws RuntimeException { + Checksum crc = new CRC32(); + crc.update(b, 3, b.length - 3); + long check = crc.getValue(); + b[0] ^= (byte) check; + b[1] ^= (byte) (check >> 8); + b[2] ^= (byte) (check >> 16); + int flag = b[0] & 0xff; + if ((flag & 0xf8) != 0) + throw new IllegalArgumentException("Corrupt b32 or unsupported options"); + if ((flag & 0x01) != 0) + throw new IllegalArgumentException("Two byte sig types unsupported"); + if ((flag & 0x04) != 0) + throw new IllegalArgumentException("Per-client auth unsupported"); + // TODO two-byte sigtypes + int st1 = b[1] & 0xff; + int st2 = b[2] & 0xff; + SigType sigt1 = SigType.getByCode(st1); + SigType sigt2 = SigType.getByCode(st2); + if (sigt1 == null) + throw new IllegalArgumentException("Unknown sig type " + st1); + if (!sigt1.isAvailable()) + throw new IllegalArgumentException("Unavailable sig type " + sigt1); + if (sigt2 == null) + throw new IllegalArgumentException("Unknown blinded sig type " + st2); + if (!sigt2.isAvailable()) + throw new IllegalArgumentException("Unavailable blinded sig type " + sigt2); + // todo secret/privkey + int spkLen = sigt1.getPubkeyLen(); + if (3 + spkLen > b.length) + throw new IllegalArgumentException("b32 too short"); + byte[] spkData = new byte[spkLen]; + System.arraycopy(b, 3, spkData, 0, spkLen); + SigningPublicKey spk = new SigningPublicKey(sigt1, spkData); + String secret; + if ((flag & 0x02) != 0) { + if (4 + spkLen > b.length) + throw new IllegalArgumentException("No secret data"); + int secLen = b[3 + spkLen] & 0xff; + if (4 + spkLen + secLen != b.length) + throw new IllegalArgumentException("Bad b32 length"); + secret = DataHelper.getUTF8(b, 4 + spkLen, secLen); + } else if (3 + spkLen != b.length) { + throw new IllegalArgumentException("b32 too long"); + } else { + secret = null; + } + BlindData rv = new BlindData(ctx, spk, sigt2, secret); + return rv; + } + + /** + * Encode a public key as a new-format b32 address. + * PRELIMINARY - Subject to change - see proposal 149 + * + * @param secret may be empty or null + * @return (56+ chars).b32.i2p + * @throws IllegalArgumentException on bad inputs + * @throws UnsupportedOperationException unless supported SigTypes + * @since 0.9.40 + */ + public static String encode(I2PAppContext ctx, SigningPublicKey key, String secret) throws RuntimeException { + SigType type = key.getType(); + if (type != TYPE && type != TYPER) + throw new UnsupportedOperationException(); + byte sdata[] = (secret != null) ? DataHelper.getUTF8(secret) : null; + int slen = (secret != null) ? 1 + sdata.length : 0; + if (slen > 256) + throw new IllegalArgumentException("secret too long"); + byte[] d = key.getData(); + byte[] b = new byte[d.length + slen + 3]; + System.arraycopy(d, 0, b, 3, d.length); + if (slen > 0) { + b[3 + d.length] = (byte) sdata.length; + System.arraycopy(sdata, 0, b, 4 + d.length, sdata.length); + } + Checksum crc = new CRC32(); + crc.update(b, 3, b.length - 3); + long check = crc.getValue(); + // TODO two-byte sigtypes + if (slen > 0) + b[0] = 0x02; + b[1] = (byte) (type.getCode() & 0xff); + b[2] = (byte) (TYPER.getCode() & 0xff); + b[0] ^= (byte) check; + b[1] ^= (byte) (check >> 8); + b[2] ^= (byte) (check >> 16); + // todo privkey + return Base32.encode(b) + ".b32.i2p"; + } + /****** public static void main(String args[]) throws Exception { net.i2p.data.SimpleDataStructure[] keys = KeyGenerator.getInstance().generateSigningKeys(TYPE); SigningPublicKey pub = (SigningPublicKey) keys[0]; SigningPrivateKey priv = (SigningPrivateKey) keys[1]; + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + //String b32 = encode(ctx, pub, null); + String b32 = encode(ctx, pub, "foobarbaz"); + System.out.println("pub b32 is " + b32); + BlindData bd = decode(ctx, b32); + if (bd.getBlindedPubKey().equals(pub)) + System.out.println("B32 test failed"); + else + System.out.println("B32 test passed"); byte[] b = new byte[64]; - net.i2p.I2PAppContext.getGlobalContext().random().nextBytes(b); + ctx.random().nextBytes(b); b = EdDSABlinding.reduce(b); SigningPrivateKey alpha = new SigningPrivateKey(TYPER, b); SigningPublicKey bpub = null; diff --git a/core/java/src/net/i2p/data/BlindData.java b/core/java/src/net/i2p/data/BlindData.java new file mode 100644 index 0000000000000000000000000000000000000000..0d3eed59ce674c4bc2ca1bb81060df237dc4f72a --- /dev/null +++ b/core/java/src/net/i2p/data/BlindData.java @@ -0,0 +1,130 @@ +package net.i2p.data; + +import net.i2p.I2PAppContext; +import net.i2p.crypto.Blinding; +import net.i2p.crypto.SigType; + +/** + * Cache data for Blinding EdDSA keys. + * PRELIMINARY - Subject to change - see proposal 123 + * + * @since 0.9.40 + */ +public class BlindData { + + private final I2PAppContext _context; + private final SigningPublicKey _clearSPK; + private final String _secret; + private SigningPublicKey _blindSPK; + private final SigType _blindType; + private final int _authType; + private final PrivateKey _authKey; + private Hash _blindHash; + private SigningPrivateKey _alpha; + private Destination _dest; + private long _routingKeyGenMod; + + /** + * @throws IllegalArgumentException on various errors + */ + public BlindData(I2PAppContext ctx, Destination dest, SigType blindType, String secret) { + this(ctx, dest.getSigningPublicKey(), blindType, secret); + _dest = dest; + } + + /** + * @throws IllegalArgumentException on various errors + */ + public BlindData(I2PAppContext ctx, SigningPublicKey spk, SigType blindType, String secret) { + _context = ctx; + _clearSPK = spk; + _blindType = blindType; + _secret = secret; + _authType = 0; + _authKey = null; + // defer until needed + //calculate(); + } + + /** + * @return The blinded key for the current day + */ + public synchronized SigningPublicKey getBlindedPubKey() { + calculate(); + return _blindSPK; + } + + /** + * @return The hash of the blinded key for the current day + */ + public synchronized Hash getBlindedHash() { + calculate(); + return _blindHash; + } + + /** + * @return Alpha for the current day + */ + public synchronized SigningPrivateKey getAlpha() { + calculate(); + return _alpha; + } + + /** + * @return null if unknown + */ + public synchronized Destination getDestination() { + return _dest; + } + + /** + * @throws IllegalArgumentException on SigningPublicKey mismatch + */ + public synchronized void setDestination(Destination d) { + if (_dest != null) { + if (!_dest.equals(d)) + throw new IllegalArgumentException("Dest mismatch"); + return; + } + if (!d.getSigningPublicKey().equals(_clearSPK)) + throw new IllegalArgumentException("Dest mismatch"); + _dest = d; + } + + /** + * @return null if none + */ + public String getSecret() { + return _secret; + } + + /** + * @return 0 for no client auth + */ + public int getAuthType() { + return _authType; + } + + private synchronized void calculate() { + if (_context.isRouterContext()) { + RoutingKeyGenerator gen = _context.routingKeyGenerator(); + long mod = gen.getLastChanged(); + if (mod == _routingKeyGenMod) + return; + _routingKeyGenMod = mod; + } + // For now, always calculate in app context, + // where we don't have a routingKeyGenerator + // TODO we could cache based on current day + _alpha = Blinding.generateAlpha(_context, _clearSPK, _secret); + _blindSPK = Blinding.blind(_clearSPK, _alpha); + SigType bsigt2 = _blindSPK.getType(); + if (_blindType != bsigt2) { + throw new IllegalArgumentException("Requested blinded sig type " + _blindType + " supported type " + bsigt2); + } + byte[] hashData = new byte[2 + Hash.HASH_LENGTH]; + DataHelper.toLong(hashData, 0, 2, _blindType.getCode()); + System.arraycopy(_blindSPK.getData(), 0, hashData, 2, _blindSPK.length()); + _blindHash = _context.sha().calculateHash(hashData); + } +} diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java index db064bfc21aa16b7f3b9d4afd8d305fa3cf88069..96e73b3d18f31bb7adc78bf7840011cb86eeed48 100644 --- a/core/java/src/net/i2p/data/PrivateKeyFile.java +++ b/core/java/src/net/i2p/data/PrivateKeyFile.java @@ -20,12 +20,14 @@ import com.nettgryppa.security.HashCash; import gnu.getopt.Getopt; +import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.I2PClient; import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.client.naming.HostTxtEntry; +import net.i2p.crypto.Blinding; import net.i2p.crypto.DSAEngine; import net.i2p.crypto.KeyGenerator; import net.i2p.crypto.SigType; @@ -755,6 +757,15 @@ public class PrivateKeyFile { s.append(this.dest != null ? this.dest.toBase64() : "null"); s.append("\nB32: "); s.append(this.dest != null ? this.dest.toBase32() : "null"); + if (dest != null) { + SigningPublicKey spk = dest.getSigningPublicKey(); + SigType type = spk.getType(); + if (type == SigType.EdDSA_SHA512_Ed25519 || + type == SigType.RedDSA_SHA512_Ed25519) { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + s.append("\nBlinded B32: ").append(Blinding.encode(ctx, spk, null)); + } + } s.append("\nContains: "); s.append(this.dest); s.append("\nPrivate Key: "); diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java index 02212dc7e167a5b482a401327f5879be93526a90..b6a1cd939ffbc5cf560ed38d1a635baff3ba1e83 100644 --- a/router/java/src/net/i2p/router/client/LookupDestJob.java +++ b/router/java/src/net/i2p/router/client/LookupDestJob.java @@ -6,7 +6,9 @@ package net.i2p.router.client; import java.util.Locale; +import net.i2p.crypto.Blinding; import net.i2p.data.Base32; +import net.i2p.data.BlindData; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; @@ -17,12 +19,14 @@ import net.i2p.data.i2cp.I2CPMessageException; import net.i2p.data.i2cp.SessionId; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; +import net.i2p.util.Log; /** * Look up the lease of a hash, to convert it to a Destination for the client. * Or, since 0.9.11, lookup a host name in the naming service. */ class LookupDestJob extends JobImpl { + private final Log _log; private final ClientConnectionRunner _runner; private final long _reqID; private final long _timeout; @@ -52,24 +56,44 @@ class LookupDestJob extends JobImpl { long reqID, long timeout, SessionId sessID, Hash h, String name, Hash fromLocalDest) { super(context); + _log = context.logManager().getLog(LookupDestJob.class); if ((h == null && name == null) || (h != null && name != null) || (reqID >= 0 && sessID == null) || - (reqID < 0 && name != null)) + (reqID < 0 && name != null)) { + _log.warn("bad args"); throw new IllegalArgumentException(); + } _runner = runner; _reqID = reqID; _timeout = timeout; _sessID = sessID; _fromLocalDest = fromLocalDest; - if (name != null && name.length() == 60) { + if (name != null && name.length() >= 60) { // convert a b32 lookup to a hash lookup String nlc = name.toLowerCase(Locale.US); if (nlc.endsWith(".b32.i2p")) { - byte[] b = Base32.decode(nlc.substring(0, 52)); - if (b != null && b.length == Hash.HASH_LENGTH) { - h = Hash.create(b); - name = null; + byte[] b = Base32.decode(nlc.substring(0, nlc.length() - 8)); + if (b != null) { + if (b.length == Hash.HASH_LENGTH) { + h = Hash.create(b); + if (_log.shouldDebug()) + _log.debug("Converting name lookup " + name + " to " + h); + name = null; + } else if (b.length >= 35) { + // encrypted LS2 + try { + BlindData bd = Blinding.decode(context, b); + h = bd.getBlindedHash(); + if (_log.shouldDebug()) + _log.debug("Converting name lookup " + name + " to blinded " + h); + name = null; + } catch (RuntimeException re) { + if (_log.shouldWarn()) + _log.debug("Failed blinding conversion of " + name, re); + // lookup as a name, which will probably fail + } + } } } } @@ -86,10 +110,15 @@ class LookupDestJob extends JobImpl { if (_name != null) { // inline, ignore timeout Destination d = getContext().namingService().lookup(_name); - if (d != null) + if (d != null) { + if (_log.shouldDebug()) + _log.debug("Found name lookup " + _name + " to " + d); returnDest(d); - else + } else { + if (_log.shouldDebug()) + _log.debug("Failed name lookup " + _name); returnFail(); + } } else { DoneJob done = new DoneJob(getContext()); getContext().netDb().lookupDestination(_hash, done, _timeout, _fromLocalDest); @@ -103,10 +132,15 @@ class LookupDestJob extends JobImpl { public String getName() { return "LeaseSet Lookup Reply to Client"; } public void runJob() { Destination dest = getContext().netDb().lookupDestinationLocally(_hash); - if (dest != null) + if (dest != null) { + if (_log.shouldDebug()) + _log.debug("Found hash lookup " + _hash + " to " + dest); returnDest(dest); - else + } else { + if (_log.shouldDebug()) + _log.debug("Failed hash lookup " + _hash); returnFail(); + } } }