package com.limegroup.gnutella.guess; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.util.Arrays; import java.security.SecureRandom; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Abstraction for a Query Key as detailed in the GUESS protocol spec. * Provides: * - encapsulation of (all, LW and non-LW) Query Keys * - generation of Query Keys (hence, it contains the LimeWire QK Algorithm) * * A Query Key is a credential necessary to perform a GUESS Query. A Query Key * instance is immutable. * * QueryKeys make spoofing UDP IP addresses about as difficult as spoofing TCP * IP addresses by forcing some two-way communication before the heavy data * transfer occurs (sending search results). This prevents the use of the * Gnutella network as a huge DDoS botnet. * * If you want to change the underlying generation algorithm, you need to create * a new class that implements QueryKeyGenerator and modify getQueryKey(InetAddress, int) * to use your new QueryKeyGenerator implementation. */ public final class QueryKey { private static final Log LOG = LogFactory.getLog(QueryKey.class); /** * The <tt>QueryKeyGenerator</tt> holding our secret key(s). */ private static QueryKeyGenerator secretKey = null; /** * The <tt>QueryKeyGenerator</tt> used to provide graceful * transition when our secret keys expire. QueryKeys can * still be validated against the old key until the current * key expires and replaces the old key. */ private static QueryKeyGenerator oldSecretKey = null; // TODO: Every 6 hours, move secretKey to oldSecretKey and // generate a new secretKey so that no key is in use for more // than 12 hours. /** As detailed by the GUESS spec. */ public static final int MIN_QK_SIZE_IN_BYTES = 4; /** As detailed by the GUESS spec. */ public static final int MAX_QK_SIZE_IN_BYTES = 16; /** The Query Key. MIN_QK_SIZE_IN_BYTES <=_queryKey.length <= * MAX_QK_SIZE_IN_BYTES */ private byte[] _queryKey; /** * Cached value to make hashCode() much faster. */ private final int _hashCode; static { secretKey = createKeyGenerator(); // Start out with the old and new generators // being identical. oldSecretKey = secretKey; } private QueryKey(byte[] key, boolean prepareForNet) throws IllegalArgumentException { if(!isValidQueryKeyBytes(key)) throw new IllegalArgumentException(); key = (byte[]) key.clone(); if (prepareForNet) { for (int i = key.length - 1; i >= 0; --i) { // The old prepareForNetwork() seemed to leave cobbs encoding to get // of nulls? TODO: is it okay to leave nulls alone? if (key[i] == 0x1c) { key[i] = (byte) (0xFA); } } } // While we have key in the CPU data cache, calculate _hashCode int code = 0x5A5A5A5A; // Mix all bits of key fairly evenly into code for (int i = key.length - 1; i >= 0; --i) { code ^= (0xFF & key[i]); // One-to-one mixing function from RC6 cipher: // f(x) = (2*x*x + x) mod 2**N // We only care about the low-order 32-bits, so there's no // need to use longs to emulate 32-bit unsigned multiply. code = (int) (code * ((code << 1) + 1)); // Left circular shift (rotate) code by 5 bits code = (code >>> 27) | (code << 5); } _queryKey = key; _hashCode = code; } /** Validates that a QueryKey was generated by secretKey (or oldSecretKey) * for the given IP and port. */ public boolean isFor(InetAddress ip, int port) { if (secretKey.checkKeyBytes(_queryKey,ip,port)) { if (LOG.isDebugEnabled()) { LOG.debug("QueryKey valid:"+this); } return true; } // Check if qk was generated by the previous secret key if (oldSecretKey.checkKeyBytes(_queryKey,ip,port)) { if (LOG.isDebugEnabled()) { LOG.debug("QueryKey old but valid:"+this); } return true; } if (LOG.isDebugEnabled()) { LOG.debug("QueryKey corrupt or expired:"+this); } return false; } public boolean equals(Object o) { if (o.hashCode() != _hashCode) return false; if (!(o instanceof QueryKey)) return false; QueryKey other = (QueryKey) o; return Arrays.equals(_queryKey, other._queryKey); } public int hashCode() { return _hashCode; } public void write(OutputStream out) throws IOException { out.write(_queryKey); } /** Returns a String with the QueryKey represented in hexadecimal. */ public String toString() { return "{Query Key: " + (new java.math.BigInteger(1,_queryKey)).toString(16) + "}"; } //-------------------------------------- //--- PUBLIC STATIC CONSTRUCTION METHODS /** * Determines if the bytes are valid for a qkey. */ public static boolean isValidQueryKeyBytes(byte[] key) { return key != null && key.length >= MIN_QK_SIZE_IN_BYTES && key.length <= MAX_QK_SIZE_IN_BYTES; } /** Use this method to construct Query Keys that you get from network * commerce. If you are using this for testing purposes, be aware that * QueryKey in QueryRequests cannot contain the GEM extension delimiter * 0x1c or nulls, so send true as the second param... * * * @param networkQK the bytes you want to make a QueryKey. * @param prepareForNet true to prepare the QueryKey for net transport. */ public static QueryKey getQueryKey(byte[] networkQK, boolean prepareForNet) throws IllegalArgumentException { return new QueryKey(networkQK, prepareForNet); } /** Generates a QueryKey for a given IP:Port combo. * For a given IP:Port combo, using a different SecretKey and/or SecretPad * will result in a different QueryKey. The return value is constructed * with prepareForNet equal to true. * * @param ip the IP address of the other node * @param port the port of the other node */ public static QueryKey getQueryKey(InetAddress ip, int port) { return new QueryKey(secretKey.getKeyBytes(ip,port), true); } /** Generates a QueryKey for a given IP:Port combo. * For a given IP:Port combo, using a different QueryKeyGenerator * will result in a different QueryKey. The return value is constructed * with prepareForNet equal to true. * * @param ip the IP address of the other node * @param port the port of the other node */ public static QueryKey getQueryKey(InetAddress ip, int port, QueryKeyGenerator keyGen) { return new QueryKey(keyGen.getKeyBytes(ip, port), true); } // ------------------------------------------- // -- FACTORY METHOD /** Returns a new QueryKeyGenerator with random secret key(s), * using the default QueryKeyGenerator implementation. */ public static QueryKeyGenerator createKeyGenerator() { return new TEAQueryKeyGenerator(); } }