From 0c4b39bdee4c2b508438e390b7584aca46b863ee Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Tue, 31 Mar 2015 21:06:42 +0200 Subject: [PATCH] It's now possible to send a 'version' message that will be accepted by the other node. --- README.md | 7 +- .../ch/dissem/bitmessage/entity/Addr.java | 4 +- .../bitmessage/entity/BitmessageAddress.java | 71 ++++++++ .../{MessagePayload.java => Command.java} | 4 +- .../ch/dissem/bitmessage/entity/GetData.java | 70 +++++++ .../java/ch/dissem/bitmessage/entity/Inv.java | 70 +++++++ .../bitmessage/entity/NetworkMessage.java | 26 ++- .../bitmessage/entity/ObjectMessage.java | 109 +++++++++++ .../dissem/bitmessage/entity/Streamable.java | 2 +- .../ch/dissem/bitmessage/entity/VerAck.java | 6 +- .../ch/dissem/bitmessage/entity/Version.java | 35 ++-- .../entity/payload/GenericPayload.java | 36 ++++ .../bitmessage/entity/payload/GetPubkey.java | 50 +++++ .../entity/{ => payload}/ObjectPayload.java | 8 +- .../bitmessage/entity/payload/Pubkey.java | 30 +++ .../bitmessage/entity/payload/V2Pubkey.java | 59 ++++++ .../bitmessage/entity/payload/V3Pubkey.java | 45 +++++ .../bitmessage/entity/payload/V4Pubkey.java | 62 +++++++ .../entity/valueobject/NetworkAddress.java | 38 +++- .../ch/dissem/bitmessage/factory/Factory.java | 54 ++++++ .../bitmessage/factory/V3MessageFactory.java | 172 ++++++++++++++++++ .../ch/dissem/bitmessage/ports/Inventory.java | 4 +- .../ports/NetworkMessageReceiver.java | 8 +- .../ports/NetworkMessageSender.java | 2 +- .../ch/dissem/bitmessage/utils/Base58.java | 32 ++++ .../ch/dissem/bitmessage/utils/Decode.java | 86 +++++++++ .../ch/dissem/bitmessage/utils/Encode.java | 4 +- .../ch/dissem/bitmessage/utils/Security.java | 52 ++++++ .../dissem/bitmessage/utils/DecodeTest.java | 43 +++++ .../bitmessage/networking/NetworkNode.java | 151 +++++++++++++++ .../networking/NetworkNodeTest.java | 56 ++++++ 31 files changed, 1351 insertions(+), 45 deletions(-) create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java rename domain/src/main/java/ch/dissem/bitmessage/entity/{MessagePayload.java => Command.java} (87%) create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/GetData.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/Inv.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java rename domain/src/main/java/ch/dissem/bitmessage/entity/{ => payload}/ObjectPayload.java (74%) create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/utils/Base58.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java create mode 100644 domain/src/main/java/ch/dissem/bitmessage/utils/Security.java create mode 100644 domain/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java create mode 100644 networking/src/main/java/ch/dissem/bitmessage/networking/NetworkNode.java create mode 100644 networking/src/test/java/ch/dissem/bitmessage/networking/NetworkNodeTest.java diff --git a/README.md b/README.md index 591d299..bded415 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ Jabit ===== -A Java implementation for the Bitmessage protocol. To build, use command `gradle build`. \ No newline at end of file +A Java implementation for the Bitmessage protocol. To build, use command `gradle build`. Note that for some tests to run, a standard Bitmessage client needs to run on the same system, using port 8444 (the default port). + +Security +-------- + +If you're able to audit Jabit to verify its security, you would be very very welcome. Please be aware though that the official Bitmessage project would like an audit, too, and they were first in line. \ No newline at end of file diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Addr.java b/domain/src/main/java/ch/dissem/bitmessage/entity/Addr.java index b2b09e4..8fc13cc 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Addr.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/Addr.java @@ -25,9 +25,9 @@ import java.util.ArrayList; import java.util.List; /** - * Created by chris on 13.03.15. + * The 'addr' command holds a list of known active Bitmessage nodes. */ -public class Addr implements MessagePayload { +public class Addr implements Command { private final List addresses; private Addr(Builder builder) { diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java b/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java new file mode 100644 index 0000000..73adc67 --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity; + +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.utils.Base58; +import ch.dissem.bitmessage.utils.Encode; +import ch.dissem.bitmessage.utils.Security; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static ch.dissem.bitmessage.utils.Security.ripemd160; +import static ch.dissem.bitmessage.utils.Security.sha512; + +/** + * A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address + * holding private keys. + */ +public abstract class BitmessageAddress { + private long version; + private long streamNumber; + + private Pubkey pubkey; + + public BitmessageAddress(Pubkey pubkey) { + this.pubkey = pubkey; + } + + public BitmessageAddress(String address) { + Base58.decode(address.substring(3)); + } + + @Override + public String toString() { + try { + byte[] combinedKeys = new byte[pubkey.getSigningKey().length + pubkey.getEncryptionKey().length]; + System.arraycopy(pubkey.getSigningKey(), 0, combinedKeys, 0, pubkey.getSigningKey().length); + System.arraycopy(pubkey.getEncryptionKey(), 0, combinedKeys, pubkey.getSigningKey().length, pubkey.getEncryptionKey().length); + + byte[] hash = ripemd160(sha512(combinedKeys)); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + Encode.varInt(version, stream); + Encode.varInt(streamNumber, stream); + stream.write(hash); + + byte[] checksum = Security.doubleSha512(stream.toByteArray()); + for (int i = 0; i < 4; i++) { + stream.write(checksum[i]); + } + return "BM-" + Base58.encode(stream.toByteArray()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java b/domain/src/main/java/ch/dissem/bitmessage/entity/Command.java similarity index 87% rename from domain/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java rename to domain/src/main/java/ch/dissem/bitmessage/entity/Command.java index 0f1df1f..68e0a62 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/MessagePayload.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/Command.java @@ -17,8 +17,8 @@ package ch.dissem.bitmessage.entity; /** - * Created by chris on 10.03.15. + * A command can hold a network message payload */ -public interface MessagePayload extends Streamable { +public interface Command extends Streamable { String getCommand(); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/GetData.java b/domain/src/main/java/ch/dissem/bitmessage/entity/GetData.java new file mode 100644 index 0000000..5cab3b5 --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/GetData.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity; + +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.utils.Encode; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.LinkedList; +import java.util.List; + +/** + * The 'getdata' command is used to request objects from a node. + */ +public class GetData implements Command { + List inventory; + + private GetData(Builder builder) { + inventory = builder.inventory; + } + + @Override + public String getCommand() { + return "getdata"; + } + + @Override + public void write(OutputStream stream) throws IOException { + Encode.varInt(inventory.size(), stream); + for (InventoryVector iv : inventory) { + iv.write(stream); + } + } + + public static final class Builder { + private List inventory = new LinkedList<>(); + + public Builder() { + } + + public Builder addInventoryVector(InventoryVector inventoryVector) { + this.inventory.add(inventoryVector); + return this; + } + + public Builder inventory(List inventory) { + this.inventory = inventory; + return this; + } + + public GetData build() { + return new GetData(this); + } + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Inv.java b/domain/src/main/java/ch/dissem/bitmessage/entity/Inv.java new file mode 100644 index 0000000..19d8161 --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/Inv.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity; + +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.utils.Encode; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.LinkedList; +import java.util.List; + +/** + * The 'inv' command holds up to 50000 inventory vectors, i.e. hashes of inventory items. + */ +public class Inv implements Command { + private List inventory; + + private Inv(Builder builder) { + inventory = builder.inventory; + } + + @Override + public String getCommand() { + return "inv"; + } + + @Override + public void write(OutputStream stream) throws IOException { + Encode.varInt(inventory.size(), stream); + for (InventoryVector iv : inventory) { + iv.write(stream); + } + } + + public static final class Builder { + private List inventory = new LinkedList<>(); + + public Builder() { + } + + public Builder addInventoryVector(InventoryVector inventoryVector) { + this.inventory.add(inventoryVector); + return this; + } + + public Builder inventory(List inventory) { + this.inventory = inventory; + return this; + } + + public Inv build() { + return new Inv(this); + } + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java b/domain/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java index c8b7518..db6dcd9 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java @@ -16,28 +16,35 @@ package ch.dissem.bitmessage.entity; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; import ch.dissem.bitmessage.utils.Encode; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.security.GeneralSecurityException; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; +import static ch.dissem.bitmessage.utils.Security.sha512; + /** - * Created by chris on 10.03.15. + * A network message is exchanged between two nodes. */ public class NetworkMessage implements Streamable { /** * Magic value indicating message origin network, and used to seek to next message when stream state is unknown */ - private final static int MAGIC = 0xE9BEB4D9; + public final static int MAGIC = 0xE9BEB4D9; + public final static byte[] MAGIC_BYTES = ByteBuffer.allocate(4).putInt(MAGIC).array(); - private MessagePayload payload; + private final NetworkAddress targetNode; - public NetworkMessage(MessagePayload payload) { + private final Command payload; + + public NetworkMessage(NetworkAddress target, Command payload) { + this.targetNode = target; this.payload = payload; } @@ -45,18 +52,21 @@ public class NetworkMessage implements Streamable { * First 4 bytes of sha512(payload) */ private byte[] getChecksum(byte[] bytes) throws NoSuchProviderException, NoSuchAlgorithmException { - MessageDigest mda = MessageDigest.getInstance("SHA-512"); - byte[] d = mda.digest(bytes); + byte[] d = sha512(bytes); return new byte[]{d[0], d[1], d[2], d[3]}; } /** * The actual data, a message or an object. Not to be confused with objectPayload. */ - public MessagePayload getPayload() { + public Command getPayload() { return payload; } + public NetworkAddress getTargetNode() { + return targetNode; + } + @Override public void write(OutputStream stream) throws IOException { // magic diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java b/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java new file mode 100644 index 0000000..b6a245c --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity; + +import ch.dissem.bitmessage.entity.payload.ObjectPayload; +import ch.dissem.bitmessage.utils.Encode; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * The 'object' command sends an object that is shared throughout the network. + */ +public class ObjectMessage implements Command { + private long nonce; + private long expiresTime; + private long objectType; + /** + * The object's version + */ + private long version; + private long streamNumber; + + private ObjectPayload payload; + + private ObjectMessage(Builder builder) { + nonce = builder.nonce; + expiresTime = builder.expiresTime; + objectType = builder.objectType; + version = builder.version; + streamNumber = builder.streamNumber; + payload = builder.payload; + } + + @Override + public String getCommand() { + return "object"; + } + + @Override + public void write(OutputStream stream) throws IOException { + Encode.int64(nonce, stream); + Encode.int64(expiresTime, stream); + Encode.int32(objectType, stream); + Encode.varInt(version, stream); + Encode.varInt(streamNumber, stream); + payload.write(stream); + } + + public static final class Builder { + private long nonce; + private long expiresTime; + private long objectType; + private long version; + private long streamNumber; + private ObjectPayload payload; + + public Builder() { + } + + public Builder nonce(long nonce) { + this.nonce = nonce; + return this; + } + + public Builder expiresTime(long expiresTime) { + this.expiresTime = expiresTime; + return this; + } + + public Builder objectType(long objectType) { + this.objectType = objectType; + return this; + } + + public Builder version(long version) { + this.version = version; + return this; + } + + public Builder streamNumber(long streamNumber) { + this.streamNumber = streamNumber; + return this; + } + + public Builder payload(ObjectPayload payload) { + this.payload = payload; + return this; + } + + public ObjectMessage build() { + return new ObjectMessage(this); + } + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Streamable.java b/domain/src/main/java/ch/dissem/bitmessage/entity/Streamable.java index 389a242..9ee4cf9 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Streamable.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/Streamable.java @@ -20,7 +20,7 @@ import java.io.IOException; import java.io.OutputStream; /** - * Created by chris on 10.03.15. + * An object that can be written to an {@link OutputStream} */ public interface Streamable { void write(OutputStream stream) throws IOException; diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/VerAck.java b/domain/src/main/java/ch/dissem/bitmessage/entity/VerAck.java index 97a0010..19748d0 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/VerAck.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/VerAck.java @@ -20,9 +20,9 @@ import java.io.IOException; import java.io.OutputStream; /** - * Created by chris on 10.03.15. + * The 'verack' command answers a 'version' command, accepting the other node's version. */ -public class VerAck implements MessagePayload { +public class VerAck implements Command { @Override public String getCommand() { return "verack"; @@ -30,6 +30,6 @@ public class VerAck implements MessagePayload { @Override public void write(OutputStream stream) throws IOException { - // NO OP + // 'verack' doesn't have any payload, so there is nothing to write } } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/Version.java b/domain/src/main/java/ch/dissem/bitmessage/entity/Version.java index e22027d..ea621eb 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/Version.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/Version.java @@ -24,9 +24,10 @@ import java.io.OutputStream; import java.util.Random; /** - * Created by chris on 10.03.15. + * The 'version' command advertises this node's latest supported protocol version upon initiation. */ -public class Version implements MessagePayload { +public class Version implements Command { + public static final int CURRENT = 3; /** * Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's * version is lower but continue with the connection if it is higher. @@ -115,7 +116,7 @@ public class Version implements MessagePayload { @Override public String getCommand() { - return "ver"; + return "version"; } @Override @@ -123,8 +124,8 @@ public class Version implements MessagePayload { Encode.int32(version, stream); Encode.int64(services, stream); Encode.int64(timestamp, stream); - addrRecv.write(stream); - addrFrom.write(stream); + addrRecv.write(stream, true); + addrFrom.write(stream, true); Encode.int64(nonce, stream); Encode.varString(userAgent, stream); Encode.varIntList(streamNumbers, stream); @@ -132,18 +133,28 @@ public class Version implements MessagePayload { public static final class Builder { - private int version = 3; - private long services = 1; // This is a normal network node - private long timestamp = System.currentTimeMillis() / 1000; + private int version; + private long services; + private long timestamp; private NetworkAddress addrRecv; private NetworkAddress addrFrom; - private long nonce = new Random().nextInt(); - private String userAgent = "/Jabit:0.0.1/"; - private long[] streamNumbers = {1}; + private long nonce; + private String userAgent; + private long[] streamNumbers; public Builder() { } + public Builder defaults() { + version = CURRENT; + services = 1; + timestamp = System.currentTimeMillis() / 1000; + nonce = new Random().nextInt(); + userAgent = "/Jabit:0.0.1/"; + streamNumbers = new long[]{1}; + return this; + } + public Builder version(int version) { this.version = version; return this; @@ -179,7 +190,7 @@ public class Version implements MessagePayload { return this; } - public Builder streamNumbers(long... streamNumbers) { + public Builder streams(long... streamNumbers) { this.streamNumbers = streamNumbers; return this; } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java new file mode 100644 index 0000000..874d1bc --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity.payload; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Created by chris on 24.03.15. + */ +public class GenericPayload implements ObjectPayload { + private byte[] data; + + public GenericPayload(byte[] data) { + this.data = data; + } + + @Override + public void write(OutputStream stream) throws IOException { + stream.write(data); + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java new file mode 100644 index 0000000..9a551d0 --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity.payload; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Created by chris on 24.03.15. + */ +public class GetPubkey implements ObjectPayload { + private byte[] ripe; + private byte[] tag; + + public GetPubkey(byte[] ripeOrTag) { + switch (ripeOrTag.length) { + case 20: + ripe = ripeOrTag; + break; + case 32: + tag = ripeOrTag; + break; + default: + throw new RuntimeException("ripe (20 bytes) or tag (32 bytes) expected, but pubkey was " + ripeOrTag.length + " bytes."); + } + } + + @Override + public void write(OutputStream stream) throws IOException { + if (tag != null) { + stream.write(tag); + } else { + stream.write(ripe); + } + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectPayload.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java similarity index 74% rename from domain/src/main/java/ch/dissem/bitmessage/entity/ObjectPayload.java rename to domain/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java index f7b280f..52599be 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/ObjectPayload.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java @@ -14,10 +14,12 @@ * limitations under the License. */ -package ch.dissem.bitmessage.entity; +package ch.dissem.bitmessage.entity.payload; + +import ch.dissem.bitmessage.entity.Streamable; /** - * Created by chris on 16.03.15. + * The payload of an 'object' command. This is shared by the network. */ -public interface ObjectPayload { +public interface ObjectPayload extends Streamable { } diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java new file mode 100644 index 0000000..859effc --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity.payload; + +/** + * Created by chris on 24.03.15. + */ +public interface Pubkey extends ObjectPayload { + long getVersion(); + + long getStream(); + + byte[] getSigningKey(); + + byte[] getEncryptionKey(); +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java new file mode 100644 index 0000000..fef5874 --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity.payload; + +import ch.dissem.bitmessage.utils.Encode; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Created by chris on 24.03.15. + */ +public class V2Pubkey implements Pubkey { + protected long streamNumber; + protected long behaviorBitfield; + protected byte[] publicSigningKey; + protected byte[] publicEncryptionKey; + + @Override + public long getVersion() { + return 2; + } + + @Override + public long getStream() { + return streamNumber; + } + + @Override + public byte[] getSigningKey() { + return publicSigningKey; + } + + @Override + public byte[] getEncryptionKey() { + return publicEncryptionKey; + } + + @Override + public void write(OutputStream stream) throws IOException { + Encode.int32(behaviorBitfield, stream); + stream.write(publicSigningKey); + stream.write(publicEncryptionKey); + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java new file mode 100644 index 0000000..8b9b088 --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity.payload; + +import ch.dissem.bitmessage.utils.Encode; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Created by chris on 27.03.15. + */ +public class V3Pubkey extends V2Pubkey { + long nonceTrialsPerByte; + long extraBytes; + byte[] signature; + + @Override + public void write(OutputStream stream) throws IOException { + super.write(stream); + Encode.varInt(nonceTrialsPerByte, stream); + Encode.varInt(extraBytes, stream); + Encode.varInt(signature.length, stream); + stream.write(signature); + } + + @Override + public long getVersion() { + return 3; + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java new file mode 100644 index 0000000..bb31ab8 --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.entity.payload; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Created by chris on 27.03.15. + */ +public class V4Pubkey implements Pubkey { + private long streamNumber; + private byte[] tag; + private byte[] encrypted; + private V3Pubkey decrypted; + + public V4Pubkey(V3Pubkey decrypted) { + this.decrypted = decrypted; + + // TODO: this.tag = new BitmessageAddress(this).doubleHash + } + + @Override + public void write(OutputStream stream) throws IOException { + stream.write(tag); + stream.write(encrypted); + } + + @Override + public long getVersion() { + return 4; + } + + @Override + public long getStream() { + return streamNumber; + } + + @Override + public byte[] getSigningKey() { + return decrypted.getSigningKey(); + } + + @Override + public byte[] getEncryptionKey() { + return decrypted.getEncryptionKey(); + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java b/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java index be1864d..545781c 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java +++ b/domain/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java @@ -21,13 +21,15 @@ import ch.dissem.bitmessage.utils.Encode; import java.io.IOException; import java.io.OutputStream; +import java.net.Inet6Address; import java.net.InetAddress; +import java.net.Socket; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Objects; /** - * Created by chris on 10.03.15. + * A node's address. It's written in IPv6 format. */ public class NetworkAddress implements Streamable { private long time; @@ -35,7 +37,7 @@ public class NetworkAddress implements Streamable { /** * Stream number for this node */ - private int stream; + private long stream; /** * same service(s) listed in version @@ -88,8 +90,14 @@ public class NetworkAddress implements Streamable { @Override public void write(OutputStream stream) throws IOException { - Encode.int64(time, stream); - Encode.int32(this.stream, stream); + write(stream, false); + } + + public void write(OutputStream stream, boolean light) throws IOException { + if (!light) { + Encode.int64(time, stream); + Encode.int32(this.stream, stream); + } Encode.int64(services, stream); stream.write(ipv6); Encode.int16(port, stream); @@ -97,7 +105,7 @@ public class NetworkAddress implements Streamable { public static final class Builder { private long time; - private int stream; + private long stream; private long services = 1; private byte[] ipv6; private int port; @@ -110,7 +118,7 @@ public class NetworkAddress implements Streamable { return this; } - public Builder stream(final int stream) { + public Builder stream(final long stream) { this.stream = stream; return this; } @@ -120,6 +128,24 @@ public class NetworkAddress implements Streamable { return this; } + public Builder ip(InetAddress inetAddress) { + byte[] addr = inetAddress.getAddress(); + if (addr.length == 16) { + this.ipv6 = addr; + } else if (addr.length == 4) { + this.ipv6 = new byte[16]; + System.arraycopy(addr, 0, this.ipv6, 12, 4); + } else { + throw new IllegalArgumentException("Weird address " + inetAddress); + } + return this; + } + + public Builder ipv6(byte[] ipv6) { + this.ipv6 = ipv6; + return this; + } + public Builder ipv6(int p00, int p01, int p02, int p03, int p04, int p05, int p06, int p07, int p08, int p09, int p10, int p11, diff --git a/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java b/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java new file mode 100644 index 0000000..2a57a0a --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/factory/Factory.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.factory; + +import ch.dissem.bitmessage.entity.NetworkMessage; +import ch.dissem.bitmessage.entity.payload.GenericPayload; +import ch.dissem.bitmessage.entity.payload.GetPubkey; +import ch.dissem.bitmessage.entity.payload.ObjectPayload; +import ch.dissem.bitmessage.utils.Decode; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Creates {@link NetworkMessage} objects from {@link InputStream InputStreams} + */ +public class Factory { + public static NetworkMessage getNetworkMessage(int version, InputStream stream) throws IOException { + return new V3MessageFactory().read(stream); + } + + static ObjectPayload getObjectPayload(long objectType, long version, InputStream stream, int length) throws IOException { + if (objectType < 4) { + switch ((int) objectType) { + case 0: // getpubkey + return new GetPubkey(Decode.bytes(stream, length)); + case 1: // pubkey + break; + case 2: // msg + break; + case 3: // broadcast + break; + } + throw new RuntimeException("This must not happen, someone broke something in the code!"); + } else { + // passthrough message + return new GenericPayload(Decode.bytes(stream, length)); + } + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java b/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java new file mode 100644 index 0000000..a6d6bbe --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java @@ -0,0 +1,172 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.factory; + +import ch.dissem.bitmessage.entity.*; +import ch.dissem.bitmessage.entity.payload.ObjectPayload; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.utils.Decode; +import ch.dissem.bitmessage.utils.Security; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Creates protocol v3 network messages from {@link InputStream InputStreams} + */ +class V3MessageFactory { + public NetworkMessage read(InputStream stream) throws IOException { + if (testMagic(stream)) { + String command = getCommand(stream); + int length = (int) Decode.uint32(stream); + byte[] checksum = Decode.bytes(stream, 4); + + byte[] payloadBytes = Decode.bytes(stream, length); + + if (testChecksum(checksum, payloadBytes)) { + Command payload = getPayload(command, new ByteArrayInputStream(payloadBytes), length); + return new NetworkMessage(payload); + } else { + throw new IOException("Checksum failed for message '" + command + "'"); + } + } + return null; + } + + private Command getPayload(String command, InputStream stream, int length) throws IOException { + switch (command) { + case "version": + return parseVersion(stream); + case "verack": + return new VerAck(); + case "addr": + return parseAddr(stream); + case "inv": + return parseInv(stream); + case "getdata": + return parseGetData(stream); + case "object": + return parseObject(stream, length); + default: + return null; + } + } + + private ObjectMessage parseObject(InputStream stream, int length) throws IOException { + long nonce = Decode.int64(stream); + long expiresTime = Decode.int64(stream); + long objectType = Decode.uint32(stream); + long version = Decode.varInt(stream); + long streamNumber = Decode.varInt(stream); + + ObjectPayload payload = Factory.getObjectPayload(objectType, version, stream, length); + + return new ObjectMessage.Builder() + .nonce(nonce) + .expiresTime(expiresTime) + .objectType(objectType) + .version(version) + .streamNumber(streamNumber) + .payload(payload) + .build(); + } + + private GetData parseGetData(InputStream stream) throws IOException { + long count = Decode.varInt(stream); + GetData.Builder builder = new GetData.Builder(); + for (int i = 0; i < count; i++) { + builder.addInventoryVector(parseInventoryVector(stream)); + } + return builder.build(); + } + + private Inv parseInv(InputStream stream) throws IOException { + long count = Decode.varInt(stream); + Inv.Builder builder = new Inv.Builder(); + for (int i = 0; i < count; i++) { + builder.addInventoryVector(parseInventoryVector(stream)); + } + return builder.build(); + } + + private Addr parseAddr(InputStream stream) throws IOException { + long count = Decode.varInt(stream); + Addr.Builder builder = new Addr.Builder(); + for (int i = 0; i < count; i++) { + builder.addAddress(parseAddress(stream)); + } + return builder.build(); + } + + private Version parseVersion(InputStream stream) throws IOException { + int version = Decode.int32(stream); + long services = Decode.int64(stream); + long timestamp = Decode.int64(stream); + NetworkAddress addrRecv = parseAddress(stream); + NetworkAddress addrFrom = parseAddress(stream); + long nonce = Decode.int64(stream); + String userAgent = Decode.varString(stream); + long[] streamNumbers = Decode.varIntList(stream); + + return new Version.Builder() + .version(version) + .services(services) + .timestamp(timestamp) + .addrRecv(addrRecv).addrFrom(addrFrom) + .nonce(nonce) + .userAgent(userAgent) + .streams(streamNumbers).build(); + } + + private InventoryVector parseInventoryVector(InputStream stream) throws IOException { + return new InventoryVector(Decode.bytes(stream, 32)); + } + + private NetworkAddress parseAddress(InputStream stream) throws IOException { + long time = Decode.int64(stream); + long streamNumber = Decode.uint32(stream); // This isn't consistent, not sure if this is correct + long services = Decode.int64(stream); + byte[] ipv6 = Decode.bytes(stream, 16); + int port = Decode.uint16(stream); + return new NetworkAddress.Builder().time(time).stream(streamNumber).services(services).ipv6(ipv6).port(port).build(); + } + + private boolean testChecksum(byte[] checksum, byte[] payload) { + byte[] payloadChecksum = Security.sha512(payload); + for (int i = 0; i < checksum.length; i++) { + if (checksum[i] != payloadChecksum[i]) { + return false; + } + } + return true; + } + + private String getCommand(InputStream stream) throws IOException { + byte[] bytes = new byte[12]; + stream.read(bytes); + return new String(bytes, "ASCII"); + } + + private boolean testMagic(InputStream stream) throws IOException { + for (byte b : NetworkMessage.MAGIC_BYTES) { + if (b != stream.read()) return false; + } + return true; + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/Inventory.java b/domain/src/main/java/ch/dissem/bitmessage/ports/Inventory.java index 68f7565..7fe2eda 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/Inventory.java +++ b/domain/src/main/java/ch/dissem/bitmessage/ports/Inventory.java @@ -16,13 +16,13 @@ package ch.dissem.bitmessage.ports; -import ch.dissem.bitmessage.entity.ObjectPayload; +import ch.dissem.bitmessage.entity.payload.ObjectPayload; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import java.util.List; /** - * Created by chris on 16.03.15. + * The Inventory stores and retrieves objects, cleans up outdated objects and can tell which objects are still missing. */ public interface Inventory { public List getInventory(); diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkMessageReceiver.java b/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkMessageReceiver.java index 6fe83c3..530f1c8 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkMessageReceiver.java +++ b/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkMessageReceiver.java @@ -19,13 +19,15 @@ package ch.dissem.bitmessage.ports; import ch.dissem.bitmessage.entity.NetworkMessage; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import java.io.IOException; + /** - * Created by chris on 16.03.15. + * Handles incoming messages */ public interface NetworkMessageReceiver { - public void registerListener(int port); + public void registerListener(int port) throws IOException; - public void registerListener(NetworkAddress node, MessageListener listener); + public void registerListener(NetworkAddress node, MessageListener listener) throws IOException; public static interface MessageListener { public void receive(NetworkMessage message); diff --git a/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkMessageSender.java b/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkMessageSender.java index 7218c9b..bac6803 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkMessageSender.java +++ b/domain/src/main/java/ch/dissem/bitmessage/ports/NetworkMessageSender.java @@ -20,7 +20,7 @@ import ch.dissem.bitmessage.entity.NetworkMessage; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; /** - * Created by chris on 16.03.15. + * Sends messages */ public interface NetworkMessageSender { public void send(NetworkAddress node, NetworkMessage message); diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Base58.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Base58.java new file mode 100644 index 0000000..95de3a6 --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Base58.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +/** + * Base58 encoder and decoder + */ +public class Base58 { + private static char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); + + public static String encode(byte[] input) { + return null; // TODO + } + + public static byte[] decode(String input) { + return null; // TODO + } +} \ No newline at end of file diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java new file mode 100644 index 0000000..dad6b4a --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Decode.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * This class handles decoding simple types from byte stream, according to + * https://bitmessage.org/wiki/Protocol_specification#Common_structures + */ +public class Decode { + public static byte[] bytes(InputStream stream, int count) throws IOException { + byte[] result = new byte[count]; + stream.read(result); + return result; + } + + public static long[] varIntList(InputStream stream) throws IOException { + int length = (int) varInt(stream); + long[] result = new long[length]; + + for (int i = 0; i < length; i++) { + result[i] = varInt(stream); + } + return result; + } + + public static long varInt(InputStream stream) throws IOException { + int first = stream.read(); + switch (first) { + case 0xfd: + return uint16(stream); + case 0xfe: + return uint32(stream); + case 0xff: + return int64(stream); + default: + return first; + } + } + + public static int uint8(InputStream stream) throws IOException { + return stream.read(); + } + + public static int uint16(InputStream stream) throws IOException { + return stream.read() * 256 + stream.read(); + } + + public static long uint32(InputStream stream) throws IOException { + return stream.read() * 16777216L + stream.read() * 65536L + stream.read() * 256L + stream.read(); + } + + public static int int32(InputStream stream) throws IOException { + return ByteBuffer.wrap(bytes(stream, 4)).getInt(); + } + + public static long int64(InputStream stream) throws IOException { + return ByteBuffer.wrap(bytes(stream, 8)).getLong(); + } + + public static String varString(InputStream stream) throws IOException { + int length = (int) varInt(stream); + // FIXME: technically, it says the length in characters, but I think this one might be correct + byte[] bytes = new byte[length]; + // FIXME: I'm also not quite sure if this works, maybe the read return value needs to be handled properly + stream.read(bytes); + return new String(bytes, "utf-8"); + } +} diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java index 9c1fcc1..1e925c5 100644 --- a/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Encode.java @@ -21,7 +21,8 @@ import java.io.OutputStream; import java.nio.ByteBuffer; /** - * Created by chris on 13.03.15. + * This class handles encoding simple types from byte stream, according to + * https://bitmessage.org/wiki/Protocol_specification#Common_structures */ public class Encode { public static void varIntList(long[] values, OutputStream stream) throws IOException { @@ -71,6 +72,7 @@ public class Encode { public static void varString(String value, OutputStream stream) throws IOException { byte[] bytes = value.getBytes("utf-8"); + // FIXME: technically, it says the length in characters, but I think this one might be correct varInt(bytes.length, stream); stream.write(bytes); } diff --git a/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java b/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java new file mode 100644 index 0000000..90612ff --- /dev/null +++ b/domain/src/main/java/ch/dissem/bitmessage/utils/Security.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Provides some methods to help with hashing and encryption. + */ +public class Security { + public static byte[] sha512(byte[] data) { + try { + MessageDigest mda = MessageDigest.getInstance("SHA-512"); + return mda.digest(data); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static byte[] doubleSha512(byte[] data) { + try { + MessageDigest mda = MessageDigest.getInstance("SHA-512"); + return mda.digest(mda.digest(data)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static byte[] ripemd160(byte[] data) { + try { + MessageDigest mda = MessageDigest.getInstance("RIPEMD-160"); + return mda.digest(data); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } +} diff --git a/domain/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java b/domain/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java new file mode 100644 index 0000000..8c18ee7 --- /dev/null +++ b/domain/src/test/java/ch/dissem/bitmessage/utils/DecodeTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import org.junit.Test; + +import java.io.*; + +import static org.junit.Assert.assertEquals; + +/** + * Created by chris on 20.03.15. + */ +public class DecodeTest { + @Test + public void ensureDecodingWorks() throws Exception { + // This should test all relevant cases for var_int and therefore also uint_16, uint_32 and int_64 + testCodec(0); + for (long i = 1; i > 0; i = 3 * i + 7) { + testCodec(i); + } + } + + private void testCodec(long number) throws IOException { + ByteArrayOutputStream is = new ByteArrayOutputStream(); + Encode.varInt(number, is); + assertEquals(number, Decode.varInt(new ByteArrayInputStream(is.toByteArray()))); + } +} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/NetworkNode.java b/networking/src/main/java/ch/dissem/bitmessage/networking/NetworkNode.java new file mode 100644 index 0000000..dc21c4a --- /dev/null +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/NetworkNode.java @@ -0,0 +1,151 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.networking; + +import ch.dissem.bitmessage.entity.NetworkMessage; +import ch.dissem.bitmessage.entity.Version; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.NetworkMessageReceiver; +import ch.dissem.bitmessage.ports.NetworkMessageSender; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Handles all the networky stuff. + */ +public class NetworkNode implements NetworkMessageSender, NetworkMessageReceiver { + private final BlockingQueue sendingQueue = new LinkedBlockingQueue<>(); + private final ExecutorService pool; + + private final Map sockets = new HashMap<>(); + private final Map versions = new HashMap<>(); + + /** + * This is only to be used where it's ignored + */ + private final static NetworkAddress LOCALHOST = new NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(8444).build(); + + public NetworkNode() { + pool = Executors.newCachedThreadPool(); + + new Thread(new Runnable() { + @Override + public void run() { + while (true) { + try { + NetworkMessage message = sendingQueue.take(); + Socket socket = getSocket(message.getTargetNode()); + message.write(socket.getOutputStream()); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }, "Sender"); + } + + @Override + public void registerListener(final int port) throws IOException { + final ServerSocket serverSocket = new ServerSocket(port); + pool.execute(new Runnable() { + @Override + public void run() { + try { + Socket socket = serverSocket.accept(); + socket.setSoTimeout(20000); + // FIXME: addd to sockets + registerListener(getVersion(null), socket, new MessageListener() { + @Override + public void receive(NetworkMessage message) { + // TODO + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + + @Override + public void registerListener(final NetworkAddress node, final MessageListener listener) throws IOException { + final Socket socket = getSocket(node); + final int version = getVersion(node); + sendVersion(node); + pool.execute(new Runnable() { + @Override + public void run() { + try { + registerListener(version, socket, listener); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + + private void sendVersion(NetworkAddress node) { + send(node, new NetworkMessage(node, new Version.Builder().defaults().addrFrom(LOCALHOST).addrRecv(node).build())); + } + + private void registerListener(int version, Socket socket, MessageListener listener) throws IOException { + NetworkMessage message = Factory.getNetworkMessage(version, socket.getInputStream()); + if (message.getPayload() instanceof Version) { + version = ((Version) message.getPayload()).getVersion(); + synchronized (versions) { + + versions.put(new NetworkAddress.Builder() + .ip(socket.getInetAddress()) + .port(socket.getPort()) + .build(), version); + } + } + listener.receive(message); + } + + @Override + public void send(final NetworkAddress node, final NetworkMessage message) { + sendingQueue.add(message); + } + + private Socket getSocket(NetworkAddress node) throws IOException { + synchronized (sockets) { + Socket socket = sockets.get(node); + if (socket == null) { + socket = new Socket(node.toInetAddress(), node.getPort()); + sockets.put(node, socket); + } + return socket; + } + } + + private synchronized int getVersion(NetworkAddress node) { + synchronized (versions) { + Integer version = versions.get(node); + return version == null ? 3 : version; + } + } +} diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkNodeTest.java b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkNodeTest.java new file mode 100644 index 0000000..1d98b37 --- /dev/null +++ b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkNodeTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.networking; + +import ch.dissem.bitmessage.entity.NetworkMessage; +import ch.dissem.bitmessage.entity.Version; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.ports.NetworkMessageReceiver; +import org.junit.Test; + +/** + * Created by chris on 20.03.15. + */ +public class NetworkNodeTest { + private NetworkAddress localhost = new NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(8444).build(); + + @Test(expected = InterruptedException.class) + public void testSendMessage() throws Exception { + final Thread baseThread = Thread.currentThread(); + NetworkNode net = new NetworkNode(); + net.registerListener(localhost, new NetworkMessageReceiver.MessageListener() { + @Override + public void receive(NetworkMessage message) { + System.out.println(message); + baseThread.interrupt(); + } + }); + NetworkMessage ver = new NetworkMessage(localhost, + new Version.Builder() + .version(3) + .services(1) + .timestamp(System.currentTimeMillis() / 1000) + .addrFrom(localhost) + .addrRecv(localhost) + .nonce(-1) + .userAgent("Test") + .streams(1, 2) + .build()); + net.send(localhost, ver); + Thread.sleep(20000); + } +}