It's now possible to send a 'version' message that will be accepted by the other node.

This commit is contained in:
Christian Basler 2015-03-31 21:06:42 +02:00
parent c2624dcd15
commit 0c4b39bdee
31 changed files with 1351 additions and 45 deletions

View File

@ -1,4 +1,9 @@
Jabit
=====
A Java implementation for the Bitmessage protocol. To build, use command `gradle build`.
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.

View File

@ -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<NetworkAddress> addresses;
private Addr(Builder builder) {

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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<InventoryVector> 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<InventoryVector> inventory = new LinkedList<>();
public Builder() {
}
public Builder addInventoryVector(InventoryVector inventoryVector) {
this.inventory.add(inventoryVector);
return this;
}
public Builder inventory(List<InventoryVector> inventory) {
this.inventory = inventory;
return this;
}
public GetData build() {
return new GetData(this);
}
}
}

View File

@ -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<InventoryVector> 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<InventoryVector> inventory = new LinkedList<>();
public Builder() {
}
public Builder addInventoryVector(InventoryVector inventoryVector) {
this.inventory.add(inventoryVector);
return this;
}
public Builder inventory(List<InventoryVector> inventory) {
this.inventory = inventory;
return this;
}
public Inv build() {
return new Inv(this);
}
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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 {
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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,

View File

@ -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));
}
}
}

View File

@ -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;
}
}

View File

@ -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<InventoryVector> getInventory();

View File

@ -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);

View File

@ -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);

View File

@ -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
}
}

View File

@ -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");
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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())));
}
}

View File

@ -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<NetworkMessage> sendingQueue = new LinkedBlockingQueue<>();
private final ExecutorService pool;
private final Map<NetworkAddress, Socket> sockets = new HashMap<>();
private final Map<NetworkAddress, Integer> 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;
}
}
}

View File

@ -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);
}
}