package net.i2p.data; /* * 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.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import net.i2p.I2PAppContext; import net.i2p.crypto.DSAEngine; import net.i2p.util.Clock; import net.i2p.util.Log; import net.i2p.util.RandomSource; /** * Defines the set of leases a destination currently has. * * Support encryption and decryption with a supplied key. * Only the gateways and tunnel IDs in the individual * leases are encrypted. * * WARNING: * Encryption is poorly designed and probably insecure. * Not recommended. * * Encrypted leases are not indicated as such. * The only way to tell a lease is encrypted is to * determine that the listed gateways do not exist. * Routers wishing to decrypt a leaseset must have the * desthash and key in their keyring. * This is required for the local router as well, since * the encryption is done on the client side of I2CP, the * router must decrypt it back again for local usage * (but not for transmission to the floodfills) * * Decrypted leases are only available through the getLease() * method, so that storage and network transmission via * writeBytes() will output the original encrypted * leases and the original leaseset signature. * * Revocation (zero leases) isn't used anywhere. In addition: * - A revoked leaseset has an EarliestLeaseDate of -1, so it will * never be stored successfully. * - Revocation of an encrypted leaseset will explode. * - So having an included signature at all is pointless? * * * @author jrandom */ public class LeaseSet extends DatabaseEntry { private Destination _destination; private PublicKey _encryptionKey; private SigningPublicKey _signingKey; // Keep leases in the order received, or else signature verification will fail! private final List<Lease> _leases; private boolean _receivedAsPublished; private boolean _receivedAsReply; // Store these since isCurrent() and getEarliestLeaseDate() are called frequently private long _firstExpiration; private long _lastExpiration; private List<Lease> _decryptedLeases; private boolean _decrypted; private boolean _checked; // cached byte version private volatile byte _byteified[]; /** * Unlimited before 0.6.3; * 6 as of 0.6.3; * Increased in version 0.9. * * Leasesets larger than 6 should be used with caution, * as each lease adds 44 bytes, and routers older than version 0.9 * will not be able to connect as they will throw an exception in * readBytes(). Also, the churn will be quite rapid, leading to * frequent netdb stores and transmission on existing connections. * * However we increase it now in case some hugely popular eepsite arrives. * Strategies elsewhere in the router to efficiently handle * large leasesets are TBD. */ public static final int MAX_LEASES = 16; private static final int OLD_MAX_LEASES = 6; public LeaseSet() { _leases = new ArrayList<Lease>(2); _firstExpiration = Long.MAX_VALUE; } /** * Same as getEarliestLeaseDate() */ public long getDate() { return getEarliestLeaseDate(); } public KeysAndCert getKeysAndCert() { return _destination; } public int getType() { return KEY_TYPE_LEASESET; } public Destination getDestination() { return _destination; } /** * @throws IllegalStateException if already signed */ public void setDestination(Destination dest) { if (_signature != null) throw new IllegalStateException(); _destination = dest; } public PublicKey getEncryptionKey() { return _encryptionKey; } /** * @throws IllegalStateException if already signed */ public void setEncryptionKey(PublicKey encryptionKey) { if (_signature != null) throw new IllegalStateException(); _encryptionKey = encryptionKey; } /** * The revocation key. * @deprecated unused */ @Deprecated public SigningPublicKey getSigningKey() { return _signingKey; } /** * The revocation key. Unused. * Must be the same type as the Destination's SigningPublicKey. * @throws IllegalArgumentException if different type */ public void setSigningKey(SigningPublicKey key) { if (key != null && _destination != null && key.getType() != _destination.getSigningPublicKey().getType()) throw new IllegalArgumentException("Signing key type mismatch"); _signingKey = key; } /** * If true, we received this LeaseSet by a remote peer publishing it to * us, rather than by searching for it ourselves or locally creating it. * Default false. */ public boolean getReceivedAsPublished() { return _receivedAsPublished; } /** Default false */ public void setReceivedAsPublished(boolean received) { _receivedAsPublished = received; } /** * If true, we received this LeaseSet by searching for it * Default false. * @since 0.7.14 */ public boolean getReceivedAsReply() { return _receivedAsReply; } /** set to true @since 0.7.14 */ public void setReceivedAsReply() { _receivedAsReply = true; } /** * @throws IllegalStateException if already signed */ public void addLease(Lease lease) { if (lease == null) throw new IllegalArgumentException("erm, null lease"); if (lease.getGateway() == null) throw new IllegalArgumentException("erm, lease has no gateway"); if (lease.getTunnelId() == null) throw new IllegalArgumentException("erm, lease has no tunnel"); if (_signature != null) throw new IllegalStateException(); if (_leases.size() >= MAX_LEASES) throw new IllegalArgumentException("Too many leases - max is " + MAX_LEASES); _leases.add(lease); long expire = lease.getEndDate().getTime(); if (expire < _firstExpiration) _firstExpiration = expire; if (expire > _lastExpiration) _lastExpiration = expire; } /** * @return 0-16 * A LeaseSet with no leases is revoked. */ public int getLeaseCount() { if (isEncrypted()) return _leases.size() - 1; else return _leases.size(); } public Lease getLease(int index) { if (isEncrypted()) return _decryptedLeases.get(index); else return _leases.get(index); } /** * Retrieve the end date of the earliest lease included in this leaseSet. * This is the date that should be used in comparisons for leaseSet age - to * determine which LeaseSet was published more recently (later earliestLeaseSetDate * means it was published later) * * @return earliest end date of any lease in the set, or -1 if there are no leases */ public long getEarliestLeaseDate() { if (_leases.isEmpty()) return -1; return _firstExpiration; } /** * Retrieve the end date of the latest lease included in this leaseSet. * This is the date used in isCurrent(). * * @return latest end date of any lease in the set, or 0 if there are no leases * @since 0.9.7 */ public long getLatestLeaseDate() { return _lastExpiration; } /** * Verify that the signature matches the lease set's destination's signing public key. * OR the included revocation key. * * @return true only if the signature matches */ @Override public boolean verifySignature() { if (super.verifySignature()) return true; // Revocation unused (see above) boolean signedByRevoker = DSAEngine.getInstance().verifySignature(_signature, getBytes(), _signingKey); return signedByRevoker; } /** * Verify that the signature matches the lease set's destination's signing public key. * OR the specified revocation key. * * @deprecated revocation unused * @return true only if the signature matches */ @Deprecated public boolean verifySignature(SigningPublicKey signingKey) { if (super.verifySignature()) return true; // Revocation unused (see above) boolean signedByRevoker = DSAEngine.getInstance().verifySignature(_signature, getBytes(), signingKey); return signedByRevoker; } /** * Determine whether ANY lease is currently valid, at least within a given * fudge factor * * @param fudge milliseconds fudge factor to allow between the current time * @return true if there are current leases, false otherwise */ public boolean isCurrent(long fudge) { long now = Clock.getInstance().now(); return _lastExpiration > now - fudge; } protected byte[] getBytes() { if (_byteified != null) return _byteified; if ((_destination == null) || (_encryptionKey == null) || (_signingKey == null)) return null; int len = _destination.size() + PublicKey.KEYSIZE_BYTES // encryptionKey + _signingKey.length() // signingKey + 1 + _leases.size() * 44; // leases ByteArrayOutputStream out = new ByteArrayOutputStream(len); try { _destination.writeBytes(out); _encryptionKey.writeBytes(out); _signingKey.writeBytes(out); out.write((byte) _leases.size()); for (Lease lease : _leases) lease.writeBytes(out); } catch (IOException ioe) { return null; } catch (DataFormatException dfe) { return null; } byte rv[] = out.toByteArray(); // if we are floodfill and this was published to us if (_receivedAsPublished) _byteified = rv; return rv; } /** * This does NOT validate the signature * * @throws IllegalStateException if called more than once or Destination already set */ public void readBytes(InputStream in) throws DataFormatException, IOException { if (_destination != null) throw new IllegalStateException(); _destination = Destination.create(in); _encryptionKey = PublicKey.create(in); // revocation signing key must be same type as the destination signing key _signingKey = new SigningPublicKey(_destination.getSigningPublicKey().getType()); _signingKey.readBytes(in); int numLeases = (int) DataHelper.readLong(in, 1); if (numLeases > MAX_LEASES) throw new DataFormatException("Too many leases - max is " + MAX_LEASES); //_version = DataHelper.readLong(in, 4); for (int i = 0; i < numLeases; i++) { Lease lease = new Lease(); lease.readBytes(in); addLease(lease); } // signature must be same type as the destination signing key _signature = new Signature(_destination.getSigningPublicKey().getType()); _signature.readBytes(in); } /** * This does NOT validate the signature */ public void writeBytes(OutputStream out) throws DataFormatException, IOException { if ((_destination == null) || (_encryptionKey == null) || (_signingKey == null) || (_signature == null)) throw new DataFormatException("Not enough data to write out a LeaseSet"); _destination.writeBytes(out); _encryptionKey.writeBytes(out); _signingKey.writeBytes(out); out.write((byte) _leases.size()); for (Lease lease : _leases) lease.writeBytes(out); _signature.writeBytes(out); } /** * Number of bytes, NOT including signature */ public int size() { return _destination.size() + PublicKey.KEYSIZE_BYTES // encryptionKey + _signingKey.length() // signingKey + 1 // number of leases + _leases.size() * (Hash.HASH_LENGTH + 4 + 8); } @Override public boolean equals(Object object) { if (object == this) return true; if ((object == null) || !(object instanceof LeaseSet)) return false; LeaseSet ls = (LeaseSet) object; return DataHelper.eq(_signature, ls.getSignature()) && DataHelper.eq(_leases, ls._leases) && DataHelper.eq(getEncryptionKey(), ls.getEncryptionKey()) && DataHelper.eq(_signingKey, ls.getSigningKey()) && DataHelper.eq(_destination, ls.getDestination()); } /** the destination has enough randomness in it to use it by itself for speed */ @Override public int hashCode() { if (_destination == null) return 0; return _destination.hashCode(); } @Override public String toString() { StringBuilder buf = new StringBuilder(128); buf.append("[LeaseSet: "); buf.append("\n\tDestination: ").append(_destination); buf.append("\n\tEncryptionKey: ").append(_encryptionKey); buf.append("\n\tSigningKey: ").append(_signingKey); //buf.append("\n\tVersion: ").append(getVersion()); buf.append("\n\tSignature: ").append(_signature); buf.append("\n\tLeases: #").append(getLeaseCount()); for (int i = 0; i < getLeaseCount(); i++) buf.append("\n\t\t").append(getLease(i)); buf.append("]"); return buf.toString(); } private static final int DATA_LEN = Hash.HASH_LENGTH + 4; private static final int IV_LEN = 16; /** * Encrypt the gateway and tunnel ID of each lease, leaving the expire dates unchanged. * This adds an extra dummy lease, because AES data must be padded to 16 bytes. * The fact that it is encrypted is not stored anywhere. * Must be called after all the leases are in place, but before sign(). */ public void encrypt(SessionKey key) { //if (_log.shouldLog(Log.WARN)) // _log.warn("encrypting lease: " + _destination.calculateHash()); try { encryp(key); } catch (DataFormatException dfe) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(LeaseSet.class); log.error("Error encrypting lease: " + _destination.calculateHash(), dfe); } catch (IOException ioe) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(LeaseSet.class); log.error("Error encrypting lease: " + _destination.calculateHash(), ioe); } } /** * - Put the {Gateway Hash, TunnelID} pairs for all the leases in a buffer * - Pad with random data to a multiple of 16 bytes * - Use the first part of the dest's public key as an IV * - Encrypt * - Pad with random data to a multiple of 36 bytes * - Add an extra lease * - Replace the Hash and TunnelID in each Lease */ private void encryp(SessionKey key) throws DataFormatException, IOException { int size = _leases.size(); if (size < 1 || size > MAX_LEASES-1) throw new IllegalArgumentException("Bad number of leases for encryption"); int datalen = ((DATA_LEN * size / 16) + 1) * 16; ByteArrayOutputStream baos = new ByteArrayOutputStream(datalen); for (int i = 0; i < size; i++) { _leases.get(i).getGateway().writeBytes(baos); _leases.get(i).getTunnelId().writeBytes(baos); } // pad out to multiple of 16 with random data before encryption int padlen = datalen - (DATA_LEN * size); byte[] pad = new byte[padlen]; RandomSource.getInstance().nextBytes(pad); baos.write(pad); byte[] iv = new byte[IV_LEN]; System.arraycopy(_destination.getPublicKey().getData(), 0, iv, 0, IV_LEN); byte[] enc = new byte[DATA_LEN * (size + 1)]; I2PAppContext.getGlobalContext().aes().encrypt(baos.toByteArray(), 0, enc, 0, key, iv, datalen); // pad out to multiple of 36 with random data after encryption // (even for 4 leases, where 36*4 is a multiple of 16, we add another, just to be consistent) padlen = enc.length - datalen; RandomSource.getInstance().nextBytes(enc, datalen, padlen); // add the padded lease... Lease padLease = new Lease(); padLease.setEndDate(_leases.get(0).getEndDate()); _leases.add(padLease); // ...and replace all the gateways and tunnel ids ByteArrayInputStream bais = new ByteArrayInputStream(enc); for (int i = 0; i < size+1; i++) { Hash h = new Hash(); h.readBytes(bais); _leases.get(i).setGateway(h); TunnelId t = new TunnelId(); t.readBytes(bais); _leases.get(i).setTunnelId(t); } } /** * Decrypt the leases, except for the last one which is partially padding. * Store the new decrypted leases in a backing store, * and keep the original leases so that verify() still works and the * encrypted leaseset can be sent on to others (via writeBytes()) */ private void decrypt(SessionKey key) throws DataFormatException, IOException { //if (_log.shouldLog(Log.WARN)) // _log.warn("decrypting lease: " + _destination.calculateHash()); int size = _leases.size(); if (size < 2) throw new DataFormatException("Bad number of leases for decryption"); int datalen = DATA_LEN * size; ByteArrayOutputStream baos = new ByteArrayOutputStream(datalen); for (int i = 0; i < size; i++) { _leases.get(i).getGateway().writeBytes(baos); _leases.get(i).getTunnelId().writeBytes(baos); } byte[] iv = new byte[IV_LEN]; System.arraycopy(_destination.getPublicKey().getData(), 0, iv, 0, IV_LEN); int enclen = ((DATA_LEN * (size - 1) / 16) + 1) * 16; byte[] enc = new byte[enclen]; System.arraycopy(baos.toByteArray(), 0, enc, 0, enclen); byte[] dec = new byte[enclen]; I2PAppContext.getGlobalContext().aes().decrypt(enc, 0, dec, 0, key, iv, enclen); ByteArrayInputStream bais = new ByteArrayInputStream(dec); _decryptedLeases = new ArrayList<Lease>(size - 1); for (int i = 0; i < size-1; i++) { Lease l = new Lease(); Hash h = new Hash(); h.readBytes(bais); l.setGateway(h); TunnelId t = new TunnelId(); t.readBytes(bais); l.setTunnelId(t); l.setEndDate(_leases.get(i).getEndDate()); _decryptedLeases.add(l); } } /** * @return true if it was encrypted, and we decrypted it successfully. * Decrypts on first call. */ private synchronized boolean isEncrypted() { if (_decrypted) return true; // If the encryption key is not set yet, it can't have been encrypted yet. // Router-side I2CP sets the destination (but not the encryption key) // on an unsigned LS which is pending signature (and possibly encryption) // by the client, and we don't want to attempt 'decryption' on it. if (_checked || _encryptionKey == null || _destination == null) return false; SessionKey key = I2PAppContext.getGlobalContext().keyRing().get(_destination.calculateHash()); if (key != null) { try { decrypt(key); _decrypted = true; } catch (DataFormatException dfe) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(LeaseSet.class); log.error("Error decrypting lease: " + _destination.calculateHash(), dfe); } catch (IOException ioe) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(LeaseSet.class); log.error("Error decrypting lease: " + _destination.calculateHash(), ioe); } } _checked = true; return _decrypted; } }