package net.i2p.router.tunnel; import net.i2p.I2PAppContext; import net.i2p.data.Base64; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.PrivateKey; import net.i2p.data.SessionKey; import net.i2p.data.i2np.BuildRequestRecord; import net.i2p.data.i2np.EncryptedBuildRecord; import net.i2p.data.i2np.TunnelBuildMessage; import net.i2p.router.RouterThrottleImpl; import net.i2p.router.util.DecayingBloomFilter; import net.i2p.router.util.DecayingHashSet; import net.i2p.util.Log; import net.i2p.util.SystemVersion; /** * Receive the build message at a certain hop, decrypt its encrypted record, * read the enclosed tunnel request, decide how to reply, write the reply, * encrypt the reply record, and return a TunnelBuildMessage to forward on to * the next hop. * * There is only one of these. * Instantiated by BuildHandler. * */ public class BuildMessageProcessor { private final I2PAppContext ctx; private final Log log; private final DecayingBloomFilter _filter; public BuildMessageProcessor(I2PAppContext ctx) { this.ctx = ctx; log = ctx.logManager().getLog(getClass()); _filter = selectFilter(); // all createRateStat in TunnelDispatcher } /** * For N typical part tunnels and rejecting 50%, that's 12N requests per hour. * This is the equivalent of (12N/600) KBps through the IVValidator filter. * * Target false positive rate is 1E-5 or lower * * @since 0.9.24 */ private DecayingBloomFilter selectFilter() { long maxMemory = SystemVersion.getMaxMemory(); int m; if (SystemVersion.isAndroid() || SystemVersion.isARM() || maxMemory < 96*1024*1024L) { // 32 KB // appx 500 part. tunnels or 6K req/hr m = 17; } else if (ctx.getProperty(RouterThrottleImpl.PROP_MAX_TUNNELS, RouterThrottleImpl.DEFAULT_MAX_TUNNELS) > RouterThrottleImpl.DEFAULT_MAX_TUNNELS && maxMemory > 256*1024*1024L) { // 2 MB // appx 20K part. tunnels or 240K req/hr m = 23; } else if (maxMemory > 256*1024*1024L) { // 1 MB // appx 10K part. tunnels or 120K req/hr m = 22; } else if (maxMemory > 128*1024*1024L) { // 512 KB // appx 5K part. tunnels or 60K req/hr m = 21; } else { // 128 KB // appx 2K part. tunnels or 24K req/hr m = 19; } if (log.shouldInfo()) log.info("Selected Bloom filter m = " + m); return new DecayingBloomFilter(ctx, 60*60*1000, 32, "TunnelBMP", m); } /** * Decrypt the record targetting us, encrypting all of the other records with the included * reply key and IV. The original, encrypted record targetting us is removed from the request * message (so that the reply can be placed in that position after going through the decrypted * request record). * * Note that this layer-decrypts the build records in-place. * Do not call this more than once for a given message. * * @return the current hop's decrypted record or null on failure */ public BuildRequestRecord decrypt(TunnelBuildMessage msg, Hash ourHash, PrivateKey privKey) { BuildRequestRecord rv = null; int ourHop = -1; long beforeActualDecrypt = 0; long afterActualDecrypt = 0; byte[] ourHashData = ourHash.getData(); long beforeLoop = System.currentTimeMillis(); for (int i = 0; i < msg.getRecordCount(); i++) { EncryptedBuildRecord rec = msg.getRecord(i); int len = BuildRequestRecord.PEER_SIZE; boolean eq = DataHelper.eq(ourHashData, 0, rec.getData(), 0, len); if (eq) { beforeActualDecrypt = System.currentTimeMillis(); try { rv = new BuildRequestRecord(ctx, privKey, rec); afterActualDecrypt = System.currentTimeMillis(); // i2pd bug boolean isBad = SessionKey.INVALID_KEY.equals(rv.readReplyKey()); if (isBad) { if (log.shouldLog(Log.WARN)) log.warn(msg.getUniqueId() + ": Bad reply key: " + rv); ctx.statManager().addRateData("tunnel.buildRequestBadReplyKey", 1); return null; } // The spec says to feed the 32-byte AES-256 reply key into the Bloom filter. // But we were using the first 32 bytes of the encrypted reply. // Fixed in 0.9.24 boolean isDup = _filter.add(rv.getData(), BuildRequestRecord.OFF_REPLY_KEY, 32); if (isDup) { if (log.shouldLog(Log.WARN)) log.warn(msg.getUniqueId() + ": Dup record: " + rv); ctx.statManager().addRateData("tunnel.buildRequestDup", 1); return null; } if (log.shouldLog(Log.DEBUG)) log.debug(msg.getUniqueId() + ": Matching record: " + rv); ourHop = i; // TODO should we keep looking for a second match and fail if found? break; } catch (DataFormatException dfe) { if (log.shouldLog(Log.WARN)) log.warn(msg.getUniqueId() + ": Matching record decrypt failure", dfe); // on the microscopic chance that there's another router // out there with the same first 16 bytes, go around again continue; } } } if (rv == null) { // none of the records matched, b0rk if (log.shouldLog(Log.WARN)) log.warn(msg.getUniqueId() + ": No matching record"); return null; } long beforeEncrypt = System.currentTimeMillis(); SessionKey replyKey = rv.readReplyKey(); byte iv[] = rv.readReplyIV(); for (int i = 0; i < msg.getRecordCount(); i++) { if (i != ourHop) { EncryptedBuildRecord data = msg.getRecord(i); //if (log.shouldLog(Log.DEBUG)) // log.debug("Encrypting record " + i + "/? with replyKey " + replyKey.toBase64() + "/" + Base64.encode(iv)); // encrypt in-place, corrupts SDS byte[] bytes = data.getData(); ctx.aes().encrypt(bytes, 0, bytes, 0, replyKey, iv, 0, EncryptedBuildRecord.LENGTH); } } long afterEncrypt = System.currentTimeMillis(); msg.setRecord(ourHop, null); if (afterEncrypt-beforeLoop > 1000) { if (log.shouldLog(Log.WARN)) log.warn("Slow decryption, total=" + (afterEncrypt-beforeLoop) + " looping=" + (beforeEncrypt-beforeLoop) + " decrypt=" + (afterActualDecrypt-beforeActualDecrypt) + " encrypt=" + (afterEncrypt-beforeEncrypt)); } return rv; } }