Merge branch 'feature/server-pow' into develop

This commit is contained in:
Christian Basler 2016-01-10 12:22:57 +01:00
commit de0100e14f
44 changed files with 1053 additions and 144 deletions

View File

@ -22,10 +22,7 @@ import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.networking.DefaultNetworkHandler;
import ch.dissem.bitmessage.ports.MemoryNodeRegistry;
import ch.dissem.bitmessage.repository.JdbcAddressRepository;
import ch.dissem.bitmessage.repository.JdbcConfig;
import ch.dissem.bitmessage.repository.JdbcInventory;
import ch.dissem.bitmessage.repository.JdbcMessageRepository;
import ch.dissem.bitmessage.repository.*;
import ch.dissem.bitmessage.security.bc.BouncySecurity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -50,6 +47,7 @@ public class Application {
.inventory(new JdbcInventory(jdbcConfig))
.nodeRegistry(new MemoryNodeRegistry())
.messageRepo(new JdbcMessageRepository(jdbcConfig))
.powRepo(new JdbcProofOfWorkRepository(jdbcConfig))
.networkHandler(new DefaultNetworkHandler())
.security(new BouncySecurity())
.port(48444)

View File

@ -51,6 +51,7 @@ public class Main {
.inventory(new JdbcInventory(jdbcConfig))
.nodeRegistry(new MemoryNodeRegistry())
.messageRepo(new JdbcMessageRepository(jdbcConfig))
.powRepo(new JdbcProofOfWorkRepository(jdbcConfig))
.networkHandler(new DefaultNetworkHandler())
.security(new BouncySecurity())
.port(48444)

View File

@ -16,9 +16,7 @@
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
@ -33,6 +31,8 @@ import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*;
import static ch.dissem.bitmessage.entity.Plaintext.Status.*;
@ -66,6 +66,8 @@ public class BitmessageContext {
private final Listener listener;
private final NetworkHandler.MessageListener networkListener;
private final boolean sendPubkeyOnIdentityCreation;
private BitmessageContext(Builder builder) {
ctx = new InternalContext(builder);
listener = builder.listener;
@ -74,10 +76,19 @@ public class BitmessageContext {
// As this thread is used for parts that do POW, which itself uses parallel threads, only
// one should be executed at any time.
pool = Executors.newFixedThreadPool(1);
sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
ctx.getProofOfWorkService().doMissingProofOfWork();
}
}, 30_000); // After 30 seconds
}
public AddressRepository addresses() {
return ctx.getAddressRepo();
return ctx.getAddressRepository();
}
public MessageRepository messages() {
@ -92,18 +103,21 @@ public class BitmessageContext {
ctx.getNetworkExtraBytes(),
features
));
ctx.getAddressRepo().save(identity);
ctx.getAddressRepository().save(identity);
if (sendPubkeyOnIdentityCreation) {
pool.submit(new Runnable() {
@Override
public void run() {
ctx.sendPubkey(identity, identity.getStream());
}
});
}
return identity;
}
public void addDistributedMailingList(String address, String alias) {
// TODO
throw new RuntimeException("not implemented");
}
public void broadcast(final BitmessageAddress from, final String subject, final String message) {
@ -122,9 +136,7 @@ public class BitmessageContext {
from,
from,
Factory.getBroadcast(from, msg),
+2 * DAY,
0,
0
+2 * DAY
);
msg.setStatus(SENT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.BROADCAST, Label.Type.SENT));
@ -161,9 +173,7 @@ public class BitmessageContext {
from,
to,
new Msg(msg),
+2 * DAY,
ctx.getNonceTrialsPerByte(to),
ctx.getExtraBytes(to)
+2 * DAY
);
msg.setStatus(SENT);
msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT));
@ -178,9 +188,7 @@ public class BitmessageContext {
requestingIdentity,
address,
new GetPubkey(address),
+28 * DAY,
ctx.getNetworkNonceTrialsPerByte(),
ctx.getNetworkExtraBytes()
+28 * DAY
);
}
@ -213,6 +221,19 @@ public class BitmessageContext {
}
}
/**
* Send a custom message to a specific node (that should implement handling for this message type) and returns
* the response, which in turn is expected to be a {@link CustomMessage}.
*
* @param server the node's address
* @param port the node's port
* @param request the request
* @return the response
*/
public CustomMessage send(InetAddress server, int port, CustomMessage request) {
return ctx.getNetworkHandler().send(server, port, request);
}
public void cleanup() {
ctx.getInventory().cleanup();
}
@ -222,7 +243,7 @@ public class BitmessageContext {
}
public void addContact(BitmessageAddress contact) {
ctx.getAddressRepo().save(contact);
ctx.getAddressRepository().save(contact);
tryToFindMatchingPubkey(contact);
if (contact.getPubkey() == null) {
ctx.requestPubkey(contact);
@ -239,7 +260,7 @@ public class BitmessageContext {
v4Pubkey.decrypt(address.getPublicDecryptionKey());
if (object.isSignatureValid(v4Pubkey)) {
address.setPubkey(v4Pubkey);
ctx.getAddressRepo().save(address);
ctx.getAddressRepository().save(address);
break;
} else {
LOG.info("Found pubkey for " + address + " but signature is invalid");
@ -248,7 +269,7 @@ public class BitmessageContext {
} else {
if (Arrays.equals(pubkey.getRipe(), address.getRipe())) {
address.setPubkey(pubkey);
ctx.getAddressRepo().save(address);
ctx.getAddressRepository().save(address);
break;
}
}
@ -260,7 +281,7 @@ public class BitmessageContext {
public void addSubscribtion(BitmessageAddress address) {
address.setSubscribed(true);
ctx.getAddressRepo().save(address);
ctx.getAddressRepository().save(address);
tryToFindBroadcastsForAddress(address);
}
@ -283,6 +304,14 @@ public class BitmessageContext {
);
}
/**
* Returns the {@link InternalContext} - normally you wouldn't need it,
* unless you are doing something crazy with the protocol.
*/
public InternalContext internals() {
return ctx;
}
public interface Listener {
void receive(Plaintext plaintext);
}
@ -294,12 +323,16 @@ public class BitmessageContext {
NetworkHandler networkHandler;
AddressRepository addressRepo;
MessageRepository messageRepo;
ProofOfWorkRepository proofOfWorkRepository;
ProofOfWorkEngine proofOfWorkEngine;
Security security;
MessageCallback messageCallback;
CustomCommandHandler customCommandHandler;
Listener listener;
int connectionLimit = 150;
long connectionTTL = 12 * HOUR;
boolean sendPubkeyOnIdentityCreation = true;
long pubkeyTTL = 28;
public Builder() {
}
@ -334,6 +367,11 @@ public class BitmessageContext {
return this;
}
public Builder powRepo(ProofOfWorkRepository proofOfWorkRepository) {
this.proofOfWorkRepository = proofOfWorkRepository;
return this;
}
public Builder security(Security security) {
this.security = security;
return this;
@ -344,6 +382,11 @@ public class BitmessageContext {
return this;
}
public Builder customCommandHandler(CustomCommandHandler handler) {
this.customCommandHandler = handler;
return this;
}
public Builder proofOfWorkEngine(ProofOfWorkEngine proofOfWorkEngine) {
this.proofOfWorkEngine = proofOfWorkEngine;
return this;
@ -364,12 +407,37 @@ public class BitmessageContext {
return this;
}
/**
* By default a client will send the public key when an identity is being created. On weaker devices
* this behaviour might not be desirable.
*/
public Builder doNotSendPubkeyOnIdentityCreation() {
this.sendPubkeyOnIdentityCreation = false;
return this;
}
/**
* Time to live in seconds for public keys the client sends. Defaults to the maximum of 28 days,
* but on weak devices smaller values might be desirable.
* <p>
* Please be aware that this might cause some problems where you can't receive a message (the
* sender can't receive your public key) in some special situations. Also note that it's probably
* not a good idea to set it too low.
* </p>
*/
public Builder pubkeyTTL(long days) {
if (days < 0 || days > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days");
this.pubkeyTTL = days;
return this;
}
public BitmessageContext build() {
nonNull("inventory", inventory);
nonNull("nodeRegistry", nodeRegistry);
nonNull("networkHandler", networkHandler);
nonNull("addressRepo", addressRepo);
nonNull("messageRepo", messageRepo);
nonNull("proofOfWorkRepo", proofOfWorkRepository);
if (proofOfWorkEngine == null) {
proofOfWorkEngine = new MultiThreadedPOWEngine();
}
@ -392,6 +460,14 @@ public class BitmessageContext {
}
};
}
if (customCommandHandler == null) {
customCommandHandler = new CustomCommandHandler() {
@Override
public MessagePayload handle(CustomMessage request) {
throw new RuntimeException("Received custom request, but no custom command handler configured.");
}
};
}
return new BitmessageContext(this);
}

View File

@ -69,7 +69,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
}
protected void receive(ObjectMessage object, GetPubkey getPubkey) {
BitmessageAddress identity = ctx.getAddressRepo().findIdentity(getPubkey.getRipeTag());
BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag());
if (identity != null && identity.getPrivateKey() != null) {
LOG.info("Got pubkey request for identity " + identity);
// FIXME: only send pubkey if it wasn't sent in the last 28 days
@ -82,17 +82,17 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
try {
if (pubkey instanceof V4Pubkey) {
V4Pubkey v4Pubkey = (V4Pubkey) pubkey;
address = ctx.getAddressRepo().findContact(v4Pubkey.getTag());
address = ctx.getAddressRepository().findContact(v4Pubkey.getTag());
if (address != null) {
v4Pubkey.decrypt(address.getPublicDecryptionKey());
}
} else {
address = ctx.getAddressRepo().findContact(pubkey.getRipe());
address = ctx.getAddressRepository().findContact(pubkey.getRipe());
}
if (address != null) {
address.setPubkey(pubkey);
LOG.info("Got pubkey for contact " + address);
ctx.getAddressRepo().save(address);
ctx.getAddressRepository().save(address);
List<Plaintext> messages = ctx.getMessageRepository().findMessages(Plaintext.Status.PUBKEY_REQUESTED, address);
LOG.info("Sending " + messages.size() + " messages for contact " + address);
for (Plaintext msg : messages) {
@ -102,9 +102,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
msg.getFrom(),
msg.getTo(),
new Msg(msg),
+2 * DAY,
ctx.getNonceTrialsPerByte(msg.getTo()),
ctx.getExtraBytes(msg.getTo())
+2 * DAY
);
msg.setStatus(SENT);
ctx.getMessageRepository().save(msg);
@ -115,7 +113,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
}
protected void receive(ObjectMessage object, Msg msg) throws IOException {
for (BitmessageAddress identity : ctx.getAddressRepo().getIdentities()) {
for (BitmessageAddress identity : ctx.getAddressRepository().getIdentities()) {
try {
msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey());
msg.getPlaintext().setTo(identity);
@ -136,7 +134,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener {
protected void receive(ObjectMessage object, Broadcast broadcast) throws IOException {
byte[] tag = broadcast instanceof V5Broadcast ? ((V5Broadcast) broadcast).getTag() : null;
for (BitmessageAddress subscription : ctx.getAddressRepo().getSubscriptions(broadcast.getVersion())) {
for (BitmessageAddress subscription : ctx.getAddressRepository().getSubscriptions(broadcast.getVersion())) {
if (tag != null && !Arrays.equals(tag, subscription.getTag())) {
continue;
}

View File

@ -16,7 +16,9 @@
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.*;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Encrypted;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.payload.Broadcast;
import ch.dissem.bitmessage.entity.payload.GetPubkey;
import ch.dissem.bitmessage.entity.payload.ObjectPayload;
@ -29,8 +31,6 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.TreeSet;
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
/**
* The internal context should normally only be used for port implementations. If you need it in your client
* implementation, you're either doing something wrong, something very weird, or the BitmessageContext should
@ -48,14 +48,18 @@ public class InternalContext {
private final NetworkHandler networkHandler;
private final AddressRepository addressRepository;
private final MessageRepository messageRepository;
private final ProofOfWorkRepository proofOfWorkRepository;
private final ProofOfWorkEngine proofOfWorkEngine;
private final MessageCallback messageCallback;
private final CustomCommandHandler customCommandHandler;
private final ProofOfWorkService proofOfWorkService;
private final TreeSet<Long> streams = new TreeSet<>();
private final int port;
private final long clientNonce;
private final long networkNonceTrialsPerByte = 1000;
private final long networkExtraBytes = 1000;
private final long pubkeyTTL;
private long connectionTTL;
private int connectionLimit;
@ -66,12 +70,16 @@ public class InternalContext {
this.networkHandler = builder.networkHandler;
this.addressRepository = builder.addressRepo;
this.messageRepository = builder.messageRepo;
this.proofOfWorkRepository = builder.proofOfWorkRepository;
this.proofOfWorkService = new ProofOfWorkService();
this.proofOfWorkEngine = builder.proofOfWorkEngine;
this.clientNonce = security.randomNonce();
this.messageCallback = builder.messageCallback;
this.customCommandHandler = builder.customCommandHandler;
this.port = builder.port;
this.connectionLimit = builder.connectionLimit;
this.connectionTTL = builder.connectionTTL;
this.pubkeyTTL = builder.pubkeyTTL;
Singleton.initialize(security);
@ -86,7 +94,9 @@ public class InternalContext {
streams.add(1L);
}
init(security, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, proofOfWorkEngine);
init(security, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository,
proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine,
messageCallback, customCommandHandler);
for (BitmessageAddress identity : addressRepository.getIdentities()) {
streams.add(identity.getStream());
}
@ -116,7 +126,7 @@ public class InternalContext {
return networkHandler;
}
public AddressRepository getAddressRepo() {
public AddressRepository getAddressRepository() {
return addressRepository;
}
@ -124,10 +134,18 @@ public class InternalContext {
return messageRepository;
}
public ProofOfWorkRepository getProofOfWorkRepository() {
return proofOfWorkRepository;
}
public ProofOfWorkEngine getProofOfWorkEngine() {
return proofOfWorkEngine;
}
public ProofOfWorkService getProofOfWorkService() {
return proofOfWorkService;
}
public long[] getStreams() {
long[] result = new long[streams.size()];
int i = 0;
@ -145,22 +163,12 @@ public class InternalContext {
return networkNonceTrialsPerByte;
}
public long getNonceTrialsPerByte(BitmessageAddress address) {
long nonceTrialsPerByte = address.getPubkey().getNonceTrialsPerByte();
return networkNonceTrialsPerByte > nonceTrialsPerByte ? networkNonceTrialsPerByte : nonceTrialsPerByte;
}
public long getNetworkExtraBytes() {
return networkExtraBytes;
}
public long getExtraBytes(BitmessageAddress address) {
long extraBytes = address.getPubkey().getExtraBytes();
return networkExtraBytes > extraBytes ? networkExtraBytes : extraBytes;
}
public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload,
final long timeToLive, final long nonceTrialsPerByte, final long extraBytes) {
final long timeToLive) {
try {
if (to == null) to = from;
long expires = UnixTime.now(+timeToLive);
@ -179,22 +187,7 @@ public class InternalContext {
object.encrypt(to.getPubkey());
}
messageCallback.proofOfWorkStarted(payload);
security.doProofOfWork(object, nonceTrialsPerByte, extraBytes,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] nonce) {
object.setNonce(nonce);
messageCallback.proofOfWorkCompleted(payload);
if (payload instanceof PlaintextHolder) {
Plaintext plaintext = ((PlaintextHolder) payload).getPlaintext();
plaintext.setInventoryVector(object.getInventoryVector());
messageRepository.save(plaintext);
}
inventory.storeObject(object);
networkHandler.offer(object.getInventoryVector());
messageCallback.messageOffered(payload, object.getInventoryVector());
}
});
proofOfWorkService.doProofOfWork(to, object);
} catch (IOException e) {
throw new RuntimeException(e);
}
@ -202,7 +195,7 @@ public class InternalContext {
public void sendPubkey(final BitmessageAddress identity, final long targetStream) {
try {
long expires = UnixTime.now(+28 * DAY);
long expires = UnixTime.now(pubkeyTTL);
LOG.info("Expires at " + expires);
final ObjectMessage response = new ObjectMessage.Builder()
.stream(targetStream)
@ -212,25 +205,15 @@ public class InternalContext {
response.sign(identity.getPrivateKey());
response.encrypt(security.createPublicKey(identity.getPublicDecryptionKey()));
messageCallback.proofOfWorkStarted(identity.getPubkey());
security.doProofOfWork(response, networkNonceTrialsPerByte, networkExtraBytes,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] nonce) {
response.setNonce(nonce);
messageCallback.proofOfWorkCompleted(identity.getPubkey());
inventory.storeObject(response);
networkHandler.offer(response.getInventoryVector());
// TODO: save that the pubkey was just sent, and on which stream!
messageCallback.messageOffered(identity.getPubkey(), response.getInventoryVector());
}
});
// TODO: remember that the pubkey is just about to be sent, and on which stream!
proofOfWorkService.doProofOfWork(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void requestPubkey(final BitmessageAddress contact) {
long expires = UnixTime.now(+2 * DAY);
long expires = UnixTime.now(+pubkeyTTL);
LOG.info("Expires at " + expires);
final ObjectMessage response = new ObjectMessage.Builder()
.stream(contact.getStream())
@ -238,17 +221,7 @@ public class InternalContext {
.payload(new GetPubkey(contact))
.build();
messageCallback.proofOfWorkStarted(response.getPayload());
security.doProofOfWork(response, networkNonceTrialsPerByte, networkExtraBytes,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] nonce) {
response.setNonce(nonce);
messageCallback.proofOfWorkCompleted(response.getPayload());
inventory.storeObject(response);
networkHandler.offer(response.getInventoryVector());
messageCallback.messageOffered(response.getPayload(), response.getInventoryVector());
}
});
proofOfWorkService.doProofOfWork(response);
}
public long getClientNonce() {
@ -263,6 +236,10 @@ public class InternalContext {
return connectionLimit;
}
public CustomCommandHandler getCustomCommandHandler() {
return customCommandHandler;
}
public interface ContextHolder {
void setContext(InternalContext context);
}

View File

@ -0,0 +1,82 @@
package ch.dissem.bitmessage;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.PlaintextHolder;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
import ch.dissem.bitmessage.ports.Security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* @author Christian Basler
*/
public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalContext.ContextHolder {
private final static Logger LOG = LoggerFactory.getLogger(ProofOfWorkService.class);
private Security security;
private InternalContext ctx;
private ProofOfWorkRepository powRepo;
private MessageRepository messageRepo;
public void doMissingProofOfWork() {
List<byte[]> items = powRepo.getItems();
if (items.isEmpty()) return;
LOG.info("Doing POW for " + items.size() + " tasks.");
for (byte[] initialHash : items) {
ProofOfWorkRepository.Item item = powRepo.getItem(initialHash);
security.doProofOfWork(item.object, item.nonceTrialsPerByte, item.extraBytes, this);
}
}
public void doProofOfWork(ObjectMessage object) {
doProofOfWork(null, object);
}
public void doProofOfWork(BitmessageAddress recipient, ObjectMessage object) {
long nonceTrialsPerByte = recipient == null ?
ctx.getNetworkNonceTrialsPerByte() : recipient.getPubkey().getNonceTrialsPerByte();
long extraBytes = recipient == null ?
ctx.getNetworkExtraBytes() : recipient.getPubkey().getExtraBytes();
powRepo.putObject(object, nonceTrialsPerByte, extraBytes);
if (object.getPayload() instanceof PlaintextHolder) {
Plaintext plaintext = ((PlaintextHolder) object.getPayload()).getPlaintext();
plaintext.setInitialHash(security.getInitialHash(object));
messageRepo.save(plaintext);
}
security.doProofOfWork(object, nonceTrialsPerByte, extraBytes, this);
}
@Override
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
ObjectMessage object = powRepo.getItem(initialHash).object;
object.setNonce(nonce);
// messageCallback.proofOfWorkCompleted(payload);
Plaintext plaintext = messageRepo.getMessage(initialHash);
if (plaintext != null) {
plaintext.setInventoryVector(object.getInventoryVector());
messageRepo.save(plaintext);
}
ctx.getInventory().storeObject(object);
ctx.getProofOfWorkRepository().removeObject(initialHash);
ctx.getNetworkHandler().offer(object.getInventoryVector());
// messageCallback.messageOffered(payload, object.getInventoryVector());
}
@Override
public void setContext(InternalContext ctx) {
this.ctx = ctx;
this.security = security();
this.powRepo = ctx.getProofOfWorkRepository();
this.messageRepo = ctx.getMessageRepository();
}
}

View File

@ -87,7 +87,7 @@ public class BitmessageAddress implements Serializable {
}
}
BitmessageAddress(Pubkey publicKey) {
public BitmessageAddress(Pubkey publicKey) {
this(publicKey.getVersion(), publicKey.getStream(), publicKey.getRipe());
this.pubkey = publicKey;
}

View File

@ -0,0 +1,96 @@
/*
* 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.utils.AccessCounter;
import ch.dissem.bitmessage.utils.Encode;
import java.io.*;
import static ch.dissem.bitmessage.utils.Decode.bytes;
import static ch.dissem.bitmessage.utils.Decode.varString;
/**
* @author Christian Basler
*/
public class CustomMessage implements MessagePayload {
public static final String COMMAND_ERROR = "ERROR";
private final String command;
private final byte[] data;
public CustomMessage(String command) {
this.command = command;
this.data = null;
}
public CustomMessage(String command, byte[] data) {
this.command = command;
this.data = data;
}
public static CustomMessage read(InputStream in, int length) throws IOException {
AccessCounter counter = new AccessCounter();
return new CustomMessage(varString(in, counter), bytes(in, length - counter.length()));
}
@Override
public Command getCommand() {
return Command.CUSTOM;
}
public String getCustomCommand() {
return command;
}
public byte[] getData() {
if (data != null) {
return data;
} else {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
write(out);
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void write(OutputStream out) throws IOException {
if (data != null) {
Encode.varString(command, out);
out.write(data);
} else {
throw new RuntimeException("Tried to write custom message without data. " +
"Programmer: did you forget to override #write()?");
}
}
public boolean isError() {
return COMMAND_ERROR.equals(command);
}
public static CustomMessage error(String message) {
try {
return new CustomMessage(COMMAND_ERROR, message.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -23,6 +23,6 @@ public interface MessagePayload extends Streamable {
Command getCommand();
enum Command {
VERSION, VERACK, ADDR, INV, GETDATA, OBJECT
VERSION, VERACK, ADDR, INV, GETDATA, OBJECT, CUSTOM
}
}

View File

@ -156,7 +156,11 @@ public class ObjectMessage implements MessagePayload {
@Override
public void write(OutputStream out) throws IOException {
if (nonce != null) {
out.write(nonce);
} else {
out.write(new byte[8]);
}
out.write(getPayloadBytesWithoutNonce());
}

View File

@ -44,6 +44,7 @@ public class Plaintext implements Streamable {
private Long received;
private Set<Label> labels;
private byte[] initialHash;
private Plaintext(Builder builder) {
id = builder.id;
@ -260,6 +261,14 @@ public class Plaintext implements Streamable {
}
}
public void setInitialHash(byte[] initialHash) {
this.initialHash = initialHash;
}
public byte[] getInitialHash() {
return initialHash;
}
public enum Encoding {
IGNORE(0), TRIVIAL(1), SIMPLE(2);

View File

@ -21,7 +21,6 @@ import ch.dissem.bitmessage.entity.Encrypted;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.PlaintextHolder;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.ports.Security;
import java.io.IOException;

View File

@ -38,7 +38,14 @@ public class CryptoBox implements Streamable {
private final byte[] mac;
private byte[] encrypted;
private long addressVersion;
public CryptoBox(Streamable data, byte[] K) throws IOException {
this(Encode.bytes(data), K);
}
public CryptoBox(byte[] data, byte[] K) throws IOException {
curveType = 0x02CA;
// 1. The destination public key is called K.
@ -58,7 +65,7 @@ public class CryptoBox implements Streamable {
byte[] key_m = Arrays.copyOfRange(H, 32, 64);
// 7. Pad the input text to a multiple of 16 bytes, in accordance to PKCS7.
// 8. Encrypt the data with AES-256-CBC, using IV as initialization vector, key_e as encryption key and the padded input text as payload. Call the output cipher text.
encrypted = security().crypt(true, Encode.bytes(data), key_e, initializationVector);
encrypted = security().crypt(true, data, key_e, initializationVector);
// 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC.
mac = calculateMac(key_m);

View File

@ -73,12 +73,18 @@ class V3MessageFactory {
return parseGetData(stream);
case "object":
return readObject(stream, length);
case "custom":
return readCustom(stream, length);
default:
LOG.debug("Unknown command: " + command);
return null;
}
}
private static MessagePayload readCustom(InputStream in, int length) throws IOException {
return CustomMessage.read(in, length);
}
public static ObjectMessage readObject(InputStream in, int length) throws IOException {
AccessCounter counter = new AccessCounter();
byte nonce[] = Decode.bytes(in, 8, counter);

View File

@ -34,6 +34,8 @@ import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import static ch.dissem.bitmessage.utils.Numbers.max;
/**
* Implements everything that isn't directly dependent on either Spongy- or Bouncycastle.
*/
@ -41,6 +43,8 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont
public static final Logger LOG = LoggerFactory.getLogger(Security.class);
private static final SecureRandom RANDOM = new SecureRandom();
private static final BigInteger TWO = BigInteger.valueOf(2);
private static final BigInteger TWO_POW_64 = TWO.pow(64);
private static final BigInteger TWO_POW_16 = TWO.pow(16);
private final String provider;
private InternalContext context;
@ -94,18 +98,14 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont
public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte,
long extraBytes, ProofOfWorkEngine.Callback callback) {
try {
if (nonceTrialsPerByte < 1000) nonceTrialsPerByte = 1000;
if (extraBytes < 1000) extraBytes = 1000;
nonceTrialsPerByte = max(nonceTrialsPerByte, context.getNetworkNonceTrialsPerByte());
extraBytes = max(extraBytes, context.getNetworkExtraBytes());
byte[] initialHash = getInitialHash(object);
byte[] target = getProofOfWorkTarget(object, nonceTrialsPerByte, extraBytes);
context.getProofOfWorkEngine().calculateNonce(initialHash, target, callback);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes)
@ -117,15 +117,25 @@ public abstract class AbstractSecurity implements Security, InternalContext.Cont
}
}
private byte[] getInitialHash(ObjectMessage object) throws IOException {
@Override
public byte[] getInitialHash(ObjectMessage object) {
return sha512(object.getPayloadBytesWithoutNonce());
}
private byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) throws IOException {
@Override
public byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
if (nonceTrialsPerByte == 0) nonceTrialsPerByte = context.getNetworkNonceTrialsPerByte();
if (extraBytes == 0) extraBytes = context.getNetworkExtraBytes();
BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now());
BigInteger numerator = TWO.pow(64);
BigInteger numerator = TWO_POW_64;
BigInteger powLength = BigInteger.valueOf(object.getPayloadBytesWithoutNonce().length + extraBytes);
BigInteger denominator = BigInteger.valueOf(nonceTrialsPerByte).multiply(powLength.add(powLength.multiply(TTL).divide(BigInteger.valueOf(2).pow(16))));
BigInteger denominator = BigInteger.valueOf(nonceTrialsPerByte)
.multiply(
powLength.add(
powLength.multiply(TTL).divide(TWO_POW_16)
)
);
return Bytes.expand(numerator.divide(denominator).toByteArray(), 8);
}

View File

@ -0,0 +1,27 @@
/*
* 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.ports;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.entity.MessagePayload;
/**
* @author Christian Basler
*/
public interface CustomCommandHandler {
MessagePayload handle(CustomMessage request);
}

View File

@ -30,6 +30,8 @@ public interface MessageRepository {
int countUnread(Label label);
Plaintext getMessage(byte[] initialHash);
List<Plaintext> findMessages(Label label);
List<Plaintext> findMessages(Status status);

View File

@ -102,7 +102,7 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
synchronized (callback) {
if (!Thread.interrupted()) {
try {
callback.onNonceCalculated(nonce);
callback.onNonceCalculated(initialHash, nonce);
} finally {
semaphore.release();
for (Worker w : workers) {
@ -128,12 +128,12 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine {
}
@Override
public void onNonceCalculated(byte[] nonce) {
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
synchronized (this) {
if (waiting) {
LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds");
waiting = false;
callback.onNonceCalculated(nonce);
callback.onNonceCalculated(initialHash, nonce);
}
}
}

View File

@ -16,6 +16,7 @@
package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.utils.Property;
@ -34,7 +35,18 @@ public interface NetworkHandler {
* An implementation should disconnect if either the timeout is reached or the returned thread is interrupted.
* </p>
*/
Future<?> synchronize(InetAddress trustedHost, int port, MessageListener listener, long timeoutInSeconds);
Future<?> synchronize(InetAddress server, int port, MessageListener listener, long timeoutInSeconds);
/**
* Send a custom message to a specific node (that should implement handling for this message type) and returns
* the response, which in turn is expected to be a {@link CustomMessage}.
*
* @param server the node's address
* @param port the node's port
* @param request the request
* @return the response
*/
CustomMessage send(InetAddress server, int port, CustomMessage request);
/**
* Start a full network node, accepting incoming connections and relaying objects.

View File

@ -35,6 +35,6 @@ public interface ProofOfWorkEngine {
/**
* @param nonce 8 bytes nonce
*/
void onNonceCalculated(byte[] nonce);
void onNonceCalculated(byte[] initialHash, byte[] nonce);
}
}

View File

@ -0,0 +1,32 @@
package ch.dissem.bitmessage.ports;
import ch.dissem.bitmessage.entity.ObjectMessage;
import java.util.List;
/**
* Objects that proof of work is currently being done for.
*
* @author Christian Basler
*/
public interface ProofOfWorkRepository {
Item getItem(byte[] initialHash);
List<byte[]> getItems();
void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes);
void removeObject(byte[] initialHash);
class Item {
public final ObjectMessage object;
public final long nonceTrialsPerByte;
public final long extraBytes;
public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
this.object = object;
this.nonceTrialsPerByte = nonceTrialsPerByte;
this.extraBytes = extraBytes;
}
}
}

View File

@ -134,6 +134,10 @@ public interface Security {
void checkProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes)
throws IOException;
byte[] getInitialHash(ObjectMessage object);
byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes);
/**
* Calculates the MAC for a message (data)
*

View File

@ -40,6 +40,6 @@ public class SimplePOWEngine implements ProofOfWorkEngine {
mda.update(nonce);
mda.update(initialHash);
} while (Bytes.lt(target, mda.digest(mda.digest()), 8));
callback.onNonceCalculated(nonce);
callback.onNonceCalculated(initialHash, nonce);
}
}

View File

@ -130,9 +130,13 @@ public class Decode {
}
public static String varString(InputStream stream) throws IOException {
int length = (int) varInt(stream);
return varString(stream, null);
}
public static String varString(InputStream stream, AccessCounter counter) throws IOException {
int length = (int) varInt(stream, counter);
// FIXME: technically, it says the length in characters, but I think this one might be correct
// otherwise it will get complicated, as we'll need to read UTF-8 char by char...
return new String(bytes(stream, length), "utf-8");
return new String(bytes(stream, length, counter), "utf-8");
}
}

View File

@ -103,15 +103,23 @@ public class Encode {
inc(counter, 8);
}
public static void varString(String value, OutputStream stream) throws IOException {
public static void varString(String value, OutputStream out) throws IOException {
byte[] bytes = value.getBytes("utf-8");
// FIXME: technically, it says the length in characters, but I think this one might be correct
// Technically, it says the length in characters, but I think this one might be correct.
// It doesn't really matter, as only ASCII characters are being used.
// see also Decode#varString()
varInt(bytes.length, stream);
stream.write(bytes);
varInt(bytes.length, out);
out.write(bytes);
}
public static void varBytes(byte[] data, OutputStream out) throws IOException {
varInt(data.length, out);
out.write(data);
}
/**
* Serializes a {@link Streamable} object and returns the byte array.
*
* @param streamable the object to be serialized
* @return an array of bytes representing the given streamable object.
* @throws IOException if an I/O error occurs.

View File

@ -0,0 +1,10 @@
package ch.dissem.bitmessage.utils;
/**
* Created by chrig on 07.12.2015.
*/
public class Numbers {
public static long max(long a, long b) {
return a > b ? a : b;
}
}

View File

@ -43,7 +43,7 @@ public class ProofOfWorkEngineTest extends TestBase {
engine.calculateNonce(initialHash, target,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] nonce) {
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
waiter1.setValue(nonce);
}
});
@ -59,7 +59,7 @@ public class ProofOfWorkEngineTest extends TestBase {
engine.calculateNonce(initialHash2, target2,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] nonce) {
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
waiter2.setValue(nonce);
}
});

36
extensions/build.gradle Normal file
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.
*/
uploadArchives {
repositories {
mavenDeployer {
pom.project {
name 'Jabit Extensions'
artifactId = 'jabit-extensions'
description 'Protocol extensions used for some extended features, e.g. server and mobile client.'
}
}
}
}
dependencies {
compile project(':domain')
testCompile 'junit:junit:4.11'
testCompile 'org.slf4j:slf4j-simple:1.7.12'
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile project(path: ':domain', configuration: 'testArtifacts')
testCompile project(':security-bc')
}

View File

@ -0,0 +1,143 @@
/*
* 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.extensions;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.entity.payload.CryptoBox;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.Encode;
import java.io.*;
import static ch.dissem.bitmessage.utils.Decode.*;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* A {@link CustomMessage} implementation that contains signed and encrypted data.
*
* @author Christian Basler
*/
public class CryptoCustomMessage<T extends Streamable> extends CustomMessage {
public static final String COMMAND = "ENCRYPTED";
private final Reader<T> dataReader;
private CryptoBox container;
private BitmessageAddress sender;
private T data;
public CryptoCustomMessage(T data) throws IOException {
super(COMMAND);
this.data = data;
this.dataReader = null;
}
private CryptoCustomMessage(CryptoBox container, Reader<T> dataReader) {
super(COMMAND);
this.container = container;
this.dataReader = dataReader;
}
public static <T extends Streamable> CryptoCustomMessage<T> read(CustomMessage data, Reader<T> dataReader) throws IOException {
CryptoBox cryptoBox = CryptoBox.read(new ByteArrayInputStream(data.getData()), data.getData().length);
return new CryptoCustomMessage<>(cryptoBox, dataReader);
}
public BitmessageAddress getSender() {
return sender;
}
public void signAndEncrypt(BitmessageAddress identity, byte[] publicKey) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Encode.varInt(identity.getVersion(), out);
Encode.varInt(identity.getStream(), out);
Encode.int32(identity.getPubkey().getBehaviorBitfield(), out);
out.write(identity.getPubkey().getSigningKey(), 1, 64);
out.write(identity.getPubkey().getEncryptionKey(), 1, 64);
if (identity.getVersion() >= 3) {
Encode.varInt(identity.getPubkey().getNonceTrialsPerByte(), out);
Encode.varInt(identity.getPubkey().getExtraBytes(), out);
}
data.write(out);
Encode.varBytes(security().getSignature(out.toByteArray(), identity.getPrivateKey()), out);
container = new CryptoBox(out.toByteArray(), publicKey);
}
public T decrypt(byte[] privateKey) throws IOException, DecryptionFailedException {
SignatureCheckingInputStream in = new SignatureCheckingInputStream(container.decrypt(privateKey));
long addressVersion = varInt(in);
long stream = varInt(in);
int behaviorBitfield = int32(in);
byte[] publicSigningKey = bytes(in, 64);
byte[] publicEncryptionKey = bytes(in, 64);
long nonceTrialsPerByte = addressVersion >= 3 ? varInt(in) : 0;
long extraBytes = addressVersion >= 3 ? varInt(in) : 0;
sender = new BitmessageAddress(Factory.createPubkey(
addressVersion,
stream,
publicSigningKey,
publicEncryptionKey,
nonceTrialsPerByte,
extraBytes,
behaviorBitfield
));
data = dataReader.read(sender, in);
in.checkSignature(sender.getPubkey());
return data;
}
@Override
public void write(OutputStream out) throws IOException {
Encode.varString(COMMAND, out);
container.write(out);
}
public interface Reader<T> {
T read(BitmessageAddress sender, InputStream in) throws IOException;
}
private class SignatureCheckingInputStream extends InputStream {
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
private final InputStream wrapped;
private SignatureCheckingInputStream(InputStream wrapped) {
this.wrapped = wrapped;
}
@Override
public int read() throws IOException {
int read = wrapped.read();
if (read >= 0) out.write(read);
return read;
}
public void checkSignature(Pubkey pubkey) throws IOException, RuntimeException {
if (!security().isSignatureValid(out.toByteArray(), varBytes(wrapped), pubkey)) {
throw new RuntimeException("Signature check failed");
}
}
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.extensions.pow;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Streamable;
import ch.dissem.bitmessage.extensions.CryptoCustomMessage;
import ch.dissem.bitmessage.utils.Encode;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import static ch.dissem.bitmessage.utils.Decode.*;
/**
* @author Christian Basler
*/
public class ProofOfWorkRequest implements Streamable {
private final BitmessageAddress sender;
private final byte[] initialHash;
private final Request request;
private final byte[] data;
public ProofOfWorkRequest(BitmessageAddress sender, byte[] initialHash, Request request) {
this(sender, initialHash, request, new byte[0]);
}
public ProofOfWorkRequest(BitmessageAddress sender, byte[] initialHash, Request request, byte[] data) {
this.sender = sender;
this.initialHash = initialHash;
this.request = request;
this.data = data;
}
public static ProofOfWorkRequest read(BitmessageAddress client, InputStream in) throws IOException {
return new ProofOfWorkRequest(
client,
bytes(in, 64),
Request.valueOf(varString(in)),
varBytes(in)
);
}
public BitmessageAddress getSender() {
return sender;
}
public byte[] getInitialHash() {
return initialHash;
}
public Request getRequest() {
return request;
}
public byte[] getData() {
return data;
}
@Override
public void write(OutputStream out) throws IOException {
out.write(initialHash);
Encode.varString(request.name(), out);
Encode.varBytes(data, out);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProofOfWorkRequest other = (ProofOfWorkRequest) o;
if (!sender.equals(other.sender)) return false;
if (!Arrays.equals(initialHash, other.initialHash)) return false;
if (request != other.request) return false;
return Arrays.equals(data, other.data);
}
@Override
public int hashCode() {
int result = sender.hashCode();
result = 31 * result + Arrays.hashCode(initialHash);
result = 31 * result + request.hashCode();
result = 31 * result + Arrays.hashCode(data);
return result;
}
public static class Reader implements CryptoCustomMessage.Reader<ProofOfWorkRequest> {
private final BitmessageAddress identity;
public Reader(BitmessageAddress identity) {
this.identity = identity;
}
@Override
public ProofOfWorkRequest read(BitmessageAddress sender, InputStream in) throws IOException {
return ProofOfWorkRequest.read(identity, in);
}
}
public enum Request {
CALCULATE,
CALCULATING,
COMPLETE
}
}

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.extensions;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.entity.payload.GenericPayload;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
import ch.dissem.bitmessage.utils.TestBase;
import ch.dissem.bitmessage.utils.TestUtils;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import static ch.dissem.bitmessage.utils.Singleton.security;
import static org.junit.Assert.assertEquals;
public class CryptoCustomMessageTest extends TestBase {
@Test
public void ensureEncryptThenDecryptYieldsSameObject() throws Exception {
PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey"));
BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey);
GenericPayload payloadBefore = new GenericPayload(0, 1, security().randomBytes(100));
CryptoCustomMessage<GenericPayload> messageBefore = new CryptoCustomMessage<>(payloadBefore);
messageBefore.signAndEncrypt(sendingIdentity, security().createPublicKey(sendingIdentity.getPublicDecryptionKey()));
ByteArrayOutputStream out = new ByteArrayOutputStream();
messageBefore.write(out);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
CustomMessage customMessage = CustomMessage.read(in, out.size());
CryptoCustomMessage<GenericPayload> messageAfter = CryptoCustomMessage.read(customMessage,
new CryptoCustomMessage.Reader<GenericPayload>() {
@Override
public GenericPayload read(BitmessageAddress ignore, InputStream in) throws IOException {
return GenericPayload.read(0, in, 1, 100);
}
});
GenericPayload payloadAfter = messageAfter.decrypt(sendingIdentity.getPublicDecryptionKey());
assertEquals(payloadBefore, payloadAfter);
}
@Test
public void testWithActualRequest() throws Exception {
PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey"));
final BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey);
ProofOfWorkRequest requestBefore = new ProofOfWorkRequest(sendingIdentity, security().randomBytes(64),
ProofOfWorkRequest.Request.CALCULATE);
CryptoCustomMessage<ProofOfWorkRequest> messageBefore = new CryptoCustomMessage<>(requestBefore);
messageBefore.signAndEncrypt(sendingIdentity, security().createPublicKey(sendingIdentity.getPublicDecryptionKey()));
ByteArrayOutputStream out = new ByteArrayOutputStream();
messageBefore.write(out);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
CustomMessage customMessage = CustomMessage.read(in, out.size());
CryptoCustomMessage<ProofOfWorkRequest> messageAfter = CryptoCustomMessage.read(customMessage,
new ProofOfWorkRequest.Reader(sendingIdentity));
ProofOfWorkRequest requestAfter = messageAfter.decrypt(sendingIdentity.getPublicDecryptionKey());
assertEquals(requestBefore, requestAfter);
}
}

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip

View File

@ -259,6 +259,7 @@ public class Connection {
LOG.debug("Received " + addr.getAddresses().size() + " addresses.");
ctx.getNodeRegistry().offerAddresses(addr.getAddresses());
break;
case CUSTOM:
case VERACK:
case VERSION:
throw new RuntimeException("Unexpectedly received '" + messagePayload.getCommand() + "' command");
@ -394,6 +395,13 @@ public class Connection {
break;
}
break;
case CUSTOM:
MessagePayload response = ctx.getCustomCommandHandler().handle((CustomMessage) msg.getPayload());
if (response != null) {
send(response);
}
disconnect();
break;
default:
throw new NodeException("Command 'version' or 'verack' expected, but was '"
+ msg.getPayload().getCommand() + "'");

View File

@ -18,8 +18,12 @@ package ch.dissem.bitmessage.networking;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.InternalContext.ContextHolder;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.entity.NetworkMessage;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.NetworkAddress;
import ch.dissem.bitmessage.exception.NodeException;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.NetworkHandler;
import ch.dissem.bitmessage.utils.Collections;
import ch.dissem.bitmessage.utils.Property;
@ -71,9 +75,9 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder {
}
@Override
public Future<?> synchronize(InetAddress trustedHost, int port, MessageListener listener, long timeoutInSeconds) {
public Future<?> synchronize(InetAddress server, int port, MessageListener listener, long timeoutInSeconds) {
try {
Connection connection = Connection.sync(ctx, trustedHost, port, listener, timeoutInSeconds);
Connection connection = Connection.sync(ctx, server, port, listener, timeoutInSeconds);
Future<?> reader = pool.submit(connection.getReader());
pool.execute(connection.getWriter());
return reader;
@ -82,6 +86,27 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder {
}
}
@Override
public CustomMessage send(InetAddress server, int port, CustomMessage request) {
try (Socket socket = new Socket(server, port)) {
socket.setSoTimeout(Connection.READ_TIMEOUT);
new NetworkMessage(request).write(socket.getOutputStream());
NetworkMessage networkMessage = Factory.getNetworkMessage(3, socket.getInputStream());
if (networkMessage != null && networkMessage.getPayload() instanceof CustomMessage) {
return (CustomMessage) networkMessage.getPayload();
} else {
if (networkMessage == null) {
throw new NodeException("No response from node " + server);
} else {
throw new NodeException("Unexpected response from node " +
server + ": " + networkMessage.getPayload().getCommand());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void start(final MessageListener listener) {
if (listener == null) {

View File

@ -21,6 +21,7 @@ import ch.dissem.bitmessage.entity.valueobject.NetworkAddress;
import ch.dissem.bitmessage.ports.AddressRepository;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.ports.NetworkHandler;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
import ch.dissem.bitmessage.security.bc.BouncySecurity;
import ch.dissem.bitmessage.utils.Property;
import org.junit.AfterClass;
@ -54,6 +55,7 @@ public class NetworkHandlerTest {
.addressRepo(Mockito.mock(AddressRepository.class))
.inventory(peerInventory)
.messageRepo(Mockito.mock(MessageRepository.class))
.powRepo(Mockito.mock(ProofOfWorkRepository.class))
.port(6001)
.nodeRegistry(new TestNodeRegistry())
.networkHandler(new DefaultNetworkHandler())
@ -68,6 +70,7 @@ public class NetworkHandlerTest {
.addressRepo(Mockito.mock(AddressRepository.class))
.inventory(nodeInventory)
.messageRepo(Mockito.mock(MessageRepository.class))
.powRepo(Mockito.mock(ProofOfWorkRepository.class))
.port(6002)
.nodeRegistry(new TestNodeRegistry(localhost))
.networkHandler(networkHandler)

View File

@ -31,7 +31,7 @@ import static ch.dissem.bitmessage.utils.Strings.hex;
/**
* Helper class that does Flyway migration, provides JDBC connections and some helper methods.
*/
abstract class JdbcHelper {
public abstract class JdbcHelper {
private static final Logger LOG = LoggerFactory.getLogger(JdbcHelper.class);
protected final JdbcConfig config;

View File

@ -22,6 +22,7 @@ import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.utils.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -108,6 +109,20 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
return 0;
}
@Override
public Plaintext getMessage(byte[] initialHash) {
List<Plaintext> plaintexts = find("initial_hash=X'" + Strings.hex(initialHash) + "'");
switch (plaintexts.size()) {
case 0:
return null;
case 1:
return plaintexts.get(0);
default:
throw new RuntimeException("This shouldn't happen, found " + plaintexts.size() +
" messages, one or none was expected");
}
}
@Override
public List<Plaintext> findMessages(Label label) {
return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")");
@ -141,8 +156,8 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
long id = rs.getLong("id");
builder.id(id);
builder.IV(new InventoryVector(iv));
builder.from(ctx.getAddressRepo().getAddress(rs.getString("sender")));
builder.to(ctx.getAddressRepo().getAddress(rs.getString("recipient")));
builder.from(ctx.getAddressRepository().getAddress(rs.getString("sender")));
builder.to(ctx.getAddressRepository().getAddress(rs.getString("recipient")));
builder.sent(rs.getLong("sent"));
builder.received(rs.getLong("received"));
builder.status(Plaintext.Status.valueOf(rs.getString("status")));
@ -173,12 +188,12 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
public void save(Plaintext message) {
// save from address if necessary
if (message.getId() == null) {
BitmessageAddress savedAddress = ctx.getAddressRepo().getAddress(message.getFrom().getAddress());
BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message.getFrom().getAddress());
if (savedAddress == null || savedAddress.getPrivateKey() == null) {
if (savedAddress != null && savedAddress.getAlias() != null) {
message.getFrom().setAlias(savedAddress.getAlias());
}
ctx.getAddressRepo().save(message.getFrom());
ctx.getAddressRepository().save(message.getFrom());
}
}
@ -219,7 +234,7 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
private void insert(Connection connection, Plaintext message) throws SQLException, IOException {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO Message (iv, type, sender, recipient, data, sent, received, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO Message (iv, type, sender, recipient, data, sent, received, status, initial_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS);
ps.setBytes(1, message.getInventoryVector() != null ? message.getInventoryVector().getHash() : null);
ps.setString(2, message.getType().name());
@ -229,6 +244,7 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito
ps.setLong(6, message.getSent());
ps.setLong(7, message.getReceived());
ps.setString(8, message.getStatus() != null ? message.getStatus().name() : null);
ps.setBytes(9, message.getInitialHash());
ps.executeUpdate();

View File

@ -0,0 +1,93 @@
package ch.dissem.bitmessage.repository;
import ch.dissem.bitmessage.entity.ObjectMessage;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
import ch.dissem.bitmessage.utils.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.LinkedList;
import java.util.List;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* @author Christian Basler
*/
public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWorkRepository {
private static final Logger LOG = LoggerFactory.getLogger(JdbcProofOfWorkRepository.class);
public JdbcProofOfWorkRepository(JdbcConfig config) {
super(config);
}
@Override
public Item getItem(byte[] initialHash) {
try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement("SELECT data, version, nonce_trials_per_byte, extra_bytes FROM POW WHERE initial_hash=?");
ps.setBytes(1, initialHash);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
Blob data = rs.getBlob("data");
return new Item(
Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()),
rs.getLong("nonce_trials_per_byte"),
rs.getLong("extra_bytes")
);
} else {
throw new RuntimeException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash));
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
@Override
public List<byte[]> getItems() {
try (Connection connection = config.getConnection()) {
List<byte[]> result = new LinkedList<>();
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT initial_hash FROM POW");
while (rs.next()) {
result.add(rs.getBytes("initial_hash"));
}
return result;
} catch (SQLException e) {
LOG.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
@Override
public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) {
try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement("INSERT INTO POW (initial_hash, data, version, nonce_trials_per_byte, extra_bytes) VALUES (?, ?, ?, ?, ?)");
ps.setBytes(1, security().getInitialHash(object));
writeBlob(ps, 2, object);
ps.setLong(3, object.getVersion());
ps.setLong(4, nonceTrialsPerByte);
ps.setLong(5, extraBytes);
ps.executeUpdate();
} catch (SQLException e) {
LOG.debug("Error storing object of type " + object.getPayload().getClass().getSimpleName(), e);
throw new RuntimeException(e);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
@Override
public void removeObject(byte[] initialHash) {
try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement("DELETE FROM POW WHERE initial_hash=?");
ps.setBytes(1, initialHash);
ps.executeUpdate();
} catch (SQLException e) {
LOG.debug(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,2 @@
ALTER TABLE Message ADD COLUMN initial_hash BINARY(64);
ALTER TABLE Message ADD CONSTRAINT initial_hash_unique UNIQUE(initial_hash);

View File

@ -0,0 +1,7 @@
CREATE TABLE POW (
initial_hash BINARY(64) PRIMARY KEY,
data BLOB NOT NULL,
version BIGINT NOT NULL,
nonce_trials_per_byte BIGINT NOT NULL,
extra_bytes BIGINT NOT NULL
);

View File

@ -91,7 +91,7 @@ public class SecurityTest {
security.doProofOfWork(objectMessage, 1000, 1000,
new ProofOfWorkEngine.Callback() {
@Override
public void onNonceCalculated(byte[] nonce) {
public void onNonceCalculated(byte[] initialHash, byte[] nonce) {
waiter.setValue(nonce);
}
});

View File

@ -13,3 +13,5 @@ include 'wif'
include 'security-sc'
include 'security-bc'
include 'extensions'

View File

@ -39,6 +39,7 @@ public class WifExporterTest {
.networkHandler(mock(NetworkHandler.class))
.inventory(mock(Inventory.class))
.messageRepo(mock(MessageRepository.class))
.powRepo(mock(ProofOfWorkRepository.class))
.nodeRegistry(mock(NodeRegistry.class))
.addressRepo(repo)
.build();
@ -72,14 +73,14 @@ public class WifExporterTest {
@Test
public void testAddIdentity() throws Exception {
String expected = "[BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn]\n" +
"label = Nuked Address\n" +
"enabled = true\n" +
"decoy = false\n" +
"noncetrialsperbyte = 320\n" +
"payloadlengthextrabytes = 14000\n" +
"privsigningkey = 5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9\n" +
"privencryptionkey = 5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck\n\n";
String expected = "[BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn]" + System.lineSeparator() +
"label = Nuked Address" + System.lineSeparator() +
"enabled = true" + System.lineSeparator() +
"decoy = false" + System.lineSeparator() +
"noncetrialsperbyte = 320" + System.lineSeparator() +
"payloadlengthextrabytes = 14000" + System.lineSeparator() +
"privsigningkey = 5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9" + System.lineSeparator() +
"privencryptionkey = 5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck" + System.lineSeparator() + System.lineSeparator();
importer = new WifImporter(ctx, expected);
exporter.addIdentity(importer.getIdentities().get(0));
assertEquals(expected, exporter.toString());

View File

@ -42,6 +42,7 @@ public class WifImporterTest {
.networkHandler(mock(NetworkHandler.class))
.inventory(mock(Inventory.class))
.messageRepo(mock(MessageRepository.class))
.powRepo(mock(ProofOfWorkRepository.class))
.nodeRegistry(mock(NodeRegistry.class))
.addressRepo(repo)
.build();