forked from I2P_Developers/i2p.i2p
Router: Add support for building tunnels through ECIES routers (proposals 152,156)
Preliminary, proposal not finalized, subject to change Not yet compatibility tested with other implementations Add peers to match requested length for explicitPeers remove commented out code log tweaks
This commit is contained in:
@ -302,19 +302,22 @@ public class DataHelper {
|
||||
*
|
||||
* Properties from the defaults table of props (if any) are not written out by this method.
|
||||
*
|
||||
* @deprecated unused
|
||||
*
|
||||
* @param target returned array as specified in data structure spec
|
||||
* @param props source may be null
|
||||
* @return new offset
|
||||
* @throws DataFormatException if any string is over 255 bytes long, or if the total length
|
||||
* (not including the two length bytes) is greater than 65535 bytes.
|
||||
* @since un-deprecated in 0.9.48
|
||||
*/
|
||||
@Deprecated
|
||||
public static int toProperties(byte target[], int offset, Properties props) throws DataFormatException, IOException {
|
||||
if (props != null) {
|
||||
OrderedProperties p = new OrderedProperties();
|
||||
p.putAll(props);
|
||||
if (props != null && !props.isEmpty()) {
|
||||
Properties p;
|
||||
if (props instanceof OrderedProperties) {
|
||||
p = props;
|
||||
} else {
|
||||
p = new OrderedProperties();
|
||||
p.putAll(props);
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(p.size() * 64);
|
||||
for (Map.Entry<Object, Object> entry : p.entrySet()) {
|
||||
String key = (String) entry.getKey();
|
||||
|
@ -1,3 +1,12 @@
|
||||
2020-10-03 zzz
|
||||
* Router: Support building tunnels through ECIES routers (proposal 152)
|
||||
|
||||
2020-09-28 zzz
|
||||
* Router: Don't unregister a message without a selector (ticket #2771)
|
||||
|
||||
2020-09-27 zzz
|
||||
* Streaming: Fix tag option handling
|
||||
|
||||
2020-09-26 zzz
|
||||
* JBigI: GMP 6.2.0 for linux 64 bit Zen and Zen2 (ticket #1869)
|
||||
|
||||
|
@ -130,16 +130,19 @@ public class HandshakeState implements Destroyable, Cloneable {
|
||||
|
||||
public static final String protocolName = "Noise_XKaesobfse+hs2+hs3_25519_ChaChaPoly_SHA256";
|
||||
public static final String protocolName2 = "Noise_IKelg2+hs2_25519_ChaChaPoly_SHA256";
|
||||
public static final String protocolName3 = "Noise_N_25519_ChaChaPoly_SHA256";
|
||||
private static final String prefix;
|
||||
private final String patternId;
|
||||
public static final String PATTERN_ID_XK = "XK";
|
||||
public static final String PATTERN_ID_IK = "IK";
|
||||
public static final String PATTERN_ID_N = "N";
|
||||
private static String dh;
|
||||
private static final String cipher;
|
||||
private static final String hash;
|
||||
private final short[] pattern;
|
||||
private static final short[] PATTERN_XK;
|
||||
private static final short[] PATTERN_IK;
|
||||
private static final short[] PATTERN_N;
|
||||
|
||||
static {
|
||||
// Parse the protocol name into its components.
|
||||
@ -169,13 +172,21 @@ public class HandshakeState implements Destroyable, Cloneable {
|
||||
PATTERN_IK = Pattern.lookup(id);
|
||||
if (PATTERN_IK == null)
|
||||
throw new IllegalArgumentException("Handshake pattern is not recognized");
|
||||
// N
|
||||
components = protocolName3.split("_");
|
||||
id = components[1];
|
||||
if (!PATTERN_ID_N.equals(id))
|
||||
throw new IllegalArgumentException();
|
||||
PATTERN_N = Pattern.lookup(id);
|
||||
if (PATTERN_N == null)
|
||||
throw new IllegalArgumentException("Handshake pattern is not recognized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Noise handshake.
|
||||
* Noise protocol name is hardcoded.
|
||||
*
|
||||
* @param patternId XK or IK
|
||||
* @param patternId XK, IK, or N
|
||||
* @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER.
|
||||
* @param xdh The key pair factory for ephemeral keys
|
||||
*
|
||||
@ -192,6 +203,8 @@ public class HandshakeState implements Destroyable, Cloneable {
|
||||
pattern = PATTERN_XK;
|
||||
else if (patternId.equals(PATTERN_ID_IK))
|
||||
pattern = PATTERN_IK;
|
||||
else if (patternId.equals(PATTERN_ID_N))
|
||||
pattern = PATTERN_N;
|
||||
else
|
||||
throw new IllegalArgumentException("Handshake pattern is not recognized");
|
||||
short flags = pattern[0];
|
||||
|
@ -54,6 +54,15 @@ class Pattern {
|
||||
public static final short FLAG_REMOTE_HYBRID = 0x1000;
|
||||
public static final short FLAG_REMOTE_HYBRID_REQ = 0x2000;
|
||||
|
||||
private static final short[] noise_pattern_N = {
|
||||
FLAG_LOCAL_EPHEMERAL |
|
||||
FLAG_REMOTE_STATIC |
|
||||
FLAG_REMOTE_REQUIRED,
|
||||
|
||||
E,
|
||||
ES
|
||||
};
|
||||
|
||||
private static final short[] noise_pattern_XK = {
|
||||
FLAG_LOCAL_STATIC |
|
||||
FLAG_LOCAL_EPHEMERAL |
|
||||
@ -96,7 +105,9 @@ class Pattern {
|
||||
*/
|
||||
public static short[] lookup(String name)
|
||||
{
|
||||
if (name.equals("XK"))
|
||||
if (name.equals("N"))
|
||||
return noise_pattern_N;
|
||||
else if (name.equals("XK"))
|
||||
return noise_pattern_XK;
|
||||
else if (name.equals("IK"))
|
||||
return noise_pattern_IK;
|
||||
|
@ -39,10 +39,12 @@ class SymmetricState implements Destroyable, Cloneable {
|
||||
// precalculated hash of the Noise name
|
||||
private static final byte[] INIT_HASH_XK;
|
||||
private static final byte[] INIT_HASH_IK;
|
||||
private static final byte[] INIT_HASH_N;
|
||||
|
||||
static {
|
||||
INIT_HASH_XK = initHash(HandshakeState.protocolName);
|
||||
INIT_HASH_IK = initHash(HandshakeState.protocolName2);
|
||||
INIT_HASH_N = initHash(HandshakeState.protocolName3);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -102,6 +104,8 @@ class SymmetricState implements Destroyable, Cloneable {
|
||||
initHash = INIT_HASH_XK;
|
||||
else if (patternId.equals(HandshakeState.PATTERN_ID_IK))
|
||||
initHash = INIT_HASH_IK;
|
||||
else if (patternId.equals(HandshakeState.PATTERN_ID_N))
|
||||
initHash = INIT_HASH_N;
|
||||
else
|
||||
throw new IllegalArgumentException("Handshake pattern is not recognized");
|
||||
System.arraycopy(initHash, 0, h, 0, hashLength);
|
||||
|
@ -1,8 +1,15 @@
|
||||
package net.i2p.data.i2np;
|
||||
|
||||
import java.util.Date;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.southernstorm.noise.protocol.HandshakeState;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.crypto.EncType;
|
||||
import net.i2p.crypto.KeyFactory;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.data.DataFormatException;
|
||||
@ -11,8 +18,18 @@ import net.i2p.data.Hash;
|
||||
import net.i2p.data.PrivateKey;
|
||||
import net.i2p.data.PublicKey;
|
||||
import net.i2p.data.SessionKey;
|
||||
import net.i2p.router.RouterContext;
|
||||
|
||||
/**
|
||||
* As of 0.9.48, supports two formats.
|
||||
* The original 222-byte ElGamal format and the new 464-byte ECIES format.
|
||||
* See proposal 152 for details on the new format.
|
||||
*
|
||||
* None of the readXXX() calls are cached. For efficiency,
|
||||
* they should only be called once.
|
||||
*
|
||||
* Original ElGamal format:
|
||||
*
|
||||
* Holds the unencrypted 222-byte tunnel request record,
|
||||
* with a constructor for ElGamal decryption and a method for ElGamal encryption.
|
||||
* Iterative AES encryption/decryption is done elsewhere.
|
||||
@ -39,9 +56,45 @@ import net.i2p.data.SessionKey;
|
||||
* bytes 16-527: ElGamal encrypted block (discarding zero bytes at elg[0] and elg[257])
|
||||
* </pre>
|
||||
*
|
||||
* New ECIES format, ref: proposal 152:
|
||||
*
|
||||
* Holds the unencrypted 464-byte tunnel request record,
|
||||
* with a constructor for ECIES decryption and a method for ECIES encryption.
|
||||
* Iterative AES encryption/decryption is done elsewhere.
|
||||
*
|
||||
* Cleartext:
|
||||
* <pre>
|
||||
* bytes 0-3: tunnel ID to receive messages as, nonzero
|
||||
* bytes 4-7: next tunnel ID, nonzero
|
||||
* bytes 8-39: next router identity hash
|
||||
* bytes 40-71: AES-256 tunnel layer key
|
||||
* bytes 72-103: AES-256 tunnel IV key
|
||||
* bytes 104-135: AES-256 reply key
|
||||
* bytes 136-151: AES-256 reply IV
|
||||
* byte 152: flags
|
||||
* bytes 153-155: more flags, unused, set to 0 for compatibility
|
||||
* bytes 156-159: request time (in minutes since the epoch, rounded down)
|
||||
* bytes 160-163: request expiration (in seconds since creation)
|
||||
* bytes 164-167: next message ID
|
||||
* bytes 168-x: tunnel build options (Mapping)
|
||||
* bytes x-x: other data as implied by flags or options
|
||||
* bytes x-463: random padding
|
||||
* </pre>
|
||||
*
|
||||
* Encrypted:
|
||||
* <pre>
|
||||
* bytes 0-15: Hop's truncated identity hash
|
||||
* bytes 16-47: Sender's ephemeral X25519 public key
|
||||
* bytes 48-511: ChaCha20 encrypted BuildRequestRecord
|
||||
* bytes 512-527: Poly1305 MAC
|
||||
* </pre>
|
||||
*
|
||||
*/
|
||||
public class BuildRequestRecord {
|
||||
private final byte[] _data;
|
||||
private final boolean _isEC;
|
||||
private SessionKey _chachaReplyKey;
|
||||
private byte[] _chachaReplyAD;
|
||||
|
||||
/**
|
||||
* If set in the flag byte, any peer may send a message into this tunnel, but if
|
||||
@ -61,10 +114,11 @@ public class BuildRequestRecord {
|
||||
public static final int PEER_SIZE = 16;
|
||||
|
||||
/**
|
||||
* @return 222 bytes, non-null
|
||||
* @return 222 (ElG) or 464 (ECIES) bytes, non-null
|
||||
*/
|
||||
public byte[] getData() { return _data; }
|
||||
|
||||
// Original ElGamal format
|
||||
private static final int OFF_RECV_TUNNEL = 0;
|
||||
private static final int OFF_OUR_IDENT = OFF_RECV_TUNNEL + 4;
|
||||
private static final int OFF_SEND_TUNNEL = OFF_OUR_IDENT + Hash.HASH_LENGTH;
|
||||
@ -79,7 +133,23 @@ public class BuildRequestRecord {
|
||||
private static final int PADDING_SIZE = 29;
|
||||
// 222
|
||||
private static final int LENGTH = OFF_SEND_MSG_ID + 4 + PADDING_SIZE;
|
||||
|
||||
// New ECIES format
|
||||
private static final int OFF_SEND_TUNNEL_EC = OFF_OUR_IDENT;
|
||||
private static final int OFF_SEND_IDENT_EC = OFF_SEND_TUNNEL_EC + 4;
|
||||
private static final int OFF_LAYER_KEY_EC = OFF_SEND_IDENT_EC + Hash.HASH_LENGTH;
|
||||
private static final int OFF_IV_KEY_EC = OFF_LAYER_KEY_EC + SessionKey.KEYSIZE_BYTES;
|
||||
public static final int OFF_REPLY_KEY_EC = OFF_IV_KEY_EC + SessionKey.KEYSIZE_BYTES;
|
||||
private static final int OFF_REPLY_IV_EC = OFF_REPLY_KEY_EC + SessionKey.KEYSIZE_BYTES;
|
||||
private static final int OFF_FLAG_EC = OFF_REPLY_IV_EC + IV_SIZE;
|
||||
private static final int OFF_REQ_TIME_EC = OFF_FLAG_EC + 4;
|
||||
private static final int OFF_SEND_MSG_ID_EC = OFF_REQ_TIME_EC + 4;
|
||||
private static final int OFF_OPTIONS = OFF_SEND_MSG_ID_EC + 4;
|
||||
private static final int LENGTH_EC = 464;
|
||||
private static final int MAX_OPTIONS_LENGTH = LENGTH_EC - OFF_OPTIONS; // includes options length
|
||||
|
||||
private static final boolean TEST = false;
|
||||
private static KeyFactory TESTKF;
|
||||
|
||||
/** what tunnel ID should this receive messages on */
|
||||
public long readReceiveTunnelId() {
|
||||
@ -91,7 +161,8 @@ public class BuildRequestRecord {
|
||||
* this specifies the tunnel ID to which the reply should be sent.
|
||||
*/
|
||||
public long readNextTunnelId() {
|
||||
return DataHelper.fromLong(_data, OFF_SEND_TUNNEL, 4);
|
||||
int off = _isEC ? OFF_SEND_TUNNEL_EC : OFF_SEND_TUNNEL;
|
||||
return DataHelper.fromLong(_data, off, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,10 +170,8 @@ public class BuildRequestRecord {
|
||||
* the gateway to which the reply should be sent.
|
||||
*/
|
||||
public Hash readNextIdentity() {
|
||||
//byte rv[] = new byte[Hash.HASH_LENGTH];
|
||||
//System.arraycopy(_data, OFF_SEND_IDENT, rv, 0, Hash.HASH_LENGTH);
|
||||
//return new Hash(rv);
|
||||
return Hash.create(_data, OFF_SEND_IDENT);
|
||||
int off = _isEC ? OFF_SEND_IDENT_EC : OFF_SEND_IDENT;
|
||||
return Hash.create(_data, off);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,7 +179,8 @@ public class BuildRequestRecord {
|
||||
*/
|
||||
public SessionKey readLayerKey() {
|
||||
byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
|
||||
System.arraycopy(_data, OFF_LAYER_KEY, key, 0, SessionKey.KEYSIZE_BYTES);
|
||||
int off = _isEC ? OFF_LAYER_KEY_EC : OFF_LAYER_KEY;
|
||||
System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES);
|
||||
return new SessionKey(key);
|
||||
}
|
||||
|
||||
@ -119,7 +189,8 @@ public class BuildRequestRecord {
|
||||
*/
|
||||
public SessionKey readIVKey() {
|
||||
byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
|
||||
System.arraycopy(_data, OFF_IV_KEY, key, 0, SessionKey.KEYSIZE_BYTES);
|
||||
int off = _isEC ? OFF_IV_KEY_EC : OFF_IV_KEY;
|
||||
System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES);
|
||||
return new SessionKey(key);
|
||||
}
|
||||
|
||||
@ -128,7 +199,8 @@ public class BuildRequestRecord {
|
||||
*/
|
||||
public SessionKey readReplyKey() {
|
||||
byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
|
||||
System.arraycopy(_data, OFF_REPLY_KEY, key, 0, SessionKey.KEYSIZE_BYTES);
|
||||
int off = _isEC ? OFF_REPLY_KEY_EC : OFF_REPLY_KEY;
|
||||
System.arraycopy(_data, off, key, 0, SessionKey.KEYSIZE_BYTES);
|
||||
return new SessionKey(key);
|
||||
}
|
||||
|
||||
@ -137,7 +209,8 @@ public class BuildRequestRecord {
|
||||
*/
|
||||
public byte[] readReplyIV() {
|
||||
byte iv[] = new byte[IV_SIZE];
|
||||
System.arraycopy(_data, OFF_REPLY_IV, iv, 0, IV_SIZE);
|
||||
int off = _isEC ? OFF_REPLY_IV_EC : OFF_REPLY_IV;
|
||||
System.arraycopy(_data, off, iv, 0, IV_SIZE);
|
||||
return iv;
|
||||
}
|
||||
|
||||
@ -147,7 +220,8 @@ public class BuildRequestRecord {
|
||||
*
|
||||
*/
|
||||
public boolean readIsInboundGateway() {
|
||||
return (_data[OFF_FLAG] & FLAG_UNRESTRICTED_PREV) != 0;
|
||||
int off = _isEC ? OFF_FLAG_EC : OFF_FLAG;
|
||||
return (_data[off] & FLAG_UNRESTRICTED_PREV) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -155,14 +229,18 @@ public class BuildRequestRecord {
|
||||
* fields refer to where the reply should be sent.
|
||||
*/
|
||||
public boolean readIsOutboundEndpoint() {
|
||||
return (_data[OFF_FLAG] & FLAG_OUTBOUND_ENDPOINT) != 0;
|
||||
int off = _isEC ? OFF_FLAG_EC : OFF_FLAG;
|
||||
return (_data[off] & FLAG_OUTBOUND_ENDPOINT) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time that the request was sent (ms), truncated to the nearest hour.
|
||||
* For ElGamal, time that the request was sent (ms), truncated to the nearest hour.
|
||||
* For ECIES, time that the request was sent (ms), truncated to the nearest minute.
|
||||
* This ignores leap seconds.
|
||||
*/
|
||||
public long readRequestTime() {
|
||||
if (_isEC)
|
||||
return DataHelper.fromLong(_data, OFF_REQ_TIME_EC, 4) * (60 * 1000L);
|
||||
return DataHelper.fromLong(_data, OFF_REQ_TIME, 4) * (60 * 60 * 1000L);
|
||||
}
|
||||
|
||||
@ -171,7 +249,26 @@ public class BuildRequestRecord {
|
||||
* this specifies the message ID with which the reply should be sent.
|
||||
*/
|
||||
public long readReplyMessageId() {
|
||||
return DataHelper.fromLong(_data, OFF_SEND_MSG_ID, 4);
|
||||
int off = _isEC ? OFF_SEND_MSG_ID_EC : OFF_SEND_MSG_ID;
|
||||
return DataHelper.fromLong(_data, off, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* ECIES only.
|
||||
* @return null for ElGamal or on error
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public Properties readOptions() {
|
||||
if (!_isEC)
|
||||
return null;
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(_data, OFF_OPTIONS, MAX_OPTIONS_LENGTH);
|
||||
try {
|
||||
return DataHelper.readProperties(in, null);
|
||||
} catch (DataFormatException dfe) {
|
||||
return null;
|
||||
} catch (IOException ioe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -180,9 +277,14 @@ public class BuildRequestRecord {
|
||||
* bytes 15-527: ElGamal-2048 encrypted block
|
||||
* </pre>
|
||||
*
|
||||
* ElGamal only
|
||||
*
|
||||
* @return non-null
|
||||
*/
|
||||
public EncryptedBuildRecord encryptRecord(I2PAppContext ctx, PublicKey toKey, Hash toPeer) {
|
||||
EncType type = toKey.getType();
|
||||
if (type != EncType.ELGAMAL_2048)
|
||||
throw new IllegalArgumentException();
|
||||
byte[] out = new byte[EncryptedBuildRecord.LENGTH];
|
||||
System.arraycopy(toPeer.getData(), 0, out, 0, PEER_SIZE);
|
||||
byte encrypted[] = ctx.elGamalEngine().encrypt(_data, toKey);
|
||||
@ -193,6 +295,63 @@ public class BuildRequestRecord {
|
||||
return new EncryptedBuildRecord(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the record to the specified peer. ECIES only.
|
||||
* The ChaCha reply key and IV will be available via the getters
|
||||
* after this call.
|
||||
* See class javadocs for format.
|
||||
* See proposal 152.
|
||||
*
|
||||
* @return non-null
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public EncryptedBuildRecord encryptECIESRecord(RouterContext ctx, PublicKey toKey, Hash toPeer) {
|
||||
EncType type = toKey.getType();
|
||||
if (type != EncType.ECIES_X25519)
|
||||
throw new IllegalArgumentException();
|
||||
byte[] out = new byte[EncryptedBuildRecord.LENGTH];
|
||||
System.arraycopy(toPeer.getData(), 0, out, 0, PEER_SIZE);
|
||||
HandshakeState state = null;
|
||||
try {
|
||||
KeyFactory kf = TEST ? TESTKF : ctx.commSystem().getXDHFactory();
|
||||
state = new HandshakeState(HandshakeState.PATTERN_ID_N, HandshakeState.INITIATOR, kf);
|
||||
state.getRemotePublicKey().setPublicKey(toKey.getData(), 0);
|
||||
state.start();
|
||||
state.writeMessage(out, PEER_SIZE, _data, 0, LENGTH_EC);
|
||||
EncryptedBuildRecord rv = new EncryptedBuildRecord(out);
|
||||
_chachaReplyKey = new SessionKey(state.getChainingKey());
|
||||
_chachaReplyAD = new byte[32];
|
||||
System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32);
|
||||
return rv;
|
||||
} catch (GeneralSecurityException gse) {
|
||||
throw new IllegalStateException("failed", gse);
|
||||
} finally {
|
||||
if (state != null)
|
||||
state.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid after calling encryptECIESRecord() or after the decrypting constructor
|
||||
* with an ECIES private key.
|
||||
* See proposal 152.
|
||||
*
|
||||
* @return null if no ECIES encrypt/decrypt operation was performed
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public SessionKey getChaChaReplyKey() { return _chachaReplyKey; }
|
||||
|
||||
/**
|
||||
* Valid after calling encryptECIESRecord() or after the decrypting constructor
|
||||
* with an ECIES private key.
|
||||
* See proposal 152.
|
||||
*
|
||||
* @return null if no ECIES encrypt/decrypt operation was performed
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public byte[] getChaChaReplyAD() { return _chachaReplyAD; }
|
||||
|
||||
|
||||
/**
|
||||
* Decrypt the data from the specified record, writing the decrypted record into this instance's
|
||||
* data buffer
|
||||
@ -200,28 +359,61 @@ public class BuildRequestRecord {
|
||||
* Caller MUST check that first 16 bytes of our hash matches first 16 bytes of encryptedRecord
|
||||
* before calling this. Not checked here.
|
||||
*
|
||||
* The ChaCha reply key and IV will be available via the getters
|
||||
* after this call if ourKey is ECIES.
|
||||
*
|
||||
* @throws DataFormatException on decrypt fail
|
||||
* @since 0.9.18, was decryptRecord()
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public BuildRequestRecord(I2PAppContext ctx, PrivateKey ourKey,
|
||||
public BuildRequestRecord(RouterContext ctx, PrivateKey ourKey,
|
||||
EncryptedBuildRecord encryptedRecord) throws DataFormatException {
|
||||
byte decrypted[];
|
||||
EncType type = ourKey.getType();
|
||||
if (type == EncType.ELGAMAL_2048) {
|
||||
byte preDecrypt[] = new byte[514];
|
||||
System.arraycopy(encryptedRecord.getData(), PEER_SIZE, preDecrypt, 1, 256);
|
||||
System.arraycopy(encryptedRecord.getData(), PEER_SIZE + 256, preDecrypt, 258, 256);
|
||||
byte decrypted[] = ctx.elGamalEngine().decrypt(preDecrypt, ourKey);
|
||||
if (decrypted != null) {
|
||||
_data = decrypted;
|
||||
} else {
|
||||
throw new DataFormatException("decrypt fail");
|
||||
decrypted = ctx.elGamalEngine().decrypt(preDecrypt, ourKey);
|
||||
_isEC = false;
|
||||
} else if (type == EncType.ECIES_X25519) {
|
||||
HandshakeState state = null;
|
||||
try {
|
||||
KeyFactory kf = TEST ? TESTKF : ctx.commSystem().getXDHFactory();
|
||||
state = new HandshakeState(HandshakeState.PATTERN_ID_N, HandshakeState.RESPONDER, kf);
|
||||
state.getLocalKeyPair().setPublicKey(ourKey.toPublic().getData(), 0);
|
||||
state.getLocalKeyPair().setPrivateKey(ourKey.getData(), 0);
|
||||
state.start();
|
||||
decrypted = new byte[LENGTH_EC];
|
||||
state.readMessage(encryptedRecord.getData(), PEER_SIZE, EncryptedBuildRecord.LENGTH - PEER_SIZE,
|
||||
decrypted, 0);
|
||||
_chachaReplyKey = new SessionKey(state.getChainingKey());
|
||||
_chachaReplyAD = new byte[32];
|
||||
System.arraycopy(state.getHandshakeHash(), 0, _chachaReplyAD, 0, 32);
|
||||
} catch (GeneralSecurityException gse) {
|
||||
throw new DataFormatException("decrypt fail", gse);
|
||||
} finally {
|
||||
if (state != null)
|
||||
state.destroy();
|
||||
}
|
||||
_isEC = true;
|
||||
} else {
|
||||
throw new DataFormatException("Unsupported EncType " + type);
|
||||
}
|
||||
if (decrypted != null) {
|
||||
_data = decrypted;
|
||||
} else {
|
||||
throw new DataFormatException("decrypt fail");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate this instance with data. A new buffer is created to contain the data, with the
|
||||
* necessary randomized padding.
|
||||
*
|
||||
* ElGamal only. ECIES constructor below.
|
||||
*
|
||||
* @param receiveTunnelId tunnel the current hop will receive messages on
|
||||
* @param peer current hop's identity
|
||||
* @param peer current hop's identity, unused, no read() method
|
||||
* @param nextTunnelId id for the next hop, or where we send the reply (if we are the outbound endpoint)
|
||||
* @param nextHop next hop's identity, or where we send the reply (if we are the outbound endpoint)
|
||||
* @param nextMsgId message ID to use when sending on to the next hop (or for the reply)
|
||||
@ -238,6 +430,7 @@ public class BuildRequestRecord {
|
||||
boolean isOutEndpoint) {
|
||||
byte buf[] = new byte[LENGTH];
|
||||
_data = buf;
|
||||
_isEC = false;
|
||||
|
||||
/* bytes 0-3: tunnel ID to receive messages as
|
||||
* bytes 4-35: local router identity hash
|
||||
@ -272,10 +465,61 @@ public class BuildRequestRecord {
|
||||
DataHelper.toLong(buf, OFF_REQ_TIME, 4, truncatedHour);
|
||||
DataHelper.toLong(buf, OFF_SEND_MSG_ID, 4, nextMsgId);
|
||||
ctx.random().nextBytes(buf, OFF_SEND_MSG_ID+4, PADDING_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate this instance with data. A new buffer is created to contain the data, with the
|
||||
* necessary randomized padding.
|
||||
*
|
||||
* ECIES only. ElGamal constructor above.
|
||||
*
|
||||
* @param receiveTunnelId tunnel the current hop will receive messages on
|
||||
* @param nextTunnelId id for the next hop, or where we send the reply (if we are the outbound endpoint)
|
||||
* @param nextHop next hop's identity, or where we send the reply (if we are the outbound endpoint)
|
||||
* @param nextMsgId message ID to use when sending on to the next hop (or for the reply)
|
||||
* @param layerKey tunnel layer key to be used by the peer
|
||||
* @param ivKey tunnel IV key to be used by the peer
|
||||
* @param replyKey key to be used when encrypting the reply to this build request
|
||||
* @param iv iv to be used when encrypting the reply to this build request
|
||||
* @param isInGateway are we the gateway of an inbound tunnel?
|
||||
* @param isOutEndpoint are we the endpoint of an outbound tunnel?
|
||||
* @param options 296 bytes max when serialized
|
||||
* @since 0.9.48
|
||||
* @throws IllegalArgumentException if options too long
|
||||
*/
|
||||
public BuildRequestRecord(I2PAppContext ctx, long receiveTunnelId, long nextTunnelId, Hash nextHop, long nextMsgId,
|
||||
SessionKey layerKey, SessionKey ivKey, SessionKey replyKey, byte iv[], boolean isInGateway,
|
||||
boolean isOutEndpoint, Properties options) {
|
||||
byte buf[] = new byte[LENGTH_EC];
|
||||
_data = buf;
|
||||
_isEC = true;
|
||||
|
||||
byte wroteIV[] = readReplyIV();
|
||||
if (!DataHelper.eq(iv, wroteIV))
|
||||
throw new RuntimeException("foo");
|
||||
DataHelper.toLong(buf, OFF_RECV_TUNNEL, 4, receiveTunnelId);
|
||||
DataHelper.toLong(buf, OFF_SEND_TUNNEL_EC, 4, nextTunnelId);
|
||||
System.arraycopy(nextHop.getData(), 0, buf, OFF_SEND_IDENT_EC, Hash.HASH_LENGTH);
|
||||
System.arraycopy(layerKey.getData(), 0, buf, OFF_LAYER_KEY_EC, SessionKey.KEYSIZE_BYTES);
|
||||
System.arraycopy(ivKey.getData(), 0, buf, OFF_IV_KEY_EC, SessionKey.KEYSIZE_BYTES);
|
||||
System.arraycopy(replyKey.getData(), 0, buf, OFF_REPLY_KEY_EC, SessionKey.KEYSIZE_BYTES);
|
||||
System.arraycopy(iv, 0, buf, OFF_REPLY_IV_EC, IV_SIZE);
|
||||
if (isInGateway)
|
||||
buf[OFF_FLAG_EC] |= FLAG_UNRESTRICTED_PREV;
|
||||
else if (isOutEndpoint)
|
||||
buf[OFF_FLAG_EC] |= FLAG_OUTBOUND_ENDPOINT;
|
||||
long truncatedMinute = ctx.clock().now();
|
||||
// prevent hop identification at top of the minute
|
||||
truncatedMinute -= ctx.random().nextInt(2048);
|
||||
// this ignores leap seconds
|
||||
truncatedMinute /= (60*1000L);
|
||||
DataHelper.toLong(buf, OFF_REQ_TIME_EC, 4, truncatedMinute);
|
||||
DataHelper.toLong(buf, OFF_SEND_MSG_ID_EC, 4, nextMsgId);
|
||||
try {
|
||||
int off = DataHelper.toProperties(buf, OFF_OPTIONS, options);
|
||||
int sz = LENGTH_EC - off;
|
||||
if (sz > 0)
|
||||
ctx.random().nextBytes(buf, off, sz);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("options", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -284,7 +528,8 @@ public class BuildRequestRecord {
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder buf = new StringBuilder(256);
|
||||
buf.append("BRR ");
|
||||
buf.append(_isEC ? "ECIES" : "ElGamal");
|
||||
buf.append(" BRR ");
|
||||
boolean isIBGW = readIsInboundGateway();
|
||||
boolean isOBEP = readIsOutboundEndpoint();
|
||||
if (isIBGW) {
|
||||
@ -301,10 +546,57 @@ public class BuildRequestRecord {
|
||||
.append(" IV key: ").append(readIVKey())
|
||||
.append(" reply key: ").append(readReplyKey())
|
||||
.append(" reply IV: ").append(Base64.encode(readReplyIV()))
|
||||
.append(" hour: ").append(new Date(readRequestTime()))
|
||||
.append(" time: ").append(DataHelper.formatTime(readRequestTime()))
|
||||
.append(" reply msg id: ").append(readReplyMessageId());
|
||||
if (_isEC) {
|
||||
buf.append(" options: ").append(readOptions());
|
||||
if (_chachaReplyKey != null) {
|
||||
buf.append(" chacha reply key: ").append(_chachaReplyKey)
|
||||
.append(" chacha reply IV: ").append(Base64.encode(_chachaReplyAD));
|
||||
}
|
||||
}
|
||||
// to chase i2pd bug
|
||||
//buf.append('\n').append(net.i2p.util.HexDump.dump(readReplyKey().getData()));
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/****
|
||||
public static void main(String[] args) throws Exception {
|
||||
RouterContext ctx = new RouterContext(null);
|
||||
TESTKF = new net.i2p.router.transport.crypto.X25519KeyFactory(ctx);
|
||||
byte[] h = new byte[32];
|
||||
ctx.random().nextBytes(h);
|
||||
Hash bh = new Hash(h);
|
||||
SessionKey k1 = ctx.keyGenerator().generateSessionKey();
|
||||
SessionKey k2 = ctx.keyGenerator().generateSessionKey();
|
||||
SessionKey k3 = ctx.keyGenerator().generateSessionKey();
|
||||
byte[] iv = new byte[16];
|
||||
ctx.random().nextBytes(iv);
|
||||
Properties props = new Properties();
|
||||
props.setProperty("foo", "bar");
|
||||
BuildRequestRecord brr = new BuildRequestRecord(ctx, 1, 2, bh, 3, k1, k2, k3, iv, false, false, props);
|
||||
System.out.println(brr.toString());
|
||||
System.out.println("\nplaintext request:\n" + net.i2p.util.HexDump.dump(brr.getData()));
|
||||
net.i2p.crypto.KeyPair kp = ctx.keyGenerator().generatePKIKeys(net.i2p.crypto.EncType.ECIES_X25519);
|
||||
PublicKey bpub = kp.getPublic();
|
||||
PrivateKey bpriv = kp.getPrivate();
|
||||
EncryptedBuildRecord record = brr.encryptECIESRecord(ctx, bpub, bh);
|
||||
System.out.println("\nencrypted request:\n" + net.i2p.util.HexDump.dump(record.getData()));
|
||||
System.out.println("reply key: " + brr.getChaChaReplyKey());
|
||||
System.out.println("reply IV: " + net.i2p.data.Base64.encode(brr.getChaChaReplyAD()));
|
||||
BuildRequestRecord brr2 = new BuildRequestRecord(ctx, bpriv, record);
|
||||
System.out.println(brr2.toString());
|
||||
System.out.println("\nreply key: " + brr2.getChaChaReplyKey());
|
||||
System.out.println("reply IV: " + net.i2p.data.Base64.encode(brr2.getChaChaReplyAD()));
|
||||
props.setProperty("yes", "no");
|
||||
EncryptedBuildRecord ebr = BuildResponseRecord.create(ctx, 1, brr.getChaChaReplyKey(), brr.getChaChaReplyAD(), props);
|
||||
System.out.println("\nencrypted reply:\n" + net.i2p.util.HexDump.dump(ebr.getData()));
|
||||
BuildResponseRecord.decrypt(ebr, brr2.getChaChaReplyKey(), brr2.getChaChaReplyAD());
|
||||
System.out.println("\nplaintext reply:\n" + net.i2p.util.HexDump.dump(ebr.getData()));
|
||||
Properties p2 = new net.i2p.util.OrderedProperties();
|
||||
DataHelper.fromProperties(ebr.getData(), 0, p2);
|
||||
System.out.println("reply props: " + p2);
|
||||
System.out.println("reply status: " + (ebr.getData()[511] & 0xff));
|
||||
}
|
||||
****/
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
package net.i2p.data.i2np;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.southernstorm.noise.protocol.ChaChaPolyCipherState;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Hash;
|
||||
import net.i2p.data.SessionKey;
|
||||
//import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Class that creates an encrypted tunnel build message record.
|
||||
@ -22,7 +26,8 @@ import net.i2p.data.SessionKey;
|
||||
public class BuildResponseRecord {
|
||||
|
||||
/**
|
||||
* Create a new encrypted response
|
||||
* Create a new encrypted response.
|
||||
* AES only for ElGamal routers.
|
||||
*
|
||||
* @param status the response 0-255
|
||||
* @param replyIV 16 bytes
|
||||
@ -31,17 +36,85 @@ public class BuildResponseRecord {
|
||||
*/
|
||||
public static EncryptedBuildRecord create(I2PAppContext ctx, int status, SessionKey replyKey,
|
||||
byte replyIV[], long responseMessageId) {
|
||||
//Log log = ctx.logManager().getLog(BuildResponseRecord.class);
|
||||
byte rv[] = new byte[TunnelBuildReplyMessage.RECORD_SIZE];
|
||||
ctx.random().nextBytes(rv, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE - Hash.HASH_LENGTH - 1);
|
||||
rv[TunnelBuildMessage.RECORD_SIZE-1] = (byte) status;
|
||||
// rv = AES(SHA256(padding+status) + padding + status, replyKey, replyIV)
|
||||
ctx.sha().calculateHash(rv, Hash.HASH_LENGTH, rv.length - Hash.HASH_LENGTH, rv, 0);
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug(responseMessageId + ": before encrypt: " + Base64.encode(rv, 0, 128) + " with " + replyKey.toBase64() + "/" + Base64.encode(replyIV));
|
||||
ctx.aes().encrypt(rv, 0, rv, 0, replyKey, replyIV, rv.length);
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug(responseMessageId + ": after encrypt: " + Base64.encode(rv, 0, 128));
|
||||
return new EncryptedBuildRecord(rv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new encrypted response.
|
||||
* ChaCha/Poly only for ECIES routers.
|
||||
*
|
||||
* @param status the response 0-255
|
||||
* @param replyAD 32 bytes
|
||||
* @param options 511 bytes max when serialized
|
||||
* @return a 528-byte response record
|
||||
* @throws IllegalArgumentException if options too big or on encryption failure
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public static EncryptedBuildRecord create(I2PAppContext ctx, int status, SessionKey replyKey,
|
||||
byte replyAD[], Properties options) {
|
||||
byte rv[] = new byte[TunnelBuildReplyMessage.RECORD_SIZE];
|
||||
int off;
|
||||
try {
|
||||
off = DataHelper.toProperties(rv, 0, options);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("options", e);
|
||||
}
|
||||
int sz = TunnelBuildReplyMessage.RECORD_SIZE - off - 1;
|
||||
if (sz > 0)
|
||||
ctx.random().nextBytes(rv, off, sz);
|
||||
else if (sz < 0)
|
||||
throw new IllegalArgumentException("options");
|
||||
rv[TunnelBuildMessage.RECORD_SIZE - 17] = (byte) status;
|
||||
boolean ok = encryptAEADBlock(replyAD, rv, replyKey);
|
||||
if (!ok)
|
||||
throw new IllegalArgumentException("encrypt fail");
|
||||
return new EncryptedBuildRecord(rv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts in place
|
||||
* @param ad non-null
|
||||
* @return success
|
||||
* @since 0.9.48
|
||||
*/
|
||||
private static final boolean encryptAEADBlock(byte[] ad, byte data[], SessionKey key) {
|
||||
ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
|
||||
chacha.initializeKey(key.getData(), 0);
|
||||
try {
|
||||
chacha.encryptWithAd(ad, data, 0, data, 0, TunnelBuildReplyMessage.RECORD_SIZE - 16);
|
||||
} catch (GeneralSecurityException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* ChaCha/Poly only for ECIES routers.
|
||||
* Decrypts in place in bytes 0-511.
|
||||
* Status will be rec.getData()[511].
|
||||
* Properties will be at rec.getData()[0].
|
||||
*
|
||||
* @param rec 528 bytes, data will be decrypted in place.
|
||||
* @param ad non-null
|
||||
* @return success
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public static boolean decrypt(EncryptedBuildRecord rec, SessionKey key, byte[] ad) {
|
||||
ChaChaPolyCipherState chacha = new ChaChaPolyCipherState();
|
||||
chacha.initializeKey(key.getData(), 0);
|
||||
try {
|
||||
// this is safe to do in-place, it checks the mac before starting decryption
|
||||
byte[] data = rec.getData();
|
||||
chacha.decryptWithAd(ad, data, 0, data, 0, TunnelBuildReplyMessage.RECORD_SIZE);
|
||||
} catch (GeneralSecurityException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -32,9 +32,17 @@ public class LeaseSetKeys {
|
||||
* @since 0.9.44
|
||||
*/
|
||||
public static final Set<EncType> SET_ELG = Collections.unmodifiableSet(EnumSet.of(EncType.ELGAMAL_2048));
|
||||
/**
|
||||
* Unmodifiable, ECIES-X25519 only
|
||||
* @since public since 0.9.46
|
||||
*/
|
||||
public static final Set<EncType> SET_EC = Collections.unmodifiableSet(EnumSet.of(EncType.ECIES_X25519));
|
||||
private static final Set<EncType> SET_BOTH = Collections.unmodifiableSet(EnumSet.of(EncType.ELGAMAL_2048, EncType.ECIES_X25519));
|
||||
private static final Set<EncType> SET_NONE = Collections.unmodifiableSet(EnumSet.noneOf(EncType.class));
|
||||
/**
|
||||
* Unmodifiable, ElGamal and ECIES-X25519.
|
||||
* @since public since 0.9.48
|
||||
*/
|
||||
public static final Set<EncType> SET_BOTH = Collections.unmodifiableSet(EnumSet.of(EncType.ELGAMAL_2048, EncType.ECIES_X25519));
|
||||
private static final Set<EncType> SET_NONE = Collections.emptySet();
|
||||
|
||||
/**
|
||||
* Client with a single key
|
||||
|
@ -18,7 +18,7 @@ public class RouterVersion {
|
||||
/** deprecated */
|
||||
public final static String ID = "Monotone";
|
||||
public final static String VERSION = CoreVersion.VERSION;
|
||||
public final static long BUILD = 4;
|
||||
public final static long BUILD = 5;
|
||||
|
||||
/** for example "-test" */
|
||||
public final static String EXTRA = "";
|
||||
|
@ -3,6 +3,8 @@ package net.i2p.router.tunnel;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.crypto.EncType;
|
||||
import net.i2p.data.EmptyProperties;
|
||||
import net.i2p.data.Hash;
|
||||
import net.i2p.data.PublicKey;
|
||||
import net.i2p.data.SessionKey;
|
||||
@ -10,45 +12,13 @@ import net.i2p.data.i2np.BuildRequestRecord;
|
||||
import net.i2p.data.i2np.EncryptedBuildRecord;
|
||||
import net.i2p.data.i2np.I2NPMessage;
|
||||
import net.i2p.data.i2np.TunnelBuildMessage;
|
||||
import net.i2p.router.RouterContext;
|
||||
|
||||
/**
|
||||
* Fill in the encrypted BuildRequestRecords in a TunnelBuildMessage
|
||||
*/
|
||||
public abstract class BuildMessageGenerator {
|
||||
|
||||
/** return null if it is unable to find a router's public key (etc) */
|
||||
/****
|
||||
public TunnelBuildMessage createInbound(RouterContext ctx, TunnelCreatorConfig cfg) {
|
||||
return create(ctx, cfg, null, -1);
|
||||
}
|
||||
****/
|
||||
|
||||
/** return null if it is unable to find a router's public key (etc) */
|
||||
/****
|
||||
public TunnelBuildMessage createOutbound(RouterContext ctx, TunnelCreatorConfig cfg, Hash replyRouter, long replyTunnel) {
|
||||
return create(ctx, cfg, replyRouter, replyTunnel);
|
||||
}
|
||||
****/
|
||||
|
||||
/****
|
||||
private TunnelBuildMessage create(RouterContext ctx, TunnelCreatorConfig cfg, Hash replyRouter, long replyTunnel) {
|
||||
TunnelBuildMessage msg = new TunnelBuildMessage(ctx);
|
||||
List order = new ArrayList(ORDER.length);
|
||||
for (int i = 0; i < ORDER.length; i++) order.add(ORDER[i]);
|
||||
Collections.shuffle(order, ctx.random());
|
||||
for (int i = 0; i < ORDER.length; i++) {
|
||||
int hop = ((Integer)order.get(i)).intValue();
|
||||
Hash peer = cfg.getPeer(hop);
|
||||
RouterInfo ri = ctx.netDb().lookupRouterInfoLocally(peer);
|
||||
if (ri == null)
|
||||
return null;
|
||||
createRecord(i, hop, msg, cfg, replyRouter, replyTunnel, ctx, ri.getIdentity().getPublicKey());
|
||||
}
|
||||
layeredEncrypt(ctx, msg, cfg, order);
|
||||
return msg;
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* Place the asymmetrically encrypted record in the specified record slot,
|
||||
* containing the hop's configuration (as well as the reply info, if it is an outbound endpoint)
|
||||
@ -61,29 +31,35 @@ public abstract class BuildMessageGenerator {
|
||||
*/
|
||||
public static void createRecord(int recordNum, int hop, TunnelBuildMessage msg,
|
||||
TunnelCreatorConfig cfg, Hash replyRouter,
|
||||
long replyTunnel, I2PAppContext ctx, PublicKey peerKey) {
|
||||
//Log log = ctx.logManager().getLog(BuildMessageGenerator.class);
|
||||
long replyTunnel, RouterContext ctx, PublicKey peerKey) {
|
||||
EncryptedBuildRecord erec;
|
||||
if (peerKey != null) {
|
||||
BuildRequestRecord req = null;
|
||||
boolean isEC = peerKey.getType() == EncType.ECIES_X25519;
|
||||
BuildRequestRecord req;
|
||||
if ( (!cfg.isInbound()) && (hop + 1 == cfg.getLength()) ) //outbound endpoint
|
||||
req = createUnencryptedRecord(ctx, cfg, hop, replyRouter, replyTunnel);
|
||||
req = createUnencryptedRecord(ctx, cfg, hop, replyRouter, replyTunnel, isEC);
|
||||
else
|
||||
req = createUnencryptedRecord(ctx, cfg, hop, null, -1);
|
||||
req = createUnencryptedRecord(ctx, cfg, hop, null, -1, isEC);
|
||||
if (req == null)
|
||||
throw new IllegalArgumentException("hop bigger than config");
|
||||
Hash peer = cfg.getPeer(hop);
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug("Record " + recordNum + "/" + hop + "/" + peer.toBase64()
|
||||
// + ": unencrypted = " + Base64.encode(req.getData().getData()));
|
||||
erec = req.encryptRecord(ctx, peerKey, peer);
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug("Record " + recordNum + "/" + hop + ": encrypted = " + Base64.encode(encrypted));
|
||||
if (isEC) {
|
||||
erec = req.encryptECIESRecord(ctx, peerKey, peer);
|
||||
cfg.setChaChaReplyKeys(hop, req.getChaChaReplyKey(), req.getChaChaReplyAD());
|
||||
} else {
|
||||
erec = req.encryptRecord(ctx, peerKey, peer);
|
||||
}
|
||||
} else {
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug("Record " + recordNum + "/" + hop + "/ is blank/random");
|
||||
byte encrypted[] = new byte[TunnelBuildMessage.RECORD_SIZE];
|
||||
ctx.random().nextBytes(encrypted);
|
||||
if (cfg.isInbound() && hop + 1 == cfg.getLength()) { // IBEP
|
||||
System.arraycopy(cfg.getPeer(hop).getData(), 0, encrypted, 0, BuildRequestRecord.PEER_SIZE);
|
||||
ctx.random().nextBytes(encrypted, BuildRequestRecord.PEER_SIZE, TunnelBuildMessage.RECORD_SIZE - BuildRequestRecord.PEER_SIZE);
|
||||
byte[] h = new byte[Hash.HASH_LENGTH];
|
||||
ctx.sha().calculateHash(encrypted, 0, TunnelBuildMessage.RECORD_SIZE, h, 0);
|
||||
cfg.setBlankHash(new Hash(h));
|
||||
} else {
|
||||
ctx.random().nextBytes(encrypted);
|
||||
}
|
||||
erec = new EncryptedBuildRecord(encrypted);
|
||||
}
|
||||
msg.setRecord(recordNum, erec);
|
||||
@ -93,8 +69,7 @@ public abstract class BuildMessageGenerator {
|
||||
* Returns null if hop >= cfg.length
|
||||
*/
|
||||
private static BuildRequestRecord createUnencryptedRecord(I2PAppContext ctx, TunnelCreatorConfig cfg, int hop,
|
||||
Hash replyRouter, long replyTunnel) {
|
||||
//Log log = ctx.logManager().getLog(BuildMessageGenerator.class);
|
||||
Hash replyRouter, long replyTunnel, boolean isEC) {
|
||||
if (hop < cfg.getLength()) {
|
||||
// ok, now lets fill in some data
|
||||
HopConfig hopConfig = cfg.getConfig(hop);
|
||||
@ -138,15 +113,17 @@ public abstract class BuildMessageGenerator {
|
||||
// dont care about these intermediary hops
|
||||
nextMsgId = ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE);
|
||||
}
|
||||
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug("Hop " + hop + " has the next message ID of " + nextMsgId + " for " + cfg
|
||||
// + " with replyKey " + replyKey.toBase64() + " and replyIV " + Base64.encode(iv));
|
||||
|
||||
BuildRequestRecord rec= new BuildRequestRecord(ctx, recvTunnelId, peer, nextTunnelId, nextPeer,
|
||||
nextMsgId, layerKey, ivKey, replyKey,
|
||||
iv, isInGW, isOutEnd);
|
||||
|
||||
BuildRequestRecord rec;
|
||||
if (isEC) {
|
||||
// TODO pass properties from cfg
|
||||
rec = new BuildRequestRecord(ctx, recvTunnelId, nextTunnelId, nextPeer,
|
||||
nextMsgId, layerKey, ivKey, replyKey,
|
||||
iv, isInGW, isOutEnd, EmptyProperties.INSTANCE);
|
||||
} else {
|
||||
rec = new BuildRequestRecord(ctx, recvTunnelId, peer, nextTunnelId, nextPeer,
|
||||
nextMsgId, layerKey, ivKey, replyKey,
|
||||
iv, isInGW, isOutEnd);
|
||||
}
|
||||
return rec;
|
||||
} else {
|
||||
return null;
|
||||
@ -163,34 +140,25 @@ public abstract class BuildMessageGenerator {
|
||||
*/
|
||||
public static void layeredEncrypt(I2PAppContext ctx, TunnelBuildMessage msg,
|
||||
TunnelCreatorConfig cfg, List<Integer> order) {
|
||||
//Log log = ctx.logManager().getLog(BuildMessageGenerator.class);
|
||||
// encrypt the records so that the right elements will be visible at the right time
|
||||
for (int i = 0; i < msg.getRecordCount(); i++) {
|
||||
EncryptedBuildRecord rec = msg.getRecord(i);
|
||||
Integer hopNum = order.get(i);
|
||||
int hop = hopNum.intValue();
|
||||
if ( (isBlank(cfg, hop)) || (!cfg.isInbound() && hop == 1) ) {
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug(msg.getUniqueId() + ": not pre-decrypting record " + i + "/" + hop + " for " + cfg);
|
||||
if ((isBlank(cfg, hop) && !(cfg.isInbound() && hop + 1 == cfg.getLength())) ||
|
||||
(!cfg.isInbound() && hop == 1)) {
|
||||
continue;
|
||||
}
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug(msg.getUniqueId() + ": pre-decrypting record " + i + "/" + hop + " for " + cfg);
|
||||
// ok, now decrypt the record with all of the reply keys from cfg.getConfig(0) through hop-1
|
||||
int stop = (cfg.isInbound() ? 0 : 1);
|
||||
for (int j = hop-1; j >= stop; j--) {
|
||||
HopConfig hopConfig = cfg.getConfig(j);
|
||||
SessionKey key = hopConfig.getReplyKey();
|
||||
byte iv[] = hopConfig.getReplyIV();
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug(msg.getUniqueId() + ": pre-decrypting record " + i + "/" + hop + " for " + cfg
|
||||
// + " with " + key.toBase64() + "/" + Base64.encode(iv));
|
||||
// corrupts the SDS
|
||||
ctx.aes().decrypt(rec.getData(), 0, rec.getData(), 0, key, iv, TunnelBuildMessage.RECORD_SIZE);
|
||||
}
|
||||
}
|
||||
//if (log.shouldLog(Log.DEBUG))
|
||||
// log.debug(msg.getUniqueId() + ": done pre-decrypting all records for " + cfg);
|
||||
}
|
||||
|
||||
public static boolean isBlank(TunnelCreatorConfig cfg, int hop) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.i2p.router.tunnel;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.crypto.EncType;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataFormatException;
|
||||
@ -11,6 +10,7 @@ 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.RouterContext;
|
||||
import net.i2p.router.RouterThrottleImpl;
|
||||
import net.i2p.router.util.DecayingBloomFilter;
|
||||
import net.i2p.router.util.DecayingHashSet;
|
||||
@ -28,11 +28,11 @@ import net.i2p.util.SystemVersion;
|
||||
*
|
||||
*/
|
||||
public class BuildMessageProcessor {
|
||||
private final I2PAppContext ctx;
|
||||
private final RouterContext ctx;
|
||||
private final Log log;
|
||||
private final DecayingBloomFilter _filter;
|
||||
|
||||
public BuildMessageProcessor(I2PAppContext ctx) {
|
||||
public BuildMessageProcessor(RouterContext ctx) {
|
||||
this.ctx = ctx;
|
||||
log = ctx.logManager().getLog(getClass());
|
||||
_filter = selectFilter();
|
||||
@ -89,10 +89,6 @@ public class BuildMessageProcessor {
|
||||
* @return the current hop's decrypted record or null on failure
|
||||
*/
|
||||
public BuildRequestRecord decrypt(TunnelBuildMessage msg, Hash ourHash, PrivateKey privKey) {
|
||||
// TODO proposal 152
|
||||
if (privKey.getType() != EncType.ELGAMAL_2048)
|
||||
return null;
|
||||
|
||||
BuildRequestRecord rv = null;
|
||||
int ourHop = -1;
|
||||
long beforeActualDecrypt = 0;
|
||||
@ -101,8 +97,7 @@ public class BuildMessageProcessor {
|
||||
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);
|
||||
boolean eq = DataHelper.eq(ourHashData, 0, rec.getData(), 0, BuildRequestRecord.PEER_SIZE);
|
||||
if (eq) {
|
||||
beforeActualDecrypt = System.currentTimeMillis();
|
||||
try {
|
||||
@ -121,7 +116,9 @@ public class BuildMessageProcessor {
|
||||
// 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);
|
||||
boolean isEC = ctx.keyManager().getPrivateKey().getType() == EncType.ECIES_X25519;
|
||||
int off = isEC ? BuildRequestRecord.OFF_REPLY_KEY_EC : BuildRequestRecord.OFF_REPLY_KEY;
|
||||
boolean isDup = _filter.add(rv.getData(), off, 32);
|
||||
if (isDup) {
|
||||
if (log.shouldLog(Log.WARN))
|
||||
log.warn(msg.getUniqueId() + ": Dup record: " + rv);
|
||||
@ -135,8 +132,10 @@ public class BuildMessageProcessor {
|
||||
// TODO should we keep looking for a second match and fail if found?
|
||||
break;
|
||||
} catch (DataFormatException dfe) {
|
||||
// For ECIES routers, this is relatively common due to old routers that don't
|
||||
// check enc type sending us ElG requests
|
||||
if (log.shouldLog(Log.WARN))
|
||||
log.warn(msg.getUniqueId() + ": Matching record decrypt failure", dfe);
|
||||
log.warn(msg.getUniqueId() + ": Matching record decrypt failure " + privKey.getType(), dfe);
|
||||
// on the microscopic chance that there's another router
|
||||
// out there with the same first 16 bytes, go around again
|
||||
continue;
|
||||
@ -146,7 +145,7 @@ public class BuildMessageProcessor {
|
||||
if (rv == null) {
|
||||
// none of the records matched, b0rk
|
||||
if (log.shouldLog(Log.WARN))
|
||||
log.warn(msg.getUniqueId() + ": No matching record");
|
||||
log.warn(msg.getUniqueId() + ": No record decrypted");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Hash;
|
||||
import net.i2p.data.SessionKey;
|
||||
import net.i2p.data.i2np.BuildResponseRecord;
|
||||
import net.i2p.data.i2np.EncryptedBuildRecord;
|
||||
import net.i2p.data.i2np.TunnelBuildReplyMessage;
|
||||
import net.i2p.util.Log;
|
||||
@ -50,19 +51,34 @@ public class BuildReplyHandler {
|
||||
for (int i = 0; i < rv.length; i++) {
|
||||
int hop = recordOrder.get(i).intValue();
|
||||
if (BuildMessageGenerator.isBlank(cfg, hop)) {
|
||||
// self...
|
||||
// self or unused...
|
||||
if (log.shouldLog(Log.DEBUG))
|
||||
log.debug(reply.getUniqueId() + ": no need to decrypt record " + i + "/" + hop + ", as its out of range: " + cfg);
|
||||
rv[i] = 0;
|
||||
log.debug(reply.getUniqueId() + ": skipping record " + i + "/" + hop + " for: " + cfg);
|
||||
if (cfg.isInbound() && hop + 1 == cfg.getLength()) { // IBEP
|
||||
byte[] h1 = new byte[Hash.HASH_LENGTH];
|
||||
ctx.sha().calculateHash(reply.getRecord(i).getData(), 0, TunnelBuildReplyMessage.RECORD_SIZE, h1, 0);
|
||||
// get stored hash put here by BuildMessageGenerator
|
||||
Hash h2 = cfg.getBlankHash();
|
||||
if (h2 != null && DataHelper.eq(h1, h2.getData())) {
|
||||
rv[i] = 0;
|
||||
} else {
|
||||
if (log.shouldWarn())
|
||||
log.warn("IBEP record corrupt on " + cfg);
|
||||
// Caller doesn't check value for this hop so fail the whole thing
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
rv[i] = 0;
|
||||
}
|
||||
} else {
|
||||
int ok = decryptRecord(reply, cfg, i, hop);
|
||||
if (ok == -1) {
|
||||
if (log.shouldLog(Log.WARN))
|
||||
log.warn(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " was not ok: " + cfg);
|
||||
log.warn(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " fail: " + cfg);
|
||||
return null;
|
||||
} else {
|
||||
if (log.shouldLog(Log.DEBUG))
|
||||
log.debug(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " was ok: " + ok + " for " + cfg);
|
||||
log.debug(reply.getUniqueId() + ": decrypt record " + i + "/" + hop + " success: " + ok + " for " + cfg);
|
||||
}
|
||||
rv[i] = ok;
|
||||
}
|
||||
@ -75,22 +91,23 @@ public class BuildReplyHandler {
|
||||
*
|
||||
* Note that this layer-decrypts the build records in-place.
|
||||
* Do not call this more than once for a given message.
|
||||
* Do not call for blank hops.
|
||||
*
|
||||
* @return the status 0-255, or -1 on decrypt failure
|
||||
*/
|
||||
private int decryptRecord(TunnelBuildReplyMessage reply, TunnelCreatorConfig cfg, int recordNum, int hop) {
|
||||
if (BuildMessageGenerator.isBlank(cfg, hop)) {
|
||||
if (log.shouldLog(Log.DEBUG))
|
||||
log.debug(reply.getUniqueId() + ": Record " + recordNum + "/" + hop + " is fake, so consider it valid...");
|
||||
return 0;
|
||||
}
|
||||
EncryptedBuildRecord rec = reply.getRecord(recordNum);
|
||||
byte[] data = rec.getData();
|
||||
int start = cfg.getLength() - 1;
|
||||
if (cfg.isInbound())
|
||||
start--; // the last hop in an inbound tunnel response doesn't actually encrypt
|
||||
int end = hop;
|
||||
boolean isEC = cfg.isEC(hop);
|
||||
// chacha decrypt after the loop
|
||||
if (isEC)
|
||||
end++;
|
||||
// do we need to adjust this for the endpoint?
|
||||
for (int j = start; j >= hop; j--) {
|
||||
for (int j = start; j >= end; j--) {
|
||||
HopConfig hopConfig = cfg.getConfig(j);
|
||||
SessionKey replyKey = hopConfig.getReplyKey();
|
||||
byte replyIV[] = hopConfig.getReplyIV();
|
||||
@ -106,24 +123,41 @@ public class BuildReplyHandler {
|
||||
}
|
||||
// ok, all of the layered encryption is stripped, so lets verify it
|
||||
// (formatted per BuildResponseRecord.create)
|
||||
// don't cache the result
|
||||
//Hash h = ctx.sha().calculateHash(data, off + Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH);
|
||||
byte[] h = SimpleByteCache.acquire(Hash.HASH_LENGTH);
|
||||
ctx.sha().calculateHash(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH, h, 0);
|
||||
boolean ok = DataHelper.eq(h, 0, data, 0, Hash.HASH_LENGTH);
|
||||
if (!ok) {
|
||||
if (log.shouldLog(Log.DEBUG))
|
||||
log.debug(reply.getUniqueId() + ": Failed verification on " + recordNum + "/" + hop + ": " + Base64.encode(h) + " calculated, " +
|
||||
Base64.encode(data, 0, Hash.HASH_LENGTH) + " expected\n" +
|
||||
"Record: " + Base64.encode(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH));
|
||||
SimpleByteCache.release(h);
|
||||
return -1;
|
||||
int rv;
|
||||
if (isEC) {
|
||||
// For last iteration, do ChaCha instead
|
||||
SessionKey replyKey = cfg.getChaChaReplyKey(hop);
|
||||
byte[] replyIV = cfg.getChaChaReplyAD(hop);
|
||||
if (log.shouldDebug())
|
||||
log.debug(reply.getUniqueId() + ": Decrypting chacha/poly record " + recordNum + "/" + hop + " with replyKey "
|
||||
+ replyKey.toBase64() + "/" + Base64.encode(replyIV) + ": " + cfg);
|
||||
boolean ok = BuildResponseRecord.decrypt(rec, replyKey, replyIV);
|
||||
if (!ok) {
|
||||
if (log.shouldWarn())
|
||||
log.debug(reply.getUniqueId() + ": chacha reply decrypt fail on " + recordNum + "/" + hop);
|
||||
return -1;
|
||||
}
|
||||
// reply properties TODO
|
||||
rv = data[TunnelBuildReplyMessage.RECORD_SIZE - 17] & 0xff;
|
||||
} else {
|
||||
// don't cache the result
|
||||
//Hash h = ctx.sha().calculateHash(data, off + Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH);
|
||||
byte[] h = SimpleByteCache.acquire(Hash.HASH_LENGTH);
|
||||
ctx.sha().calculateHash(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH, h, 0);
|
||||
boolean ok = DataHelper.eq(h, 0, data, 0, Hash.HASH_LENGTH);
|
||||
if (!ok) {
|
||||
if (log.shouldWarn())
|
||||
log.warn(reply.getUniqueId() + ": sha256 reply verify fail on " + recordNum + "/" + hop + ": " + Base64.encode(h) + " calculated, " +
|
||||
Base64.encode(data, 0, Hash.HASH_LENGTH) + " expected\n" +
|
||||
"Record: " + Base64.encode(data, Hash.HASH_LENGTH, TunnelBuildReplyMessage.RECORD_SIZE-Hash.HASH_LENGTH));
|
||||
SimpleByteCache.release(h);
|
||||
return -1;
|
||||
}
|
||||
SimpleByteCache.release(h);
|
||||
int rv = data[TunnelBuildReplyMessage.RECORD_SIZE - 1] & 0xff;
|
||||
if (log.shouldLog(Log.DEBUG))
|
||||
log.debug(reply.getUniqueId() + ": Verified: " + rv + " for record " + recordNum + "/" + hop);
|
||||
return rv;
|
||||
rv = data[TunnelBuildReplyMessage.RECORD_SIZE - 1] & 0xff;
|
||||
}
|
||||
if (log.shouldLog(Log.DEBUG))
|
||||
log.debug(reply.getUniqueId() + ": Verified: " + rv + " for record " + recordNum + "/" + hop);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ import net.i2p.data.TunnelId;
|
||||
/**
|
||||
* Defines the general configuration for a hop in a tunnel.
|
||||
*
|
||||
* This is used for both participating tunnels and tunnels we create.
|
||||
* Data only stored for tunnels we create should be in
|
||||
* TunnelCreatorConfig to save space.
|
||||
*/
|
||||
public class HopConfig {
|
||||
private byte _receiveTunnelId[];
|
||||
@ -87,19 +90,26 @@ public class HopConfig {
|
||||
public SessionKey getIVKey() { return _ivKey; }
|
||||
public void setIVKey(SessionKey key) { _ivKey = key; }
|
||||
|
||||
/** key to encrypt the reply sent for the new tunnel creation crypto */
|
||||
/**
|
||||
* Key to encrypt the reply sent for the tunnel creation crypto.
|
||||
* Not used for participating tunnels, will return null,
|
||||
* candidate for moving to TunnelCreatorConfig.
|
||||
* @return key or null
|
||||
*/
|
||||
public SessionKey getReplyKey() { return _replyKey; }
|
||||
public void setReplyKey(SessionKey key) { _replyKey = key; }
|
||||
|
||||
/**
|
||||
* IV used to encrypt the reply sent for the new tunnel creation crypto
|
||||
* IV used to encrypt the reply sent for the tunnel creation crypto.
|
||||
* Not used for participating tunnels, will return null,
|
||||
* candidate for moving to TunnelCreatorConfig.
|
||||
*
|
||||
* @return 16 bytes
|
||||
* @return 16 bytes or null
|
||||
*/
|
||||
public byte[] getReplyIV() { return _replyIV; }
|
||||
|
||||
/**
|
||||
* IV used to encrypt the reply sent for the new tunnel creation crypto
|
||||
* IV used to encrypt the reply sent for the tunnel creation crypto
|
||||
*
|
||||
* @throws IllegalArgumentException if not 16 bytes
|
||||
*/
|
||||
|
@ -6,6 +6,7 @@ import java.util.Properties;
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.Hash;
|
||||
import net.i2p.data.SessionKey;
|
||||
import net.i2p.data.TunnelId;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.TunnelInfo;
|
||||
@ -39,6 +40,10 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
|
||||
//private final double _peakThroughput[] = new double[THROUGHPUT_COUNT];
|
||||
private long _peakThroughputCurrentTotal;
|
||||
private long _peakThroughputLastCoallesce = System.currentTimeMillis();
|
||||
private Hash _blankHash;
|
||||
private SessionKey[] _replyKeys;
|
||||
private byte[][] _replyADs;
|
||||
|
||||
// Make configurable? - but can't easily get to pool options from here
|
||||
private static final int MAX_CONSECUTIVE_TEST_FAILURES = 3;
|
||||
|
||||
@ -238,6 +243,61 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
|
||||
*/
|
||||
public void setPriority(int priority) { _priority = priority; }
|
||||
|
||||
/**
|
||||
* Checksum for blank record
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public Hash getBlankHash() { return _blankHash; }
|
||||
|
||||
/**
|
||||
* Checksum for blank record
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public void setBlankHash(Hash h) { _blankHash = h; }
|
||||
|
||||
/**
|
||||
* Set ECIES reply key and IV
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public void setChaChaReplyKeys(int hop, SessionKey key, byte[] ad) {
|
||||
if (_replyKeys == null) {
|
||||
_replyKeys = new SessionKey[_config.length];
|
||||
_replyADs = new byte[_config.length][];
|
||||
}
|
||||
_replyKeys[hop] = key;
|
||||
_replyADs[hop] = ad;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it an ECIES hop?
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public boolean isEC(int hop) {
|
||||
if (_replyKeys == null)
|
||||
return false;
|
||||
return _replyKeys[hop] != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ECIES reply key
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public SessionKey getChaChaReplyKey(int hop) {
|
||||
if (_replyKeys == null)
|
||||
return null;
|
||||
return _replyKeys[hop];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ECIES reply AD
|
||||
* @since 0.9.48
|
||||
*/
|
||||
public byte[] getChaChaReplyAD(int hop) {
|
||||
if (_replyADs == null)
|
||||
return null;
|
||||
return _replyADs[hop];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// H0:1235-->H1:2345-->H2:2345
|
||||
@ -253,7 +313,7 @@ public abstract class TunnelCreatorConfig implements TunnelInfo {
|
||||
buf.append(": GW ");
|
||||
for (int i = 0; i < _peers.length; i++) {
|
||||
buf.append(_peers[i].toBase64().substring(0,4));
|
||||
buf.append(':');
|
||||
buf.append(isEC(i) ? " EC:" : " ElG:");
|
||||
if (_config[i].getReceiveTunnel() != null)
|
||||
buf.append(_config[i].getReceiveTunnel());
|
||||
else
|
||||
|
@ -1,12 +1,15 @@
|
||||
package net.i2p.router.tunnel.pool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import net.i2p.crypto.EncType;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.EmptyProperties;
|
||||
import net.i2p.data.Hash;
|
||||
import net.i2p.data.router.RouterIdentity;
|
||||
import net.i2p.data.router.RouterInfo;
|
||||
@ -96,6 +99,7 @@ class BuildHandler implements Runnable {
|
||||
private static final long MAX_REQUEST_FUTURE = 5*60*1000;
|
||||
/** must be > 1 hour due to rouding down */
|
||||
private static final long MAX_REQUEST_AGE = 65*60*1000;
|
||||
private static final long MAX_REQUEST_AGE_ECIES = 8*60*1000;
|
||||
|
||||
private static final long JOB_LAG_LIMIT_TUNNEL = 350;
|
||||
|
||||
@ -441,6 +445,7 @@ class BuildHandler implements Runnable {
|
||||
_context.statManager().addRateData("tunnel.corruptBuildReply", 1);
|
||||
// don't leak
|
||||
_exec.buildComplete(cfg);
|
||||
// TODO blame everybody
|
||||
}
|
||||
}
|
||||
|
||||
@ -718,13 +723,24 @@ class BuildHandler implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
// time is in hours, rounded down.
|
||||
// tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it was not.
|
||||
// As of 0.9.16, allow + 5 minutes to - 65 minutes.
|
||||
long time = req.readRequestTime();
|
||||
long now = _context.clock().now();
|
||||
long roundedNow = (now / (60l*60l*1000l)) * (60*60*1000);
|
||||
long timeDiff = roundedNow - time;
|
||||
boolean isEC = _context.keyManager().getPrivateKey().getType() == EncType.ECIES_X25519;
|
||||
long timeDiff;
|
||||
long maxAge;
|
||||
if (isEC) {
|
||||
// time is in minutes, rounded down.
|
||||
long roundedNow = (now / (60*1000L)) * (60*1000);
|
||||
timeDiff = roundedNow - time;
|
||||
maxAge = MAX_REQUEST_AGE_ECIES;
|
||||
} else {
|
||||
// time is in hours, rounded down.
|
||||
// tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it was not.
|
||||
// As of 0.9.16, allow + 5 minutes to - 65 minutes.
|
||||
long roundedNow = (now / (60*60*1000L)) * (60*60*1000);
|
||||
timeDiff = roundedNow - time;
|
||||
maxAge = MAX_REQUEST_AGE;
|
||||
}
|
||||
if (timeDiff > MAX_REQUEST_AGE) {
|
||||
_context.statManager().addRateData("tunnel.rejectTooOld", 1);
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
@ -915,7 +931,14 @@ class BuildHandler implements Runnable {
|
||||
+ " after " + recvDelay + " with " + response
|
||||
+ " from " + (from != null ? from : "tunnel") + ": " + req);
|
||||
|
||||
EncryptedBuildRecord reply = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId());
|
||||
EncryptedBuildRecord reply;
|
||||
if (isEC) {
|
||||
// TODO options
|
||||
Properties props = EmptyProperties.INSTANCE;
|
||||
reply = BuildResponseRecord.create(_context, response, req.getChaChaReplyKey(), req.getChaChaReplyAD(), props);
|
||||
} else {
|
||||
reply = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId());
|
||||
}
|
||||
int records = state.msg.getRecordCount();
|
||||
int ourSlot = -1;
|
||||
for (int j = 0; j < records; j++) {
|
||||
|
@ -19,6 +19,7 @@ import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Hash;
|
||||
import net.i2p.data.router.RouterInfo;
|
||||
import net.i2p.router.LeaseSetKeys;
|
||||
import net.i2p.router.Router;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.TunnelPoolSettings;
|
||||
@ -97,7 +98,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
|
||||
/**
|
||||
* For debugging, also possibly for restricted routes?
|
||||
* Needs analysis and testing
|
||||
* @return should always be false
|
||||
* @return usually false
|
||||
*/
|
||||
protected boolean shouldSelectExplicit(TunnelPoolSettings settings) {
|
||||
if (settings.isExploratory()) return false;
|
||||
@ -116,7 +117,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
|
||||
/**
|
||||
* For debugging, also possibly for restricted routes?
|
||||
* Needs analysis and testing
|
||||
* @return should always be false
|
||||
* @return the peers
|
||||
*/
|
||||
protected List<Hash> selectExplicit(TunnelPoolSettings settings, int length) {
|
||||
String peers = null;
|
||||
@ -138,8 +139,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
|
||||
if (ctx.profileOrganizer().isSelectable(peer)) {
|
||||
rv.add(peer);
|
||||
} else {
|
||||
if (log.shouldLog(Log.DEBUG))
|
||||
log.debug("Explicit peer is not selectable: " + peerStr);
|
||||
log.logAlways(Log.WARN, "Explicit peer is not selectable: " + peerStr);
|
||||
}
|
||||
} catch (DataFormatException dfe) {
|
||||
if (log.shouldLog(Log.ERROR))
|
||||
@ -148,10 +148,24 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
|
||||
}
|
||||
|
||||
int sz = rv.size();
|
||||
Collections.shuffle(rv, ctx.random());
|
||||
if (sz == 0) {
|
||||
log.logAlways(Log.WARN, "No valid explicit peers found, building zero hop");
|
||||
} else if (sz > 1) {
|
||||
Collections.shuffle(rv, ctx.random());
|
||||
}
|
||||
|
||||
while (rv.size() > length)
|
||||
while (rv.size() > length) {
|
||||
rv.remove(0);
|
||||
}
|
||||
if (rv.size() < length) {
|
||||
int more = length - rv.size();
|
||||
Set<Hash> exclude = getExclude(settings.isInbound(), settings.isExploratory());
|
||||
exclude.addAll(rv);
|
||||
Set<Hash> matches = new HashSet<Hash>(more);
|
||||
ctx.profileOrganizer().selectFastPeers(more, exclude, matches, 0);
|
||||
rv.addAll(matches);
|
||||
Collections.shuffle(rv, ctx.random());
|
||||
}
|
||||
|
||||
if (log.shouldLog(Log.INFO)) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
@ -471,7 +485,8 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
|
||||
maxLen++;
|
||||
if (cap.length() <= maxLen)
|
||||
return true;
|
||||
if (peer.getIdentity().getPublicKey().getType() != EncType.ELGAMAL_2048)
|
||||
EncType type = peer.getIdentity().getPublicKey().getType();
|
||||
if (!LeaseSetKeys.SET_BOTH.contains(type))
|
||||
return true;
|
||||
|
||||
// otherwise, it contains flags we aren't trying to focus on,
|
||||
|
@ -18,6 +18,7 @@ import net.i2p.data.i2np.BuildResponseRecord;
|
||||
import net.i2p.data.i2np.EncryptedBuildRecord;
|
||||
import net.i2p.data.i2np.TunnelBuildMessage;
|
||||
import net.i2p.data.i2np.TunnelBuildReplyMessage;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
@ -41,7 +42,7 @@ public class BuildMessageTestStandalone extends TestCase {
|
||||
private long _replyTunnel;
|
||||
|
||||
public void testBuildMessage() {
|
||||
I2PAppContext ctx = I2PAppContext.getGlobalContext();
|
||||
RouterContext ctx = new RouterContext(null);
|
||||
Log log = ctx.logManager().getLog(getClass());
|
||||
|
||||
List<Integer> order = pickOrder();
|
||||
|
Reference in New Issue
Block a user