package net.i2p.data.i2cp; /* * 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.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Date; import java.util.Map; import java.util.Properties; import net.i2p.I2PAppContext; import net.i2p.crypto.DSAEngine; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.DataStructureImpl; import net.i2p.data.Destination; import net.i2p.data.Signature; import net.i2p.data.SigningPrivateKey; import net.i2p.util.Clock; import net.i2p.util.Log; import net.i2p.util.OrderedProperties; /** * Defines the information a client must provide to create a session * * @author jrandom */ public class SessionConfig extends DataStructureImpl { private Destination _destination; private Signature _signature; private Date _creationDate; private Properties _options; /** * If the client authorized this session more than the specified period ago, * refuse it, since it may be a replay attack. * * Really? See also ClientManager.REQUEST_LEASESET_TIMEOUT. * If I2CP replay attacks are a thing, there's a lot more to do. */ private final static long OFFSET_VALIDITY = 3*60*1000; public SessionConfig() { this(null); } public SessionConfig(Destination dest) { _destination = dest; _creationDate = new Date(Clock.getInstance().now()); } /** * Retrieve the destination for which this session is supposed to connect * * @return Destination for this session */ public Destination getDestination() { return _destination; } /** * Determine when this session was authorized by the destination (so we can * prevent replay attacks) * * @return Date */ public Date getCreationDate() { return _creationDate; } public void setCreationDate(Date date) { _creationDate = date; } /** * Retrieve any configuration options for the session * * @return Properties of this session */ public Properties getOptions() { return _options; } /** * Configure the session with the given options; * keys and values 255 bytes (not chars) max each * * Defaults in SessionConfig options are, in general, NOT honored. * Defaults are not serialized out-of-JVM, and the router does not recognize defaults in-JVM. * Client side must promote defaults to the primary map. * * @param options Properties for this session */ public void setOptions(Properties options) { _options = options; } public Signature getSignature() { return _signature; } public void setSignature(Signature sig) { _signature = sig; } /** * Sign the structure using the supplied private key * * @param signingKey SigningPrivateKey to sign with * @throws DataFormatException */ public void signSessionConfig(SigningPrivateKey signingKey) throws DataFormatException { byte data[] = getBytes(); if (data == null) throw new DataFormatException("Unable to retrieve bytes for signing"); if (signingKey == null) throw new DataFormatException("No signing key"); _signature = DSAEngine.getInstance().sign(data, signingKey); if (_signature == null) throw new DataFormatException("Signature failed with " + signingKey.getType() + " key"); } /** * Verify that the signature matches the destination's signing public key. * * Note that this also returns false if the creation date is too far in the * past or future. See tooOld() and getCreationDate(). * * @return true only if the signature matches */ public boolean verifySignature() { if (getSignature() == null) { //if (_log.shouldLog(Log.WARN)) _log.warn("Signature is null!"); return false; } if (getDestination() == null) { //if (_log.shouldLog(Log.WARN)) _log.warn("Destination is null!"); return false; } if (getCreationDate() == null) { //if (_log.shouldLog(Log.WARN)) _log.warn("Date is null!"); return false; } if (tooOld()) { //if (_log.shouldLog(Log.WARN)) _log.warn("Too old!"); return false; } byte data[] = getBytes(); if (data == null) { //if (_log.shouldLog(Log.WARN)) _log.warn("Bytes could not be found"); return false; } boolean ok = DSAEngine.getInstance().verifySignature(getSignature(), data, getDestination().getSigningPublicKey()); if (!ok) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(SessionConfig.class); if (log.shouldLog(Log.WARN)) log.warn("DSA signature failed!"); } return ok; } /** * Misnamed, could be too old or too far in the future. */ public boolean tooOld() { long now = Clock.getInstance().now(); long earliestValid = now - OFFSET_VALIDITY; long latestValid = now + OFFSET_VALIDITY; if (_creationDate == null) return true; if (_creationDate.getTime() < earliestValid) return true; if (_creationDate.getTime() > latestValid) return true; return false; } private byte[] getBytes() { if (_destination == null) return null; if (_options == null) return null; if (_creationDate == null) return null; ByteArrayOutputStream out = new ByteArrayOutputStream(); try { //_log.debug("PubKey size for destination: " + _destination.getPublicKey().getData().length); //_log.debug("SigningKey size for destination: " + _destination.getSigningPublicKey().getData().length); _destination.writeBytes(out); DataHelper.writeProperties(out, _options, true); // UTF-8 DataHelper.writeDate(out, _creationDate); } catch (IOException ioe) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(SessionConfig.class); log.error("IOError signing", ioe); return null; } catch (DataFormatException dfe) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(SessionConfig.class); log.error("Error writing out the bytes for signing/verification", dfe); return null; } return out.toByteArray(); } public void readBytes(InputStream rawConfig) throws DataFormatException, IOException { _destination = Destination.create(rawConfig); _options = DataHelper.readProperties(rawConfig); _creationDate = DataHelper.readDate(rawConfig); _signature = new Signature(_destination.getSigningPublicKey().getType()); _signature.readBytes(rawConfig); } public void writeBytes(OutputStream out) throws DataFormatException, IOException { if ((_destination == null) || (_options == null) || (_signature == null) || (_creationDate == null)) throw new DataFormatException("Not enough data to create the session config"); _destination.writeBytes(out); DataHelper.writeProperties(out, _options, true); // UTF-8 DataHelper.writeDate(out, _creationDate); _signature.writeBytes(out); } @Override public boolean equals(Object object) { if ((object != null) && (object instanceof SessionConfig)) { SessionConfig cfg = (SessionConfig) object; return DataHelper.eq(getSignature(), cfg.getSignature()) && DataHelper.eq(getDestination(), cfg.getDestination()) && DataHelper.eq(getCreationDate(), cfg.getCreationDate()) && DataHelper.eq(getOptions(), cfg.getOptions()); } return false; } @Override public int hashCode() { return _signature != null ? _signature.hashCode() : 0; } @Override public String toString() { StringBuilder buf = new StringBuilder("[SessionConfig: "); buf.append("\n\tDestination: ").append(getDestination()); buf.append("\n\tSignature: ").append(getSignature()); buf.append("\n\tCreation Date: ").append(getCreationDate()); buf.append("\n\tOptions: #: ").append(_options.size()); Properties sorted = new OrderedProperties(); sorted.putAll(_options); for (Map.Entry<Object, Object> e : sorted.entrySet()) { String key = (String) e.getKey(); String val = (String) e.getValue(); buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]"); } buf.append("]"); return buf.toString(); } }