package net.i2p.router.message; /* * free (adj.): unencumbered; not under the control of others * Written by jrandom in 2003 and released into the public domain * with no warranty of any kind, either expressed or implied. * It probably won't make your computer catch on fire, or eat * your children, but it might. Use at your own risk. * */ import java.util.Set; import net.i2p.crypto.SessionKeyManager; import net.i2p.data.Certificate; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; import net.i2p.data.Payload; import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DataMessage; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.DeliveryInstructions; import net.i2p.data.i2np.DeliveryStatusMessage; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.RouterContext; import net.i2p.router.TunnelInfo; import net.i2p.router.networkdb.kademlia.MessageWrapper; import net.i2p.util.Log; /** * Static methods to create a Garlic Message with one or more cloves, as follows: * * <pre> * * GarlicMessage * Ack Clove (optional) * Tunnel delivery instructions * Garlic Message * Delivery Status Clove * Local delivery instructions * Delivery Status Message * LeaseSet Clove (optional) * Local delivery instructions * Database Store Message * Data Clove (required) * Destination delivery instructions * Data Message * * </pre> * * The low-level construction is in GarlicMessageBuilder. * */ class OutboundClientMessageJobHelper { private static final long ACK_EXTRA_EXPIRATION = 60*1000; /** * Build a garlic message that will be delivered to the router on which the target is located. * Inside the message are two cloves: one containing the payload with instructions for * delivery to the (now local) destination, and the other containing a DeliveryStatusMessage with * instructions for delivery to an inbound tunnel of this router. * * How the DeliveryStatusMessage is wrapped can vary - it can be simply sent to a tunnel (as above), * wrapped in a GarlicMessage and source routed a few hops before being tunneled, source routed the * entire way back, or not wrapped at all - in which case the payload clove contains a SourceRouteBlock * and a request for a reply. * * For now, its just a tunneled DeliveryStatusMessage * * Unused? * * @param wrappedKey output parameter that will be filled with the sessionKey used * @param wrappedTags output parameter that will be filled with the sessionTags used * @param bundledReplyLeaseSet if specified, the given LeaseSet will be packaged with the message (allowing * much faster replies, since their netDb search will return almost instantly) * @param replyTunnel non-null if requireAck is true or bundledReplyLeaseSet is non-null * @param requireAck if true, bundle replyToken in an ack clove * @return garlic, or null if no tunnels were found (or other errors) */ /**** static GarlicMessage createGarlicMessage(RouterContext ctx, long replyToken, long expiration, PublicKey recipientPK, Payload data, Hash from, Destination dest, TunnelInfo replyTunnel, SessionKey wrappedKey, Set<SessionTag> wrappedTags, boolean requireAck, LeaseSet bundledReplyLeaseSet) { PayloadGarlicConfig dataClove = buildDataClove(ctx, data, dest, expiration); return createGarlicMessage(ctx, replyToken, expiration, recipientPK, dataClove, from, dest, replyTunnel, 0, 0, wrappedKey, wrappedTags, requireAck, bundledReplyLeaseSet); } ****/ /** * Allow the app to specify the data clove directly, which enables OutboundClientMessage to resend the * same payload (including expiration and unique id) in different garlics (down different tunnels) * * This is called from OCMOSJ * * @param tagsToSendOverride if > 0, use this instead of skm's default * @param lowTagsOverride if > 0, use this instead of skm's default * @param wrappedKey output parameter that will be filled with the sessionKey used * @param wrappedTags output parameter that will be filled with the sessionTags used * @param replyTunnel non-null if requireAck is true or bundledReplyLeaseSet is non-null * @param requireAck if true, bundle replyToken in an ack clove * @param bundledReplyLeaseSet may be null; if non-null, put it in a clove * @return garlic, or null if no tunnels were found (or other errors) */ static GarlicMessage createGarlicMessage(RouterContext ctx, long replyToken, long expiration, PublicKey recipientPK, PayloadGarlicConfig dataClove, Hash from, Destination dest, TunnelInfo replyTunnel, int tagsToSendOverride, int lowTagsOverride, SessionKey wrappedKey, Set<SessionTag> wrappedTags, boolean requireAck, LeaseSet bundledReplyLeaseSet) { SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(from); if (skm == null) return null; GarlicConfig config = createGarlicConfig(ctx, replyToken, expiration, recipientPK, dataClove, from, dest, replyTunnel, requireAck, bundledReplyLeaseSet, skm); if (config == null) return null; // no use sending tags unless we have a reply token set up already int tagsToSend = replyToken >= 0 ? (tagsToSendOverride > 0 ? tagsToSendOverride : skm.getTagsToSend()) : 0; int lowThreshold = lowTagsOverride > 0 ? lowTagsOverride : skm.getLowThreshold(); GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, config, wrappedKey, wrappedTags, tagsToSend, lowThreshold, skm); return msg; } /** * Make the top-level config, with a data clove, an optional ack clove, and * an optional leaseset clove. * * @param dataClove non-null * @param replyTunnel non-null if requireAck is true or bundledReplyLeaseSet is non-null * @param requireAck if true, bundle replyToken in an ack clove * @param bundledReplyLeaseSet may be null; if non-null, put it in a clove * @param skm encrypt dsm with this skm non-null * @return null on error */ private static GarlicConfig createGarlicConfig(RouterContext ctx, long replyToken, long expiration, PublicKey recipientPK, PayloadGarlicConfig dataClove, Hash from, Destination dest, TunnelInfo replyTunnel, boolean requireAck, LeaseSet bundledReplyLeaseSet, SessionKeyManager skm) { Log log = ctx.logManager().getLog(OutboundClientMessageJobHelper.class); if (replyToken >= 0 && log.shouldLog(Log.DEBUG)) log.debug("Reply token: " + replyToken); GarlicConfig config = new GarlicConfig(); if (requireAck) { // extend the expiration of the return message PayloadGarlicConfig ackClove = buildAckClove(ctx, from, replyTunnel, replyToken, expiration + ACK_EXTRA_EXPIRATION, skm); if (ackClove == null) return null; // no tunnels... TODO carry on anyway? config.addClove(ackClove); } if (bundledReplyLeaseSet != null) { PayloadGarlicConfig leaseSetClove = buildLeaseSetClove(ctx, expiration, bundledReplyLeaseSet); config.addClove(leaseSetClove); } // As of 0.9.2, since the receiver processes them in-order, // put data clove last to speed up the ack, // and get the leaseset stored before handling the data config.addClove(dataClove); config.setCertificate(Certificate.NULL_CERT); config.setDeliveryInstructions(DeliveryInstructions.LOCAL); config.setId(ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE)); config.setExpiration(expiration); // +2*Router.CLOCK_FUDGE_FACTOR); config.setRecipientPublicKey(recipientPK); if (log.shouldLog(Log.INFO)) log.info("Creating garlic config to be encrypted to " + recipientPK + " for destination " + dest.calculateHash().toBase64()); return config; } /** * Build a clove that sends a DeliveryStatusMessage to us. * As of 0.9.12, the DSM is wrapped in a GarlicMessage. * @param skm encrypt dsm with this skm non-null * @return null on error */ private static PayloadGarlicConfig buildAckClove(RouterContext ctx, Hash from, TunnelInfo replyToTunnel, long replyToken, long expiration, SessionKeyManager skm) { Log log = ctx.logManager().getLog(OutboundClientMessageJobHelper.class); if (replyToTunnel == null) { if (log.shouldLog(Log.WARN)) log.warn("Unable to send client message from " + from.toBase64() + ", as there are no inbound tunnels available"); return null; } TunnelId replyToTunnelId = replyToTunnel.getReceiveTunnelId(0); // tunnel id on that gateway Hash replyToTunnelRouter = replyToTunnel.getPeer(0); // inbound tunnel gateway if (log.shouldLog(Log.DEBUG)) log.debug("Ack for the data message will come back along tunnel " + replyToTunnelId + ": " + replyToTunnel); DeliveryInstructions ackInstructions = new DeliveryInstructions(); ackInstructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_TUNNEL); ackInstructions.setRouter(replyToTunnelRouter); ackInstructions.setTunnelId(replyToTunnelId); // defaults //ackInstructions.setDelayRequested(false); //ackInstructions.setDelaySeconds(0); //ackInstructions.setEncrypted(false); PayloadGarlicConfig ackClove = new PayloadGarlicConfig(); ackClove.setCertificate(Certificate.NULL_CERT); ackClove.setDeliveryInstructions(ackInstructions); ackClove.setExpiration(expiration); ackClove.setId(ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE)); DeliveryStatusMessage dsm = buildDSM(ctx, replyToken); GarlicMessage msg = wrapDSM(ctx, skm, dsm); if (msg == null) { if (log.shouldLog(Log.WARN)) log.warn("Failed to wrap ack clove"); return null; } ackClove.setPayload(msg); // this does nothing, the clove is not separately encrypted //ackClove.setRecipient(ctx.router().getRouterInfo()); // defaults //ackClove.setRequestAck(false); //if (log.shouldLog(Log.DEBUG)) // log.debug("Delivery status message is targetting us [" // + ackClove.getRecipient().getIdentity().getHash().toBase64() // + "] via tunnel " + replyToTunnelId.getTunnelId() + " on " // + replyToTunnelRouter.toBase64()); return ackClove; } /** * Make a basic DSM * @since 0.9.12 */ private static DeliveryStatusMessage buildDSM(RouterContext ctx, long replyToken) { DeliveryStatusMessage msg = new DeliveryStatusMessage(ctx); msg.setArrival(ctx.clock().now()); msg.setMessageId(replyToken); return msg; } /** * As of 0.9.12, encrypt to hide it from the target and the return path OBEP and IBGW. * Wrap a DSM in a GarlicMessage, add the fake session to the SKM. * * @param skm encrypt dsm with this skm non-null * @return null on error * @since 0.9.12 */ private static GarlicMessage wrapDSM(RouterContext ctx, SessionKeyManager skm, DeliveryStatusMessage dsm) { // garlic route that DeliveryStatusMessage to ourselves so the endpoints and gateways // can't tell its a test. to simplify this, we encrypt it with a random key and tag, // remembering that key+tag so that we can decrypt it later. this means we can do the // garlic encryption without any ElGamal (yay) MessageWrapper.OneTimeSession sess = MessageWrapper.generateSession(ctx, skm); GarlicMessage msg = MessageWrapper.wrap(ctx, dsm, sess); return msg; } /** * Build a clove that sends the payload to the destination */ private static PayloadGarlicConfig buildDataClove(RouterContext ctx, Payload data, Destination dest, long expiration) { DeliveryInstructions instructions = new DeliveryInstructions(); instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_DESTINATION); instructions.setDestination(dest.calculateHash()); // defaults //instructions.setDelayRequested(false); //instructions.setDelaySeconds(0); //instructions.setEncrypted(false); PayloadGarlicConfig clove = new PayloadGarlicConfig(); clove.setCertificate(Certificate.NULL_CERT); clove.setDeliveryInstructions(instructions); clove.setExpiration(expiration); clove.setId(ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE)); DataMessage msg = new DataMessage(ctx); msg.setData(data.getEncryptedData()); clove.setPayload(msg); // defaults //clove.setRecipientPublicKey(null); //clove.setRequestAck(false); return clove; } /** * Build a clove that stores the leaseSet locally */ private static PayloadGarlicConfig buildLeaseSetClove(RouterContext ctx, long expiration, LeaseSet replyLeaseSet) { PayloadGarlicConfig clove = new PayloadGarlicConfig(); clove.setCertificate(Certificate.NULL_CERT); clove.setDeliveryInstructions(DeliveryInstructions.LOCAL); clove.setExpiration(expiration); clove.setId(ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE)); DatabaseStoreMessage msg = new DatabaseStoreMessage(ctx); msg.setEntry(replyLeaseSet); msg.setMessageExpiration(expiration); clove.setPayload(msg); // defaults //clove.setRecipientPublicKey(null); //clove.setRequestAck(false); return clove; } }