diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java index 1982563ba..bf72eb9ef 100644 --- a/core/java/src/net/i2p/client/I2PSession.java +++ b/core/java/src/net/i2p/client/I2PSession.java @@ -489,14 +489,27 @@ public interface I2PSession { public static final int PROTO_STREAMING = 6; /** - * Generally a signed datagram, but could - * also be a raw datagram, depending on the application + * A repliable and signed datagram */ public static final int PROTO_DATAGRAM = 17; /** - * A raw (unsigned) datagram + * A raw (unsigned, unrepliable) datagram * @since 0.9.2 */ public static final int PROTO_DATAGRAM_RAW = 18; + + /** + * A repliable and signed datagram. + * See Proposal 163 and datagrams/Datagram2 + * @since 0.9.66 + */ + public static final int PROTO_DATAGRAM2 = 19; + + /** + * A repliable but unsigned datagram. + * See Proposal 163 and datagrams/Datagram3 + * @since 0.9.66 + */ + public static final int PROTO_DATAGRAM3 = 20; } diff --git a/core/java/src/net/i2p/client/datagram/Datagram2.java b/core/java/src/net/i2p/client/datagram/Datagram2.java new file mode 100644 index 000000000..f49c54797 --- /dev/null +++ b/core/java/src/net/i2p/client/datagram/Datagram2.java @@ -0,0 +1,355 @@ +package net.i2p.client.datagram; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PClientFactory; +import net.i2p.client.I2PSession; +import net.i2p.crypto.KeyGenerator; +import net.i2p.crypto.SigType; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.Signature; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; +import net.i2p.data.SimpleDataStructure; +import net.i2p.util.ByteArrayStream; +import net.i2p.util.HexDump; + +/** + * Class for creating and loading I2P repliable datagrams version 2. + * Ref: Proposal 163 + * + *
+ +----+----+----+----+----+----+----+----+ + | | + ~ from ~ + ~ ~ + | | + +----+----+----+----+----+----+----+----+ + | flags | options (optional) | + +----+----+ + + ~ ~ + ~ ~ + +----+----+----+----+----+----+----+----+ + | | + ~ offline_signature (optional) ~ + ~ expires, sigtype, pubkey, offsig ~ + | | + +----+----+----+----+----+----+----+----+ + | | + ~ payload ~ + ~ ~ + | | + +----+----+----+----+----+----+----+----+ + | | + ~ signature ~ + ~ ~ + | | + +----+----+----+----+----+----+----+----+ + *+ * + * @since 0.9.66 + */ +public class Datagram2 { + + private final Destination _from; + private final byte[] _payload; + private final Properties _options; + + private static final int INIT_DGRAM_BUFSIZE = 2*1024; + private static final int MIN_DGRAM_SIZE = 387 + 2 + 40; + private static final int MAX_DGRAM_BUFSIZE = 61*1024; + private static final byte VERSION_MASK = 0x0f; + private static final byte OPTIONS = 0x10; + private static final byte OFFLINE = 0x20; + private static final byte VERSION = 2; + + + /** + * As returned from load() + */ + private Datagram2(Destination dest, byte[] data, Properties options) { + _from = dest; + _payload = data; + _options = options; + } + + /** + * Make a repliable I2P datagram2 containing the specified payload. + * + * @param payload non-null Bytes to be contained in the I2P datagram. + * @return non-null, throws on all errors + * @throws DataFormatException if payload is too big + */ + public static byte[] make(I2PAppContext ctx, I2PSession session, byte[] payload, Hash tohash) throws DataFormatException { + return make(ctx, session, payload, tohash, null); + } + + /** + * Make a repliable I2P datagram2 containing the specified payload. + * + * @param payload non-null Bytes to be contained in the I2P datagram. + * @param options may be null + * @return non-null, throws on all errors + * @throws DataFormatException if payload is too big + */ + public static byte[] make(I2PAppContext ctx, I2PSession session, byte[] payload, + Hash tohash, Properties options) throws DataFormatException { + ByteArrayOutputStream out = new ByteArrayOutputStream(payload.length + 512); + try { + Destination dest = session.getMyDestination(); + dest.writeBytes(out); + // start of signed data + int off = out.size(); + out.write(tohash.getData()); + out.write((byte) 0); // high byte flags + byte flags = VERSION; + if (options != null && !options.isEmpty()) + flags |= OPTIONS; + if (session.isOffline()) + flags |= OFFLINE; + out.write(flags); // low byte flags + if (options != null && !options.isEmpty()) + DataHelper.writeProperties(out, options); + if (session.isOffline()) { + DataHelper.writeLong(out, 4, session.getOfflineExpiration() / 1000); + SigningPublicKey tspk = session.getTransientSigningPublicKey(); + DataHelper.writeLong(out, 2, tspk.getType().getCode()); + tspk.writeBytes(out); + Signature tsig = session.getOfflineSignature(); + tsig.writeBytes(out); + } + out.write(payload); + // end of signed data + byte[] data = out.toByteArray(); + SigningPrivateKey sxPrivKey = session.getPrivateKey(); + Signature sig = ctx.dsa().sign(data, off, out.size() - off, sxPrivKey); + if (sig == null) + throw new IllegalArgumentException("Sig fail"); + sig.writeBytes(out); + if (out.size() - Hash.HASH_LENGTH > MAX_DGRAM_BUFSIZE) + throw new DataFormatException("Too big"); + byte[] rv = out.toByteArray(); + // remove hash + System.arraycopy(rv, off + Hash.HASH_LENGTH, rv, off, rv.length - (off + Hash.HASH_LENGTH)); + return Arrays.copyOfRange(rv, 0, rv.length - Hash.HASH_LENGTH); + } catch (IOException e) { + throw new DataFormatException("DG2 maker error", e); + } + } + + /** + * Load an I2P repliable datagram and verify the signature. + * + * @param dgram non-null I2P repliable datagram to be loaded + * @return non-null, throws on all errors + * @throws DataFormatException If there is an error in the datagram format + * @throws I2PInvalidDatagramException If the signature fails + */ + public static Datagram2 load(I2PAppContext ctx, I2PSession session, byte[] dgram) throws DataFormatException, I2PInvalidDatagramException { + if (dgram.length < MIN_DGRAM_SIZE) + throw new DataFormatException("Datagram2 too small: " + dgram.length); + ByteArrayInputStream in = new ByteArrayInputStream(dgram); + try { + Destination rxDest = Destination.create(in); + // start of signed data + int off = dgram.length - in.available(); + in.read(); // ignore high byte of flags + int flags = in.read(); + int version = flags & VERSION_MASK; + if (version != VERSION) + throw new DataFormatException("Bad version " + version); + SigningPublicKey spk = rxDest.getSigningPublicKey(); + SigType type = spk.getType(); + if (type == null) + throw new DataFormatException("unsupported sig type"); + int optlen = 0; + Properties options = null; + if ((flags & OPTIONS) != 0) { + in.mark(0); + optlen = (int) DataHelper.readLong(in, 2); + if (optlen > 0) { + in.reset(); + if (in.available() < optlen) + throw new DataFormatException("too small for options: " + dgram.length); + options = DataHelper.readProperties(in, null, true); + } + optlen += 2; + } + int offlinelen = 0; + if ((flags & OFFLINE) != 0) { + offlinelen = 6; + int off2 = dgram.length - in.available(); + long transientExpires = DataHelper.readLong(in, 4) * 1000; + if (transientExpires < ctx.clock().now()) + throw new I2PInvalidDatagramException("Offline signature expired"); + int itype = (int) DataHelper.readLong(in, 2); + SigType ttype = SigType.getByCode(itype); + if (ttype == null || !ttype.isAvailable()) + throw new I2PInvalidDatagramException("Unsupported transient sig type: " + itype); + SigningPublicKey transientSigningPublicKey = new SigningPublicKey(type); + byte[] buf = new byte[transientSigningPublicKey.length()]; + offlinelen += buf.length; + in.read(buf); + transientSigningPublicKey.setData(buf); + SigType otype = rxDest.getSigningPublicKey().getType(); + Signature offlineSignature = new Signature(otype); + buf = new byte[offlineSignature.length()]; + offlinelen += buf.length; + in.read(buf); + offlineSignature.setData(buf); + byte[] data = new byte[0]; // fixme + if (!ctx.dsa().verifySignature(offlineSignature, dgram, off2, 6 + transientSigningPublicKey.length(), spk)) + throw new I2PInvalidDatagramException("Bad offline signature"); + } + int siglen = type.getSigLen(); + in.skip(in.available() - siglen); + // end of signed data + Signature sig = new Signature(type); + sig.readBytes(in); + byte[] buf = new byte[dgram.length + Hash.HASH_LENGTH - (off + siglen)]; + System.arraycopy(session.getMyDestination().calculateHash().getData(), 0, buf, 0, Hash.HASH_LENGTH); + System.arraycopy(dgram, off, buf, Hash.HASH_LENGTH, dgram.length - (off + siglen)); + if (!ctx.dsa().verifySignature(sig, buf, spk)) { + throw new I2PInvalidDatagramException("Bad signature " + type); + } + if (offlinelen > 0) + off += offlinelen; + int datalen = dgram.length - (off + 2 + optlen + siglen); + byte[] payload = new byte[datalen]; + System.arraycopy(dgram, off + 2 + optlen, payload, 0, datalen); + return new Datagram2(rxDest, payload, options); + } catch (IOException e) { + throw new DataFormatException("Error loading datagram", e); + } + } + + /** + * Get the payload carried by an I2P repliable datagram (previously loaded + * with the load() method) + * + * @return A byte array containing the datagram payload + */ + public byte[] getPayload() { + return _payload; + } + + /** + * Get the sender of an I2P repliable datagram (previously loaded with the + * load() method) + * + * @return The Destination of the I2P repliable datagram sender + */ + public Destination getSender() { + return _from; + } + + /** + * Get the options of an I2P repliable datagram (previously loaded with the + * load() method), if any + * + * @return options or null + */ + public Properties getOptions() { + return _options; + } + +/* + public static void main(String[] args) throws Exception { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + // 1 is normal dest, 2 is offline + I2PClient cl = I2PClientFactory.createClient(); + ByteArrayStream bas1 = new ByteArrayStream(800); + ByteArrayStream bas2 = new ByteArrayStream(800); + // sess 1 + cl.createDestination(bas1, SigType.EdDSA_SHA512_Ed25519); + // sess 2 offline + cl.createDestination(bas2, SigType.EdDSA_SHA512_Ed25519); + // sess 2 transient keys + SimpleDataStructure[] tr = ctx.keyGenerator().generateSigningKeys(SigType.EdDSA_SHA512_Ed25519); + SigningPublicKey tpub = (SigningPublicKey) tr[0]; + SigningPrivateKey tpriv = (SigningPrivateKey) tr[1]; + ByteArrayStream bas3 = new ByteArrayStream(800); + byte[] b2 = bas2.toByteArray(); + int pklen = SigType.EdDSA_SHA512_Ed25519.getPrivkeyLen(); + int tocopy = b2.length - pklen; + bas3.write(b2, 0, tocopy); + // zero out privkey + for (int i = 0; i < pklen; i++) { + bas3.write((byte) 0); + } + byte[] oprivb = new byte[pklen]; + System.arraycopy(b2, tocopy, oprivb, 0, pklen); + SigningPrivateKey opriv = new SigningPrivateKey(SigType.EdDSA_SHA512_Ed25519, oprivb); + // offline section + ByteArrayOutputStream baos = new ByteArrayOutputStream(70); + // expires + DataHelper.writeLong(bas3, 4, 0x7fffffff); + DataHelper.writeLong(baos, 4, 0x7fffffff); + // type + DataHelper.writeLong(bas3, 2, SigType.EdDSA_SHA512_Ed25519.getCode()); + DataHelper.writeLong(baos, 2, SigType.EdDSA_SHA512_Ed25519.getCode()); + // transient pubkey + byte[] tpubb = tpub.getData(); + bas3.write(tpubb); + baos.write(tpubb); + // sig + Signature sig = ctx.dsa().sign(baos.toByteArray(), opriv); + if (sig == null) + throw new IllegalArgumentException("Sig fail"); + bas3.write(sig.getData()); + // transient privkey + bas3.write(oprivb); + + Properties p = new Properties(); + I2PSession s1 = cl.createSession(bas1.asInputStream(), p); + I2PSession s2 = cl.createSession(bas3.asInputStream(), p); + Destination d1 = s1.getMyDestination(); + Destination d2 = s2.getMyDestination(); + + Properties opts = new Properties(); + opts.setProperty("foooooooooooo", "bar"); + opts.setProperty("a", "b"); + + // test 1: 1 to 2, normal + byte[] data1 = new byte[1024]; + ctx.random().nextBytes(data1); + byte[] dg1 = Datagram2.make(ctx, s1, data1, d2.calculateHash(), opts); + Datagram2 datag = Datagram2.load(ctx, s2, dg1); + byte[] data2 = datag.getPayload(); + Destination dr = datag.getSender(); + if (!dr.equals(d1)) { + System.out.println("ONLINE FAIL sender mismatch"); + } else if (!DataHelper.eq(data1, data2)) { + System.out.println("ONLINE FAIL data mismatch"); + System.out.println("Send Payload:\n" + HexDump.dump(data1)); + System.out.println("Rcv Payload:\n" + HexDump.dump(data2)); + } else { + System.out.println("ONLINE PASS"); + } + + // test 2: 2 to 1, offline + dg1 = Datagram2.make(ctx, s2, data1, d1.calculateHash(), opts); + datag = Datagram2.load(ctx, s1, dg1); + data2 = datag.getPayload(); + dr = datag.getSender(); + if (!dr.equals(d2)) { + System.out.println("OFFLINE FAIL sender mismatch"); + } else if (!DataHelper.eq(data1, data2)) { + System.out.println("OFFLINE FAIL data mismatch"); + System.out.println("Send Payload:\n" + HexDump.dump(data1)); + System.out.println("Rcv Payload:\n" + HexDump.dump(data2)); + } else { + System.out.println("OFFLINE PASS"); + } + } +*/ +} diff --git a/core/java/src/net/i2p/client/datagram/Datagram3.java b/core/java/src/net/i2p/client/datagram/Datagram3.java new file mode 100644 index 000000000..bc8794448 --- /dev/null +++ b/core/java/src/net/i2p/client/datagram/Datagram3.java @@ -0,0 +1,217 @@ +package net.i2p.client.datagram; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PClientFactory; +import net.i2p.client.I2PSession; +import net.i2p.crypto.KeyGenerator; +import net.i2p.crypto.SigType; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.util.ByteArrayStream; +import net.i2p.util.HexDump; + +/** + * Class for creating and loading I2P repliable datagrams version 3. + * Ref: Proposal 163 + * + *
+ +----+----+----+----+----+----+----+----+ + | | + + fromHash + + | | + + + + | | + +----+----+----+----+----+----+----+----+ + | flags | options (optional) | + +----+----+ + + ~ ~ + ~ ~ + +----+----+----+----+----+----+----+----+ + | | + ~ payload ~ + ~ ~ + | | + +----+----+----+----+----+----+----+----+ + *+ * + * @since 0.9.66 + */ +public class Datagram3 { + + private final Hash _from; + private final byte[] _payload; + private final Properties _options; + + private static final int INIT_DGRAM_BUFSIZE = 2*1024; + private static final int MIN_DGRAM_SIZE = 32 + 2; + private static final int MAX_DGRAM_BUFSIZE = 61*1024; + private static final byte VERSION_MASK = 0x0f; + private static final byte OPTIONS = 0x10; + private static final byte VERSION = 3; + + + /** + * As returned from load() + */ + private Datagram3(Hash dest, byte[] data, Properties options) { + _from = dest; + _payload = data; + _options = options; + } + + /** + * Make a repliable I2P datagram3 containing the specified payload. + * + * @param payload non-null Bytes to be contained in the I2P datagram. + * @return non-null, throws on all errors + * @throws DataFormatException if payload is too big + */ + public static byte[] make(I2PAppContext ctx, I2PSession session, byte[] payload) throws DataFormatException { + return make(ctx, session, payload, null); + } + + /** + * Make a repliable I2P datagram3 containing the specified payload. + * + * @param payload non-null Bytes to be contained in the I2P datagram. + * @param options may be null + * @return non-null, throws on all errors + * @throws DataFormatException if payload is too big + */ + public static byte[] make(I2PAppContext ctx, I2PSession session, byte[] payload, + Properties options) throws DataFormatException { + ByteArrayOutputStream out = new ByteArrayOutputStream(payload.length + 34); + try { + out.write(session.getMyDestination().calculateHash().getData()); + out.write((byte) 0); // high byte flags + byte flags = VERSION; + if (options != null && !options.isEmpty()) + flags |= OPTIONS; + out.write(flags); // low byte flags + if (options != null && !options.isEmpty()) + DataHelper.writeProperties(out, options); + out.write(payload); + if (out.size() > MAX_DGRAM_BUFSIZE) + throw new DataFormatException("Too big"); + } catch (IOException ioe) {} + return out.toByteArray(); + } + + /** + * Load an I2P repliable datagram3. + * + * @param dgram non-null I2P repliable datagram to be loaded + * @return non-null, throws on all errors + * @throws DataFormatException If there is an error in the datagram format + */ + public static Datagram3 load(I2PAppContext ctx, I2PSession session, byte[] dgram) throws DataFormatException { + if (dgram.length < MIN_DGRAM_SIZE) + throw new DataFormatException("Datagram3 too small: " + dgram.length); + ByteArrayInputStream in = new ByteArrayInputStream(dgram); + try { + Hash rxDest = Hash.create(in); + in.read(); // ignore high byte of flags + int flags = in.read(); + int version = flags & VERSION_MASK; + if (version != VERSION) + throw new DataFormatException("Bad version " + version); + int optlen = 0; + Properties options = null; + if ((flags & OPTIONS) != 0) { + in.mark(0); + optlen = (int) DataHelper.readLong(in, 2); + if (optlen > 0) { + in.reset(); + if (in.available() < optlen) + throw new DataFormatException("too small for options: " + dgram.length); + options = DataHelper.readProperties(in, null, true); + } + optlen += 2; + } + int datalen = dgram.length - (Hash.HASH_LENGTH + 2 + optlen); + byte[] payload = new byte[datalen]; + System.arraycopy(dgram, Hash.HASH_LENGTH + 2 + optlen, payload, 0, datalen); + return new Datagram3(rxDest, payload, options); + } catch (IOException e) { + throw new DataFormatException("Error loading datagram", e); + } + } + + /** + * Get the payload carried by an I2P repliable datagram (previously loaded + * with the load() method) + * + * @return A byte array containing the datagram payload + */ + public byte[] getPayload() { + return _payload; + } + + /** + * Get the sender of an I2P repliable datagram (previously loaded with the + * load() method) + * + * @return The Hash of the Destination of the I2P repliable datagram sender + */ + public Hash getSender() { + return _from; + } + + /** + * Get the options of an I2P repliable datagram (previously loaded with the + * load() method), if any + * + * @return options or null + */ + public Properties getOptions() { + return _options; + } + +/* + public static void main(String[] args) throws Exception { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + I2PClient cl = I2PClientFactory.createClient(); + ByteArrayStream bas1 = new ByteArrayStream(800); + ByteArrayStream bas2 = new ByteArrayStream(800); + // sess 1 + cl.createDestination(bas1, SigType.EdDSA_SHA512_Ed25519); + // sess 2 + cl.createDestination(bas2, SigType.EdDSA_SHA512_Ed25519); + + Properties p = new Properties(); + I2PSession s1 = cl.createSession(bas1.asInputStream(), p); + I2PSession s2 = cl.createSession(bas2.asInputStream(), p); + Destination d1 = s1.getMyDestination(); + Destination d2 = s2.getMyDestination(); + + Properties opts = new Properties(); + opts.setProperty("foooooooooooo", "bar"); + opts.setProperty("a", "b"); + + byte[] data1 = new byte[1024]; + ctx.random().nextBytes(data1); + byte[] dg1 = Datagram3.make(ctx, s1, data1, opts); + Datagram3 datag = Datagram3.load(ctx, s2, dg1); + byte[] data2 = datag.getPayload(); + Hash dr = datag.getSender(); + if (!dr.equals(d1.calculateHash())) { + System.out.println("FAIL sender mismatch"); + } else if (!DataHelper.eq(data1, data2)) { + System.out.println("FAIL data mismatch"); + System.out.println("Send Payload:\n" + HexDump.dump(data1)); + System.out.println("Rcv Payload:\n" + HexDump.dump(data2)); + } else { + System.out.println("PASS"); + } + } +*/ +}